このエントリーは、GMOアドマーケティング Advent Calendar 2018
(https://qiita.com/advent-calendar/2018/gmo-am) の 【12/09】 の記事です。
GMOアドマーケティングとしては初のAdvent Calendar参戦です。
はじめに
Go言語では、Table Driven Testという形式でテストコードを書くのが一般的です。
Goエンジニア(Gopher)の間では、「Goらしさ」という言葉がよく使われていて、
ソースコードはいつもGoらしく書くことが求められます。
Goらしさという言葉には曖昧なところが少しあると思いますが、
Table Driven Testは、Goらしさの一つです。
今回は、GoらしさをJavaに持ち込んでみた例を紹介します。
Table Driven Testとは
Table Driven Testは、以下のページで解説されています。
https://github.com/golang/go/wiki/TableDrivenTests
Table Driven Testではテストケースを構造体で表し、それをloopさせてテストを実行します。
今回は説明のために、pow.goのテストを部分的に作成しました。
package main
import (
"testing"
"math"
)
func TestPow(t *testing.T) {
for _, tt := range []struct {
name string
x float64
y float64
expected float64
}{
{"Pow(2,1)", 2, 1, 2},
{"Pow(2,2)", 2, 2, 4},
{"Pow(2,3)", 2, 3, 8},
{"Pow(2,0)", 2, 0, 1},
{"Pow(2,-1)", 2, -1, 0.5},
{"Pow(0,2)", 0, 2, 0},
{"Pow(2,+Inf)", 2, math.Inf(0), math.Inf(0)},
{"Pow(2,-Inf)", 2, math.Inf(-1), 0},
{"Pow(+Inf,2)", math.Inf(0), 2, math.Inf(0)},
{"Pow(-Inf,2)", math.Inf(-1), 2, math.Inf(0)},
} {
t.Run(tt.name, func(t *testing.T) {
result := math.Pow(tt.x, tt.y)
if result != tt.expected {
t.Errorf("expected: %v, result: %v", tt.expected, result)
}
})
}
}
この例では、構造体に、テスト名、メソッドの引数、期待する値を定義しています。
このように書くことで、それぞれの値が何を表しているか、
また、何をテストしているかが分かりやすくなっています。
Javaで実装するTable Driven Test
Table Driven TestをJavaで実装するために、JUnit 5で追加された機能を使います。
今回はMathクラスのpowのテストを作成しました。
例ではソースコードを簡潔にするために、lombokを使用しています。
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.experimental.Accessors;
import org.junit.jupiter.api.*;
import java.util.stream.Stream;
import static org.junit.jupiter.api.Assertions.assertEquals;
class MathTest {
@Nested
static class Pow {
@AllArgsConstructor
@Getter
@Accessors(fluent = true)
static class TestCase {
private String name;
private double x;
private double y;
private double expected;
}
@TestFactory
Stream<DynamicNode> testPow() {
return Stream.of(
new TestCase("pow(2,1)", 2, 1, 2),
new TestCase("pow(2,2)", 2, 2, 4),
new TestCase("pow(2,3)", 2, 3, 8),
new TestCase("pow(2,0)", 2, 0, 1),
new TestCase("pow(2,-1)", 2, -1, 0.5),
new TestCase("pow(2,+Inf)", 2, Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY),
new TestCase("pow(2,-Inf)", 2, Double.NEGATIVE_INFINITY, 0),
new TestCase("pow(+Inf,2)", Double.POSITIVE_INFINITY, 2, Double.POSITIVE_INFINITY),
new TestCase("pow(-Inf,2)", Double.NEGATIVE_INFINITY, 2, Double.POSITIVE_INFINITY)
).map(testCase -> DynamicTest.dynamicTest(
testCase.name(),
() -> {
double result = Math.pow(testCase.x(), testCase.y());
assertEquals(testCase.expected(), result);
})
);
}
}
}
JUnit 5では、@Nested
をつけた内部クラスを定義して、テストをネストさせることができます。
今回の例では、テストをPowという内部クラスで定義しています。
Powの中には、TestCaseという内部クラスを定義しています。
TestCaseのフィールドには、テスト名、メソッドに渡す引数、期待する結果を定義しています。
各テストケースで使用する値は、TestCaseのコンストラクタで渡しています。
ここでTupleのようなクラスを使用すると、
Tupleに渡したそれぞれの値が何を表すかが分かりづらくなってしまいますが、
テストケースを例のような内部クラスに持たせることで、
名前のついたフィールドに値を紐づけることができます。
テストの実行には、JUnit 5の新機能のDynamic Testsを使用しています。
@TestFactory
をDynamicNodeのStreamやCollectionなどを返すメソッドにつけることで、
テストを動的に実行することができます。
まとめ
JUnit 5の機能とGo言語の良さを合わせることで、
Javaで可読性の高いテストを書くことができました。
今後もGoらしさのような他の言語の良いところを、うまくJavaに取り込んでいけると良いです。
クリスマスまで続くGMOアドマーケティング Advent Calendar 2018
ぜひ今後も投稿をウォッチしてください!
■エンジニアによるTechblog公開中!
https://techblog.gmo-ap.jp/
■Wantedlyページ ~ブログや求人を公開中!~
https://www.wantedly.com/projects/199431
■エンジニア採用ページ ~福利厚生や各種制度のご案内はこちら~
https://www.gmo-ap.jp/engineer/
■エンジニア学生インターン募集中! ~有償型インターンで開発現場を体験しよう~
https://hrmos.co/pages/gmo-ap/jobs/0000027