この記事はAndroidその2 Advent Calendar 2016 - Qiita 17日目の記事です。
今日はテストコードを保守していく際に役立つパターンの1つとして、
Test Data Builderパターンを紹介したいと思います。
前提
実際の開発時に、
モデル(値オブジェクトなど)を使ったクラスのテストコードを書くことが多々あると思います。
今回はそのようなケースを想定して、値オブジェクトをUser
、それを使ったクラスをUserRepository
をサンプルとして話を進めます。
// モデル
final class User {
private final long id;
private final String name;
private final int age;
private final Gender gender;
private final String avatarUrl;
private final Email email;
User(long id, String name, int age, Gender gender,
String avatarUrl, Email email) {
this.id = id;
this.name = name;
this.age = age;
this.gender = gender;
this.avatarUrl = avatarUrl;
this.email = email;
}
}
// モデルを使ったクラス
class UserRepository {
User findBy(long id) {
return null; // TODO
}
List<User> findAll() {
return null; // TODO
}
void store(User user) {
// TODO
}
}
パターンを用いないテスト方法
このRepository
のテストを書こうとしたとき、大体は以下のような感じになると思います。
public class UserRepositoryTest {
private UserRepository repo;
@Before public void setUp() {
repo = new UserRepository();
}
@Test public void store() {
long id = 100;
User expected = new User(id, "", 1, Gender.MALE, "", new Email(""));
repo.store(expected);
User actual = repo.findBy(id);
assertThat(actual, is(equalTo(expected)));
}
@Test public void findAll() {
repo.store(new User(1, "", 1, Gender.MALE, "", new Email("")));
repo.store(new User(2, "", 1, Gender.MALE, "", new Email("")));
List<User> actual = repo.findAll();
assertThat(actual.size(), is(2));
}
}
問題点
このテストコードにおいての問題点は、User
クラスのインスタンス化の部分だと私は考えています。
以下がその理由です。
-
User
クラスのコンストラクタの引数が多く、コードが見づらい-
1
や""
のような意味を持たない記述が多い
-
-
User
クラスのどの変数に着目したテストなのか、他人から見るとわかりづらい- 重要なのは
id
?それともすべて?
- 重要なのは
-
User
クラスのコンストラクタの変更に脆い- コンストラクタを使用しているテストコードすべてを修正しないとテストが実行できない
いずれもテストコードを長期間運用していくには負債となる可能性があります。
パターンを用いたテスト方法
上記の問題点に対処するため、テストのためのUser
クラスのデータを作ることに特化したクラスを用意します。
これをTest Data Builderパターンと呼んでいます。
ポイントはこのクラスのメンバ変数にデフォルト値がセットされている ことです。
class UserDataBuilder {
private long id = 1;
private String name = "name";
private int age = 20;
private Gender gender = Gender.MALE;
private String avatarUrl = "avatarUrl";
private Email email = new EmailDataBuilder().build();
public User build() {
return new User(id, name, age, gender, avatarUrl, email);
}
public UserDataBuilder withId(long id) {
this.id = id;
return this;
}
public UserDataBuilder withName(String name) {
this.name = name;
return this;
}
... 省略 ...
public UserDataBuilder withEmail(Email email) {
this.email = email;
return this;
}
}
※ このコードはテスト環境のみ参照可能で、実際のプロダクション環境には含まれません。
このUserDataBuilder
クラスを利用して、先程のテストコードを修正すると以下のようになります。
public class UserRepositoryTest {
private UserRepository repo;
@Before public void setUp() {
repo = new UserRepository();
}
@Test public void store() {
long id = 100;
User expected = new UserDataBuilder().withId(id).build();
repo.store(expected);
User actual = repo.findBy(id);
assertThat(actual, is(equalTo(expected)));
}
@Test public void findAll() {
UserDataBuilder user = new UserDataBuilder();
repo.store(user.withId(1).build());
repo.store(user.withId(2).build());
List<User> actual = repo.findAll();
assertThat(actual.size(), is(2));
}
}
パターンを適用した結果
いかがでしょうか。
パターンを用いないテストコードに比べると、こちらのコードは
-
User
クラスの冗長なコンストラクタが消え、見やすい -
id
という変数に着目したテストであることは、他人が見ても明らか -
User
クラスのコンストラクタに強い-
UserDataBuilder
クラスの変更だけで済む
-
というようなメリットがあると思います。
まとめ
今回の例で使ったUser
クラスは、比較的インスタンス化が簡単なオブジェクトでした。
しかし実際の開発では、依存しているクラスが多く、テストデータの用意が難しいクラスを用意するケースが多々あります。
そういった時に、
- 自分も他人も読みやすく
- 後々の変更にも耐えやすい
というテストコードを書くために、
このTest Data Builderパターンを適用してみてはいかがでしょうか。