2022年1月12日
Continuous Delivery
ビルダーファクトリーパターンは、テストの記述と保守が、手動のテストよりも確実 で簡単になるように支援します。私たちがどう活用したかをご覧ください。

中規模から大規模のコードベースでは、テストを書くことが重要です。しかし、テストにはメンテナンスコストがかかります。書くのも維持するのも大変なテストがあると、開発者はテストを書かなくなったり、自分のPRをマージしてもらうために必要な最低限のテストしか書かなくなったりします。厳密なコードレビューやコードカバレッジツールなどを追加してみても、実際の問題を解決できるわけではありません。それどころか、テストがない状態よりも悪い状態に陥ってしまう可能性すらあります。
全員にテストを書くことを奨励する最善の方法は、短期的でも(現在の開発サイクルでも)、テストを書いてメンテナンスすることが手動テストより簡単だと証明することです。
この投稿では、私たちがテストの作成とメンテナンスを容易にした方法の一つをご紹介します。言語としてJava、Builderの生成にLombok、依存性の注入にGuiceを使用しています。
どのテストも以下の3つのステ ップを経ています。
大きな課題の一つは、テストのセットアップです。テストの設定は、書きやすく、読みやすく、保守しやすいものであることが望まれます。
さらに、良いコードベースの特性は、常に物事を整理し、リファクタリングすることができることです。しかし、もしテストをセットアップする良い方法がなければ、あるサービスやPOJO(Plain Old Java Object)のリファクタリングや改良のたびに、多くのテストのセットアップが崩れてしまうでしょう。そのため、リファクタリングにより大きな手間がかかってしまい、実施する頻度も低くなります。
他のサービスや他のJavaオブジェクト(DTO、Bean、ORMエンティティーなど)の存在に依存するサービスメソッドをテストしようとする場合、これらの有効なサービスやBeanを全て作らなければならないため、テストのための設定を記述することが難しくなります。
一方、Javaオブジェクトを共有し、オブジェクトを作成するヘルパークラスを作成する場合、テストに基づいてプロパティをオーバーライドすることが難しくなります。
そのために役立つのが、次の4つのポイントです。
上記の問題を解決する1つの方法は、ビルダーのファクトリー(デフォルトのオブジェクトでビルダーを作成する)を使用し、テストに必要なプロパティをオーバーライドできるようにすることです。
これにより、デフォルトのオブジェクトを共有しつつ、 テストでプロパティをオーバーライドするための特定のロジックを持つことができ、 両者の長所を活かすことができます。
例を見てみましょう。以下のプロパティを持つ VerifyStepTask クラスがあります。accountId、orgIdentifier、projectIdentifier など、複数のオブジェクトで共有される共通プロパティを持っていることに注意してください。
@Builder
@Value
public static class VerifyStepTask {
String accountId;
String orgIdentifier;
String projectIdentifier;
String name;
String callbackId;
String serviceIdentifier;
String environmentIdentifier;
boolean skip;
Status status;
public enum Status { IN_PROGRESS, DONE }
}では、基本的なBuilderFactoryクラスを定義してみましょう。
@Value
@Builder
public static class BuilderFactory {
@Getter @Setter(AccessLevel.PRIVATE) Clock clock;
@Getter @Setter(AccessLevel.PRIVATE) Context context;
public static BuilderFactory getDefault() {
return BuilderFactory.builder()
.clock(Clock.fixed(Instant.parse("2020-04-22T10:00:00Z"), ZoneOffset.UTC))
.context(Context.defaultContext())
.build();
}
@Value
@Builder
public static class Context {
String accountId;
String orgIdentifier;
String projectIdentifier;
String serviceIdentifier;
String envIdentifier;
public static Context defaultContext() {
return Context.builder()
.accountId(randomAlphabetic(20))
.orgIdentifier(randomAlphabetic(20))
.projectIdentifier(randomAlphabetic(20))
.envIdentifier(randomAlphabetic(20))
.serviceIdentifier(randomAlphabetic(20))
.build();
}
}
public VerifyStepTask.VerifyStepTaskBuilder verifyStepTaskBuilder() {
return VerifyStepTask.builder()
.accountId(context.getAccountId())
.orgIdentifier(context.getOrgIdentifier())
.projectIdentifier(context.getProjectIdentifier())
.serviceIdentifier(context.getServiceIdentifier())
.environmentIdentifier(context.getEnvIdentifier())
.callbackId(generateUuid())
.status(VerifyStepTask.Status.IN_PROGRESS)
.skip(false);
}
}これは、複数の通話で共有できる基本的なパラメータを全て備えています。
Clock – テスト用の固定時計です。
Context – ユースケースに応じて、異なるビルダー間で共有可能なContextを定義します。この場合、Context は accountId, orgIdentifier, projectIdentifier を持つので、全てのオブジェクトは単一のテストコンテキストに対して同じ accountId, orgIdentifier, projectIdentifier を持つことになります。このアイデアは、異なるオブジェクト間のビルダーで使用できる共通の共有プロパティをコンテキストに置くことです。
では、実際のテストでビルダーファクトリーをどう使うかを見てみましょう。
public class VerifyStepTaskServiceTest {
@Inject private VerifyStepTaskService verifyStepTaskService;
BuilderFactory builderFactory;
@Before
public void setup() throws IllegalAccessException {
builderFactory = BuilderFactory.getDefault();
}
@Test
@Category(UnitTests.class)
public void testCreate() {
String activityId = generateUuid();
verifyStepTaskService.create(
builderFactory.cvngStepTaskBuilder().activityId(activityId). callbackId(activityId).build());
assertThat(verifyStepTaskService.get(activityId)).isNotNull();
}
@Test
@Category(UnitTests.class)
public void testCreate_withSkip() {
String callbackId = generateUuid();
verifyStepTaskService.create(builderFactory.cvngStepTaskBuilder().callbackId(callbackId).skip(true).build());
assertThat(verifyStepTaskService.get(callbackId)).isNotNull();
}
}これは例示のための単純なテストですが、テストが簡潔で、現在のテストロジックに関連するフィールドにのみ関係していることが分かります。しかし、将来的に新しいプロパティを追加したり、追加の検証ロジックに基づいてデフォルトビルダーを更新したりする柔軟性は保たれています。これは、より複雑なシナリオでも機能します。
例えば、VerifyStepTaskに新しい必須プロパティを追加するとします。その場合、VerifyStepTaskBuilder メソッドと、新しいプロパティのデフォルト値を取得する VerifyStepTask を使用する全てのテストを変更するだけでよいのです。
このパターンを使用するメリットを紹介します。
留意点
ユニットテストを書きやすくするためのステップを踏むことは、設計に劇的な効果をもたらします。これは、よりテストしやすいコードを書くようになったときにも言えることです。もしまだこのようなものを使っていないのであれば、コードベースのテストにビルダーファクトリーパターンを試してみてはいかがでしょうか。
関連するトピックとして、Test IntelligenceとContinuous Integration Testingの記事もご覧ください。
この記事はHarness社のウェブサイトで公開されているものをDigital Stacksが日本語に訳したものです。無断複製を禁じます。原文はこちらです。