新たなSubjectの作り方を紹介します。
必要なテストができなくても諦める必要はありません。Subjectを自作して、テストができる環境を作ってしまいましょう!
Subjectとは
アサーション方式のテストは検証項目の主張を幾つも繰り返していき、全ての主張が正しかったら対象(アプリのこと)を問題なしと判断します。
「AAはBBである」と主張する ⇒正しい 「CCはDDを含む」と主張する ⇒正しい 「EEはFFを持つ」と主張する ⇒正しい ⇒全部「正しい」ならば問題なし : 「YYはZZと同じ」と主張する ⇒正しい ^^^^^^^^ 検証項目
主張する範囲や内容によって問題あり・なしの基準は変わってきます。それはアプリの開発方針で決まります。
Truthはアサーションを次のような構文で表現します。
例:
num = 100
assertThat(num).isEqualTo(100) 「numは100である(と等しい)」
^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^ ^
IntegerSubject
検証項目「○○は△△である」が一つの????Subjectクラスで構成されます。
例は検証対象(num)がInetgerクラスなのでアサーションを構成するのはIntegerSubjectクラスです。
このように、Truthでは検証対象のクラス毎に????Subjectがあります。
そして、Subjectの中にテスト(isEqualToなど)を行う関数(メソッド)が列記されています。
アサーションの構文の構造はこれだけなのでシンプルです。
IntegerSubject-+-IsEqualTo
+-IsInstanceOf
+-他のテスト
FloatSubject---+-【同じ】
BooleanSubject-+-【同じ】
:
:
Subjectの作成
Subjectの作成方法を紹介します。
サンプルのPersonクラスを対象にPersonSubjectを作ってみましょう。
class Person(val name: String, val sex: Int, val age: Int) {
companion object {
const val MALE = 0
const val FEMALE = 1
}
override fun toString(): String {
return "${name}(${age}, ${if (sex == MALE) "M" else "F"})"
}
}
Personクラスは名前・性別・年齢の3つの属性を持っていて個人を定義します
public class Person {
public static final int MALE = 0;
public static final int FEMALE = 1;
private String mName;
private int mSex;
private int mAge;
public Person(String name, int sex, int age) {
mName = name;
mSex = sex;
mAge = age;
}
public String getName() {
return mName;
}
public int getSex() {
return mSex;
}
public int getAge() {
return mAge;
}
@Override
public String toString() {
return String.format(
"%s(%d, %s)",
mName, mAge, mSex == MALE ? "M" : "F");
}
}
Personクラスは名前・性別・年齢の3つの属性を持っていて個人を定義します
PersonSubjectはSubjectクラスを継承して作ります。
Subjectクラスは主に3つの部分を持ちます。
【ファクトリー(assetThat)】
SubjectはassetThat関数をトリガにしてインスタンスを作成するファクトリ関数(ほぼ定型)が必要になります。この部分はすべてのSubjectで同じ記述が登場します。ちょっと無駄に思える部分です。
ファクトリメソッドによりインスタンスを作成するので、コンストラクタがprivateである点に注意してください。
【テスト】
テスト関数は現実値(assertThatの引数、actual()で取得可能)と期待値(テスト関数の引数)を評価して、主張の通り「△△である」と判定されれば主張は「正しい」となり、何もしません。「△△でない」と判定されれば主張は「間違い」となり、failWithActual関数を実行します。
failWithActual関数を実行した時点でテストは終了です。failWithActual関数の後に記述したプログラムは実行されないので注意してください。
【他のへ引き継ぎ】
判定を他のSubjectに引き継ぐことも可能です。その場合はthat()を使います。
class PersonSubject
private constructor(metadata: FailureMetadata, private val actual: Person)
: Subject(metadata, actual) {
// ----- テスト
fun isEqualTo(person: Person) {
if (actual.name != person.name
|| actual.sex != person.sex
|| actual.age != person.age) {
failWithActual("is equal to", person.toString());
}
}
// ----- 他へ引き継ぎ
fun age(): IntegerSubject {
return check("age").that(actual.age)
}
fun sex(): IntegerSubject {
return check("sex").that(actual.sex)
}
fun name(): StringSubject {
return check("name").that(actual.name)
}
// ----- ファクトリー
companion object {
fun assertThat(actual: Person): PersonSubject { // ... トリガー
return Truth.assertAbout(persons()).that(actual)
}
fun persons(): Factory<PersonSubject, Person> {
return Factory { // SAM変換が行われている
metadata, actual -> PersonSubject(metadata, actual)
}
}
// 以下はSAM変換が行われる前の原形
// fun persons(): Factory<PersonSubject, Person> {
// return object: Factory<PersonSubject, Person> {
// override fun createSubject(metadata: FailureMetadata, actual: Person)
// :PersonSubject {
// return PersonSubject(metadata, actual)
// }
// }
// }
}
}
※com.google.truth:truth:1.0対応
public class PersonSubject extends Subject {
private final Person actual;
private PersonSubject(FailureMetadata metadata, Person actual) {
super(metadata, actual);
this.actual = actual;
}
// ----- テスト
public void isEqualTo(Person person) {
if(actual.getName() != person.getName()
|| actual.getSex() != person.getSex()
|| actual.getAge() != person.getAge()) {
failWithActual("is equal to", person.toString());
}
}
// ----- 他へ引き継ぎ
public IntegerSubject age() {
return check("age").that(actual.getAge());
}
public IntegerSubject sex() {
return check("sex").that(actual.getSex());
}
public StringSubject name() {
return check("name").that(actual.getName());
}
// ----- ファクトリー
public static PersonSubject assertThat(Person actual) { // ... トリガー
return Truth.assertAbout(persons()).that(actual);
}
public static Factory<PersonSubject, Person> persons() {
return PersonSubject::new;
}
}
※com.google.truth:truth:1.0対応
※Java8のコンストラクタ参照(::newの部分)を使用
メソッドが一つしかないJavaの抽象クラスを、無名クラスの形でメソッドの引数へ渡す場合に、Kotlinのラムダ式へ置き換える変換をいいます。よくある例はView#setOnClickListener(new View.OnClickListener {…})でしょう。
ただし、この変換が発生するのはJavaの抽象クラスをKotlinで使用する時のみです。Kotlinの抽象クラスにSAM変換はありません。
SAM変換を行うとクラス名とメソッド名がごっそりと省略されてしまいます。実装対象が明確(メソッドが一つしかない)なので、不要な記述は消されてしまうのです。省略前のJavaコードを知らない人には把握しにくい記述だと思うのですが…
サブジェクトの使用方法
作成したサブジェクトの使用方法を示します。
class Students_Test {
lateinit private var mStudents: Array<Person>
@Before
fun setUp() {
mStudents = arrayOf(
Person("Yamada", Person.MALE, 12),
Person("Suzuki", Person.FEMALE, 15),
Person("Suzuki", Person.MALE, 17)
)
}
@Test
fun 所属する生徒を調べる() {
assertThat(mStudents[0]).isEqualTo(Person("Yamada", Person.MALE, 12))
assertThat(mStudents[1]).age().isEqualTo(15)
assertThat(mStudents[1]).sex().isEqualTo(Person.FEMALE)
assertThat(mStudents[1]).name().isEqualTo("Suzuki")
// ↓ 以下はFailする
assertThat(mStudents[2]).isEqualTo(Person("Zuzuki", Person.MALE, 17))
assertThat(mStudents[2]).age().isEqualTo(14)
}
}
public class Students_Test {
private Person[] mStudents;
@Before
public void setUp() {
mStudents = new Person[]{
new Person("Yamada", Person.MALE, 12),
new Person("Suzuki", Person.FEMALE, 15),
new Person("Suzuki", Person.MALE, 17)
};
}
@Test
public void 所属する生徒を調べる() {
assertThat(mStudents[0]).isEqualTo(new Person("Yamada", Person.MALE, 12));
assertThat(mStudents[1]).age().isEqualTo(15);
assertThat(mStudents[1]).sex().isEqualTo(Person.FEMALE);
assertThat(mStudents[1]).name().isEqualTo("Suzuki");
// ↓以下はFailする
assertThat(mStudents[2]).isEqualTo(new Person("Zuzuki", Person.MALE, 17));
assertThat(mStudents[2]).age().isEqualTo(14);
}
}
Fail時のコメントをもっと工夫すればデバッグしやすくなると思います。
is equal to: Zuzuki(17, M) but was : Suzuki(17, M) at Students_Test.所属する生徒を調べる(Students_Test.kt:32) ---- value of : person.age expected : 14 but was : 17 person was: Suzuki(17, M) Expected :14 Actual :17 at Students_Test.所属する生徒を調べる(Students_Test.kt:33)
Failの通知方法
先に示したサンプルはfailWithActual関数を使った通知を行いました。
その他、Failの通知方法には次のものがあります。
| 通知方法 | 動作 |
|---|---|
| failWithActual(String key, Object value) | 内部でfailWithActual(fact(key, value))が実行されている |
| failWithActual(Fact first, Fact... rest) | Actual付き 「key : value」を表示 〔ログ出力の例〕 is equal to: Zuzuki(17, M) … first but was : Suzuki(17, M) … actual |
| failWithoutActual(Fact first, Fact... rest) | Actualなし 「key : value」を表示 〔ログ出力の例〕 is equal to: Zuzuki(17, M) … first |
| simpleFact(String key) | Actualなし 「key」のみを表示 〔ログ出力の例〕 is equal to ... key |
faileWithActual/failWithoutActualは一つ以上のFactオブジェクトを引数へ指定しなければなりません。
引数restは可変長引数です。指定した場合は引数firstの下に列記されます。
is equal to: Zuzuki(17, M) ... first name : Zuzuki ... rest[0] sex : 0 ... rset[1] age : 17 ... rset[2] but was : Suzuki(17, M) ... actual
