Upgrade to PRO for Only $50/Year—Limited-Time Offer! 🔥

リファクタリングへの耐性が高いモデルベースの統合テストの紹介 / Model-Base Int...

YuitoSato
October 25, 2024

リファクタリングへの耐性が高いモデルベースの統合テストの紹介 / Model-Base Integration Test for Refactoring

YuitoSato

October 25, 2024
Tweet

More Decks by YuitoSato

Other Decks in Technology

Transcript

  1. © 2024 Loglass Inc. 佐藤 有斗(ゆいと) 株式会社ログラス プロダクト開発部 エンジニア 株式会社ログラスに2020年12月に入社。

    主に新規事業の立ち上げを担当。得意分野は型と自動テスト。 KotlinでたまにOSSを作ったり、他のOSSにコントリビュート したりしてます。 Yuito Sato 自己紹介
  2. © 2024 Loglass Inc. 1. はじめに ◦ メインテーマ 2. 統合テストの基礎知識

    ◦ 統合テストとは何か? ◦ 統合テストはなぜ必要なのか? 3. モデルベースの統合テスト ◦ モデルベースの統合テストとは何か? ◦ なぜGivenとThenをモデルベースに記述するべきなのか? 4. モデルベースのテストフィクスチャ ◦ 統合テストのデータ準備めんどくさい問題 ◦ 明示的なGiven、暗黙的なGiven アジェンダ
  3. © 2024 Loglass Inc. 1. はじめに 大規模なリファクタリング への挑戦 アプリケーションの リファクタリング耐性の向上

    2. 統合テストの基礎知識 3. モデルベースの統合テスト 4.モデルベースの テストフィクスチャ
  4. © 2024 Loglass Inc. 1. はじめに ◦ メインテーマ 2. 統合テストの基礎知識

    ◦ 統合テストとは何か? ◦ 統合テストはなぜ必要なのか? 3. モデルベースの統合テスト ◦ モデルベースの統合テストとは何か? ◦ なぜGivenとThenをモデルベースに記述するべきなのか? 4. モデルベースのテストフィクスチャ ◦ 統合テストのデータ準備めんどくさい問題 ◦ 明示的なGiven、暗黙的なGiven 2. 統合テストの基礎知識
  5. © 2024 Loglass Inc. 2. 統合テストの基礎知識 統合テスト(Integration Test)とは何か? • 一つ以上のプロセス外のシステムに依存するテスト

    ◦ プロセス外のシステム(ストレージ、メールサービス)と通信しながら実行するテスト
  6. © 2024 Loglass Inc. 2. 統合テストの基礎知識 統合テストと単体テストの違い • 統合テストは単体テストの3原則を満たせない ◦

    単体テストの3原則 1. 1単位の振る舞いを検証すること 2. 実行時間が短いこと 3. 他のテストケースから隔離された状態で実行されること
  7. © 2024 Loglass Inc. 2. 統合テストの基礎知識 テストピラミッド 統合テスト 単体テスト E2E

    統合するものの数(エンドユーザーの観点への近さ) テストケースの数
  8. © 2024 Loglass Inc. Repositoryについての補足 2. 統合テストの基礎知識 package com.example.sample.domain.tasks; public

    interface TaskRepository { void insert(Task task); void update(Task task); Optional<Task> findById(Integer id); } package com.example.sample.infrastructure; @Component public class TaskJdbcRepository implements TaskRepository { private final DSLContext jooq; public TaskJdbcRepository(DSLContext dsl) { this.jooq = dsl; } @Override public void insert(Task task) { … } @Override public void update(Task task) { … } @Override public Optional<Task> findById(Integer id) { … }
  9. © 2024 Loglass Inc. package com.example.sample.infrastructure; @Component public class TaskJdbcRepository

    implements TaskRepository { private final DSLContext jooq; public TaskJdbcRepository(DSLContext dsl) { this.jooq = dsl; } @Override public void insert(Task task) { … } @Override public void update(Task task) { … } @Override public Optional<Task> findById(Integer id) { … } package com.example.sample.domain.tasks; public interface TaskRepository { void insert(Task task); void update(Task task); Optional<Task> findById(Integer id); } Repositoryについての補足 2. 統合テストの基礎知識 Repository自体は Domain層に配置し、 実際のDBアクセスは Infrastructure層の 具象クラスで実装する
  10. © 2024 Loglass Inc. ログラスでは基本はApplication 〜 DBまでの統合テストを作ることが多い 2. 統合テストの基礎知識 Domain

    Application Infrastructure DB 統合テスト Presentation テストケースの数 ・ ロジックの複雑性
  11. © 2024 Loglass Inc. Application層の統合テスト 2. 統合テストの基礎知識 @SpringBootTest @Transactional public

    class UpdateTaskUseCaseIntegrationTest { @Autowired private DSLContext jooq; @Autowired private UpdateTaskUseCase updateTaskUseCase; @Test void testUpdateTask() { // given jooq.insertInto(TASKS, TASKS.ID, TASKS.TITLE, TASKS.DESCRIPTION) .values(1, "title", "description").execute(); // when updateTaskUseCase.execute(new UpdateTaskCommand(1, "new title", "new description")); // then Record3<Integer, String, String> actual = jooq .select(TASKS.ID, TASKS.TITLE, TASKS.DESCRIPTION) .from(TASKS).where(TASKS.ID.eq(1)).fetchOptional().get(); Assertions.assertEquals("new title", actual.get(TASKS.TITLE)); Assertions.assertEquals("new description", actual.get(TASKS.DESCRIPTION)); } }
  12. © 2024 Loglass Inc. @SpringBootTest @Transactional public class UpdateTaskUseCaseIntegrationTest {

    @Autowired private DSLContext jooq; @Autowired private UpdateTaskUseCase updateTaskUseCase; @Test void testUpdateTask() { // given jooq.insertInto(TASKS, TASKS.ID, TASKS.TITLE, TASKS.DESCRIPTION) .values(1, "title", "description").execute(); // when updateTaskUseCase.execute(new UpdateTaskCommand(1, "new title", "new description")); // then Record3<Integer, String, String> actual = jooq .select(TASKS.ID, TASKS.TITLE, TASKS.DESCRIPTION) .from(TASKS).where(TASKS.ID.eq(1)).fetchOptional().get(); Assertions.assertEquals("new title", actual.get(TASKS.TITLE)); Assertions.assertEquals("new description", actual.get(TASKS.DESCRIPTION)); } } Application層の統合テスト 2. 統合テストの基礎知識 各種必要なものをDI
  13. © 2024 Loglass Inc. @SpringBootTest @Transactional public class UpdateTaskUseCaseIntegrationTest {

    @Autowired private DSLContext jooq; @Autowired private UpdateTaskUseCase updateTaskUseCase; @Test void testUpdateTask() { // given jooq.insertInto(TASKS, TASKS.ID, TASKS.TITLE, TASKS.DESCRIPTION) .values(1, "title", "description").execute(); // when updateTaskUseCase.execute(new UpdateTaskCommand(1, "new title", "new description")); // then Record3<Integer, String, String> actual = jooq .select(TASKS.ID, TASKS.TITLE, TASKS.DESCRIPTION) .from(TASKS).where(TASKS.ID.eq(1)).fetchOptional().get(); Assertions.assertEquals("new title", actual.get(TASKS.TITLE)); Assertions.assertEquals("new description", actual.get(TASKS.DESCRIPTION)); } } Application層の統合テスト 2. 統合テストの基礎知識 DBに直接インサート
  14. © 2024 Loglass Inc. @SpringBootTest @Transactional public class UpdateTaskUseCaseIntegrationTest {

    @Autowired private DSLContext jooq; @Autowired private UpdateTaskUseCase updateTaskUseCase; @Test void testUpdateTask() { // given jooq.insertInto(TASKS, TASKS.ID, TASKS.TITLE, TASKS.DESCRIPTION) .values(1, "title", "description").execute(); // when updateTaskUseCase.execute(new UpdateTaskCommand(1, "new title", "new description")); // then Record3<Integer, String, String> actual = jooq .select(TASKS.ID, TASKS.TITLE, TASKS.DESCRIPTION) .from(TASKS).where(TASKS.ID.eq(1)).fetchOptional().get(); Assertions.assertEquals("new title", actual.get(TASKS.TITLE)); Assertions.assertEquals("new description", actual.get(TASKS.DESCRIPTION)); } } Application層の統合テスト 2. 統合テストの基礎知識 処理をコール
  15. © 2024 Loglass Inc. @SpringBootTest @Transactional public class UpdateTaskUseCaseIntegrationTest {

    @Autowired private DSLContext jooq; @Autowired private UpdateTaskUseCase updateTaskUseCase; @Test void testUpdateTask() { // given jooq.insertInto(TASKS, TASKS.ID, TASKS.TITLE, TASKS.DESCRIPTION) .values(1, "title", "description").execute(); // when updateTaskUseCase.execute(new UpdateTaskCommand(1, "new title", "new description")); // then Record3<Integer, String, String> actual = jooq .select(TASKS.ID, TASKS.TITLE, TASKS.DESCRIPTION) .from(TASKS).where(TASKS.ID.eq(1)).fetchOptional().get(); Assertions.assertEquals("new title", actual.get(TASKS.TITLE)); Assertions.assertEquals("new description", actual.get(TASKS.DESCRIPTION)); } } Application層の統合テスト 2. 統合テストの基礎知識 DBから更新後の データを取得し、 アサーション
  16. © 2024 Loglass Inc. Application層の単体テスト 2. 統合テストの基礎知識 public class UpdateTaskUseCaseUnitTest

    { @Test void updateTaskTest() { // given TaskRepository taskRepository = mock(TaskRepository.class); Task task = new Task(1, "title", "description"); when(taskRepository.findById(task.id())).thenReturn(Optional.of(task)); // when UpdateTaskUseCase updateTaskUseCase = new UpdateTaskUseCase(taskRepository); updateTaskUseCase.execute( new UpdateTaskCommand(task.id(), "new title", "new description") ); // then verify(taskRepository).update(new Task(1, "new title", "new description")); }
  17. © 2024 Loglass Inc. public class UpdateTaskUseCaseUnitTest { @Test void

    updateTaskTest() { // given TaskRepository taskRepository = mock(TaskRepository.class); Task task = new Task(1, "title", "description"); when(taskRepository.findById(task.id())).thenReturn(Optional.of(task)); // when UpdateTaskUseCase updateTaskUseCase = new UpdateTaskUseCase(taskRepository); updateTaskUseCase.execute( new UpdateTaskCommand(task.id(), "new title", "new description") ); // then verify(taskRepository).update(new Task(1, "new title", "new description")); } Application層の単体テスト 2. 統合テストの基礎知識 更新対象のデータを 内部的に取得するスタブを定義
  18. © 2024 Loglass Inc. public class UpdateTaskUseCaseUnitTest { @Test void

    updateTaskTest() { // given TaskRepository taskRepository = mock(TaskRepository.class); Task task = new Task(1, "title", "description"); when(taskRepository.findById(task.id())).thenReturn(Optional.of(task)); // when UpdateTaskUseCase updateTaskUseCase = new UpdateTaskUseCase(taskRepository); updateTaskUseCase.execute( new UpdateTaskCommand(task.id(), "new title", "new description") ); // then verify(taskRepository).update(new Task(1, "new title", "new description")); } Application層の単体テスト 2. 統合テストの基礎知識 処理をコール
  19. © 2024 Loglass Inc. public class UpdateTaskUseCaseUnitTest { @Test void

    updateTaskTest() { // given TaskRepository taskRepository = mock(TaskRepository.class); Task task = new Task(1, "title", "description"); when(taskRepository.findById(task.id())).thenReturn(Optional.of(task)); // when UpdateTaskUseCase updateTaskUseCase = new UpdateTaskUseCase(taskRepository); updateTaskUseCase.execute( new UpdateTaskCommand(task.id(), "new title", "new description") ); // then verify(taskRepository).update(new Task(1, "new title", "new description")); } Application層の単体テスト 2. 統合テストの基礎知識 モックとして意図したデータで update処理を呼んだか検証
  20. © 2024 Loglass Inc. 統合テストは遅いのでケースを絞り、単体テストで網羅的にテストをするのが基本 2. 統合テストの基礎知識 Domain Application Infrastructure

    DB Presentation 単体テスト 統合テスト ハッピーパス(正常系)の1ケースのみを 検証するのがおすすめ テストケースの数 ・ ロジックの複雑性
  21. © 2024 Loglass Inc. DB 統合テストのケースを減らすためにロジックをDomain層に集めるべし 2. 統合テストの基礎知識 Domain Application

    Infrastructure Presentation 単体テスト 統合テスト テストケースの数 ・ ロジックの複雑性
  22. © 2024 Loglass Inc. 2. 統合テストの基礎知識 統合テストはなぜ必要なのか? • リファクタリングへの耐性が高いため 1.

    統合テストはモックやスタブの依存が少なく、故に実装の詳細への依存が少ない i. 観察可能な振る舞いのみを検証しやすい 2. 複数のモジュールを統合した上で全体的な動きを検証できる
  23. © 2024 Loglass Inc. 2. 統合テストの基礎知識 リファクタリングへの耐性が高いとは何か? • リファクタリングをした時にテストコードが壊れづらいこと(偽陽性が出づらい) ◦

    リファクタリングとは、「振る舞いを変えずに実装の詳細を変更すること」 • リファクタリングへの耐性が高い = 実装の詳細への依存が少ない ◦ テストコードに実装の詳細への依存がなければリファクタリングしてもテストは壊れない
  24. © 2024 Loglass Inc. 2. 統合テストの基礎知識 観察可能な振る舞いとは何か? • クライアントが目標を達成するために使う公開された状態 or

    操作 ID1のタスクを更新したい! (given, when) UpdateTaskUseCase ID1のタスクが 更新された値になっている! (then)
  25. © 2024 Loglass Inc. 2. 統合テストの基礎知識 観察可能な振る舞いとは何か? • クライアントが目標を達成するために使う公開された状態 or

    操作 ID1のタスクが 更新された値になっている! (then) ID1のタスクを更新したい! (given, when) UpdateTaskUseCase 公開された操作 公開された状態
  26. © 2024 Loglass Inc. 2. 統合テストの基礎知識 実装の詳細とは何か? • クライアントが目標を達成するために使う公開された状態 or

    操作 ではないもの ID1のタスクが 更新された値になっている! (then) ID1のタスクを更新したい! (given, when) UpdateTaskUseCase 公開された操作 公開された状態 データをどこから 取得している? バリデーションはどのライブ ラリを使っている? 実装の詳細 実装の詳細
  27. © 2024 Loglass Inc. 2. 統合テストの基礎知識 なぜ統合テストはリファクタリングへの耐性が高いのか? 1. => モックやスタブ(実装の詳細になることが多い)を使わずにテストをしているから

    ◦ 観察可能な振る舞いを検証しているケース UpdateTaskUseCase ID1のタスクを更新したい! (given, when) TaskRepository テストコード から見えない ID1のタスクが 更新された値になっている! (then)
  28. © 2024 Loglass Inc. @Test void testUpdateTask() { // given

    jooq.insertInto(TASKS, TASKS.ID, TASKS.TITLE, TASKS.DESCRIPTION) .values(1, "title", "description").execute(); // when updateTaskUseCase.execute(new UpdateTaskCommand(1, "new title", "new description")); // then Record3<Integer, String, String> actual = jooq .select(TASKS.ID, TASKS.TITLE, TASKS.DESCRIPTION) .from(TASKS).where(TASKS.ID.eq(1)).fetchOptional().get(); Assertions.assertEquals("new title", actual.get(TASKS.TITLE)); Assertions.assertEquals("new description", actual.get(TASKS.DESCRIPTION)); } Application層の統合テスト 2. 統合テストの基礎知識 ID1のタスクを更新したい! (given, when)
  29. © 2024 Loglass Inc. @Test void testUpdateTask() { // given

    jooq.insertInto(TASKS, TASKS.ID, TASKS.TITLE, TASKS.DESCRIPTION) .values(1, "title", "description").execute(); // when updateTaskUseCase.execute(new UpdateTaskCommand(1, "new title", "new description")); // then Record3<Integer, String, String> actual = jooq .select(TASKS.ID, TASKS.TITLE, TASKS.DESCRIPTION) .from(TASKS).where(TASKS.ID.eq(1)).fetchOptional().get(); Assertions.assertEquals("new title", actual.get(TASKS.TITLE)); Assertions.assertEquals("new description", actual.get(TASKS.DESCRIPTION)); } Application層の統合テスト 2. 統合テストの基礎知識 ID1のタスクが 更新された値になっている! (then)
  30. © 2024 Loglass Inc. 2. 統合テストの基礎知識 なぜ統合テストはリファクタリングへの耐性が高いのか? 1. => モックやスタブ(実装の詳細になることが多い)を使わずにテストをしているから

    ◦ 実装の詳細に依存しているケース TaskRepositoryのupdate が意図した引数で呼ばれた! ID1でTaskRepositoryの findByを呼んだらID1のタスクを 返してほしい。 その上でID1のタスクを更新したい UpdateTaskUseCase TaskRepository テストコード から実装の詳細 が見えてしまう
  31. © 2024 Loglass Inc. 2. 統合テストの基礎知識 @Test void updateTaskTest() {

    // given TaskRepository taskRepository = mock(TaskRepository.class); Task task = new Task(1, "title", "description"); when(taskRepository.findById(task.id())).thenReturn(Optional.of(task)); // when UpdateTaskUseCase updateTaskUseCase = new UpdateTaskUseCase(taskRepository); updateTaskUseCase.execute( new UpdateTaskCommand(task.id(), "new title", "new description") ); // then verify(taskRepository).update(new Task(1, "new title", "new description")); } ID1でTaskRepositoryの findByを呼んだらID1のタスクを 返してほしい。 その上でID1のタスクを更新したい
  32. © 2024 Loglass Inc. @Test void updateTaskTest() { // given

    TaskRepository taskRepository = mock(TaskRepository.class); Task task = new Task(1, "title", "description"); when(taskRepository.findById(task.id())).thenReturn(Optional.of(task)); // when UpdateTaskUseCase updateTaskUseCase = new UpdateTaskUseCase(taskRepository); updateTaskUseCase.execute( new UpdateTaskCommand(task.id(), "new title", "new description") ); // then verify(taskRepository).update(new Task(1, "new title", "new description")); } 2. 統合テストの基礎知識 TaskRepositoryのupdate が意図した引数で呼ばれた!
  33. © 2024 Loglass Inc. 2. 統合テストの基礎知識 なぜApplication層の統合テストはリファクタリングへの耐性が高いのか? 2. => 複数のモジュールを統合した上で全体的な動きを検証できる

    ◦ 実装の詳細に依存していなければ内部の実装はリファクタリング可能 Domain Application Infrastructure DB Presentation 統合テスト この範囲がリファクタ可能
  34. © 2024 Loglass Inc. 2. 統合テストの基礎知識 統合テストまとめ • 統合テストとは ◦

    1つ以上のプロセス外依存をテストすること ◦ 統合テストは単体テストに比べて遅いため、少ないケース & 長いケースに使うべし • 統合テストはなぜ必要? ◦ リファクタリングへの耐性をあげるため i. 統合テストはモックやスタブの依存が少なく、故に実装の詳細への依存が少ない ii. 複数のモジュールを統合した上で全体的な動きを検証できる
  35. © 2024 Loglass Inc. 1. はじめに ◦ メインテーマ 2. 統合テストの基礎知識

    ◦ 統合テストとは何か? ◦ 統合テストはなぜ必要なのか? 3. モデルベースの統合テスト ◦ モデルベースの統合テストとは何か? ◦ なぜGivenとThenをモデルベースに記述するべきなのか? 4. モデルベースのテストフィクスチャ ◦ 統合テストのデータ準備めんどくさい問題 ◦ 明示的なGiven、暗黙的なGiven 3. モデルベースの統合テスト
  36. © 2024 Loglass Inc. 3. モデルベースの統合テスト モデルってなに? Domain Application Presentation

    Infrastructure • Domain層に属しているシステム固有のビジネス ロジックを持ったデータ ◦ 今回の例でいうとタスク ◦ DB上のテーブルとは切り離された概念
  37. © 2024 Loglass Inc. 3. モデルベースの統合テスト モデルベースの統合テスト @Test void testUpdateTask()

    { // given Task task = new Task("title", "description"); taskRepository.insert(task); // when updateTaskUseCase.execute( new UpdateTaskCommand(task.id(), "new title", "new description") ); // then Task updatedTask = taskRepository.findById(task.id()).get(); Assertions.assertEquals("new title", updatedTask.title()); Assertions.assertEquals("new description", updatedTask.description()); }
  38. © 2024 Loglass Inc. @Test void testUpdateTask() { // given

    Task task = new Task("title", "description"); taskRepository.insert(task); // when updateTaskUseCase.execute( new UpdateTaskCommand(task.id(), "new title", "new description") ); // then Task updatedTask = taskRepository.findById(task.id()).get(); Assertions.assertEquals("new title", updatedTask.title()); Assertions.assertEquals("new description", updatedTask.description()); } 3. モデルベースの統合テスト モデルベースの統合テスト モデルとリポジトリを使って データ準備 モデルとリポジトリを使って データを検証
  39. © 2024 Loglass Inc. 3. モデルベースの統合テスト モデルベースではない 統合テスト @Test void

    testUpdateTask() { // given jooq.insertInto(TASKS, TASKS.ID, TASKS.TITLE, TASKS.DESCRIPTION) .values(1, "title", "description").execute(); // when updateTaskUseCase.execute(new UpdateTaskCommand(1, "new title", "new description")); // then Record3<Integer, String, String> actual = jooq .select(TASKS.ID, TASKS.TITLE, TASKS.DESCRIPTION) .from(TASKS).where(TASKS.ID.eq(1)).fetchOptional().get(); Assertions.assertEquals("new title", actual.get(TASKS.TITLE)); Assertions.assertEquals("new description", actual.get(TASKS.DESCRIPTION)); }
  40. © 2024 Loglass Inc. @Test void testUpdateTask() { // given

    jooq.insertInto(TASKS, TASKS.ID, TASKS.TITLE, TASKS.DESCRIPTION) .values(1, "title", "description").execute(); // when updateTaskUseCase.execute(new UpdateTaskCommand(1, "new title", "new description")); // then Record3<Integer, String, String> actual = jooq .select(TASKS.ID, TASKS.TITLE, TASKS.DESCRIPTION) .from(TASKS).where(TASKS.ID.eq(1)).fetchOptional().get(); Assertions.assertEquals("new title", actual.get(TASKS.TITLE)); Assertions.assertEquals("new description", actual.get(TASKS.DESCRIPTION)); } 3. モデルベースの統合テスト モデルベースではない 統合テスト テーブルに対して直接 書き込み、データを準備している テーブルに対して直接 参照してデータを検証している
  41. © 2024 Loglass Inc. @Test void testUpdateTask() throws IOException {

    // given loadCSVData(TASKS, "src/test/resources/tasks.csv"); … } private void loadCSVData(Table tasks, String filePath) throws IOException { try (InputStream inputstream = new FileInputStream(filePath)) { jooq.loadInto(tasks) .loadCSV(inputstream, "UTF-8") .fields(tasks.fields()) .execute(); } } 3. モデルベースの統合テスト CSVによるインポートも モデルベースではない id title description 1 “title” “description”
  42. © 2024 Loglass Inc. 3. モデルベースの統合テスト なぜGivenとThenをモデルベースに記述するべきなのか? 1. ドメインに則したデータを作り、ドメインに則したデータで検証したいため a.

    あり得ないデータでテストしないようにする 2. テーブルという実装の詳細への依存を除去してリファクタリングへの耐性を上げる a. テーブルに依存しないのでテーブル自体のリファクタリングが可能
  43. © 2024 Loglass Inc. 3. モデルベースの統合テスト 1. ドメインに則したデータを作り、ドメインに則したデータで検証する @Test void

    testUpdateTask() { // given jooq.insertInto( TASKS, TASKS.ID, TASKS.TITLE, TASKS.DESCRIPTION ).values(1, "", "").execute(); … } public class Task { … public Task(String title, String description) { if (title.isEmpty()) { throw new IllegalArgumentException(…); } … } … }
  44. © 2024 Loglass Inc. @Test void testUpdateTask() { // given

    jooq.insertInto( TASKS, TASKS.ID, TASKS.TITLE, TASKS.DESCRIPTION ).values(1, "", "").execute(); … } public class Task { … public Task(String title, String description) { if (title.isEmpty()) { throw new IllegalArgumentException(…); } … } … } 3. モデルベースの統合テスト 1. ドメインに則したデータを作り、ドメインに則したデータで検証する ありえないテストデータで テストしてしまう可能性がある
  45. © 2024 Loglass Inc. 3. モデルベースの統合テスト ドメインに則したデータ準備 @Test void testUpdateTask()

    { // given Task task = new Task("", ""); taskRepository.insert(task); // when updateTaskUseCase.execute( new UpdateTaskCommand(task.id(), "new title", "new description") ); // then Task updatedTask = taskRepository.findById(task.id()).get(); Assertions.assertEquals("new title", updatedTask.title()); Assertions.assertEquals("new description", updatedTask.description()); } 空文字でタスクを作ろうとすると この時点でエラーになる。
  46. © 2024 Loglass Inc. 3. モデルベースの統合テスト 2. テーブルへの依存を除去してリファクタリングへの耐性を上げる • テーブルの実装の詳細に依存していないためテーブルのリファクタリングが可能

    ◦ 例) tasksテーブルのimage_urlカラムを別テーブルに分割する id title description image_url1 image_url2 1 買い物 … https://~/images/1 https://~/images/2 2 洗濯 … https://~/images/3 null id title description 1 買い物 … 2 洗濯 … task_id image_url 1 https://~/images/1 1 https://~/images/2 2 https://~/images/3 分割
  47. © 2024 Loglass Inc. 3. モデルベースの統合テスト @Test void testUpdateTask() {

    // given jooq.insertInto( TASKS, …, TASKS.IMAGE_URL1, TASKS.IMAGE_URL2 ).values(1, "title", "description", "image1", "image2").execute(); // when … // then Record5<Integer, String, String, String, String> actual = jooq.select( TASKS.ID, .., TASKS.IMAGE_URL1, TASKS.IMAGE_URL2) .from(TASKS).where(TASKS.ID.eq(1)) .fetchOptional().get(); Assertions.assertEquals("new title", actual.get(TASKS.TITLE)); Assertions.assertEquals(..., actual.get(TASKS.IMAGE_URL1)); テーブルに依存したテスト
  48. © 2024 Loglass Inc. @Test void testUpdateTask() { // given

    jooq.insertInto( TASKS, …, TASKS.IMAGE_URL1, TASKS.IMAGE_URL2 ).values(1, "title", "description", "image1", "image2").execute(); // when … // then Record5<Integer, String, String, String, String> actual = jooq.select( TASKS.ID, .., TASKS.IMAGE_URL1, TASKS.IMAGE_URL2) .from(TASKS).where(TASKS.ID.eq(1)) .fetchOptional().get(); Assertions.assertEquals("new title", actual.get(TASKS.TITLE)); Assertions.assertEquals(..., actual.get(TASKS.IMAGE_URL1)); エラーが起きてしまう (jooqだとコンパイルエラーになるのでまだマシ) テーブルに依存したテスト 3. モデルベースの統合テスト
  49. © 2024 Loglass Inc. モデルベースのテスト 3. モデルベースの統合テスト @Test void testUpdateTask()

    { // given Task task = new Task("title", "description", List.of("image1", "image2")); taskRepository.insert(task); // when … // then Task updatedTask = taskRepository.findById(task.id()).get(); Assertions.assertEquals("new title", updatedTask.title()); Assertions.assertEquals("new description", updatedTask.description()); Assertions.assertEquals(List.of("image3", "image4"), updatedTask.imageUrls()); } モデルベースの場合は テーブルに依存していないので テストがそのまま成功する
  50. © 2024 Loglass Inc. 3. モデルベースの統合テスト モデルベースの統合テストまとめ • モデルベースの統合テストはGivenとThenをモデルベースで書いた統合テスト •

    モデルベースの統合テストの効能 ◦ ドメインに則したデータを作り、ドメインに則したデータで検証する ◦ テーブルという実装の詳細への依存を除去してリファクタリングへの耐性を上げる
  51. © 2024 Loglass Inc. 1. はじめに ◦ メインテーマ 2. 統合テストの基礎知識

    ◦ 統合テストとは何か? ◦ 統合テストはなぜ必要なのか? 3. モデルベースの統合テスト ◦ モデルベースの統合テストとは何か? ◦ なぜGivenとThenをモデルベースに記述するべきなのか? 4. モデルベースのテストフィクスチャ ◦ 統合テストのデータ準備めんどくさい問題 ◦ 明示的なGiven、暗黙的なGiven 4. モデルベースのテストフィクスチャ
  52. © 2024 Loglass Inc. 4. モデルベースのテストフィクスチャ • 統合テストをするためのデータ準備が難しい ◦ 整合性を取るのが難しい→Givenをモデルベースに記述することで解決(3章)

    ◦ データ準備のためのコードが多く、保守しづらいコードに ▪ →明示的なGiven、暗黙的なGivenを区別したテストフィクスチャを整備して解決す る(4章) 統合テストのデータ準備めんどくさい問題
  53. © 2024 Loglass Inc. 4. モデルベースのテストフィクスチャ 明示的なGiven、暗黙的なGiven @Test void タスクを更新できること

    () { // given User user = new User(1, "name", "[email protected]"); userRepository.insert(user); Task task = new Task(   user.id(), "title", "description", List.of("image1", "image2") ); taskRepository.insert(task); // when … // then … }
  54. © 2024 Loglass Inc. @Test void タスクを更新できること () { //

    given User user = new User(1, "name", "[email protected]"); userRepository.insert(user); Task task = new Task(   user.id(), "title", "description", List.of("image1", "image2") ); taskRepository.insert(task); // when … // then … } 4. モデルベースのテストフィクスチャ テスト観点 テスト観点: タスクを更新する (タスクの中身やユーザーはどうでもいい。 タスクの細かいロジックは単体テストで検証する)
  55. © 2024 Loglass Inc. @Test void タスクを更新できること () { //

    given User user = new User(1, "name", "[email protected]"); userRepository.insert(user); Task task = new Task(   user.id(), "title", "description", List.of("image1", "image2") ); taskRepository.insert(task); // when … // then … } 4. モデルベースのテストフィクスチャ テスト観点に関係のない項目 暗黙的なGiven
  56. © 2024 Loglass Inc. @Test void タスクを更新できること () { //

    given User user = new User(1, "name", "[email protected]"); userRepository.insert(user); Task task = new Task(   user.id(), "title", "description", List.of("image1", "image2") ); taskRepository.insert(task); // when … // then … } 4. モデルベースのテストフィクスチャ テスト観点に関係のないデータ 暗黙的なGiven
  57. © 2024 Loglass Inc. @Test void タスクを更新できること () { //

    given User user = new User(1, "name", "[email protected]"); userRepository.insert(user); Task task = new Task(   user.id(), "title", "description", List.of("image1", "image2") ); taskRepository.insert(task); // when … // then … } 4. モデルベースのテストフィクスチャ ただタスクがあればいい 明示的なGiven
  58. © 2024 Loglass Inc. テスト観点に関係しない項目をテストから 隠す • Javaのビルダーパターンを使い 項目をデフォルト値で埋めるようにする public

    class TaskTestBuilder { private Integer userId = 1; private String title = "title"; private String description = "description"; private List<String> imageUrls = List.of("image1", "image2"); public TaskTestBuilder userId(Integer userId) { … } public TaskTestBuilder title(String title) { … } public TaskTestBuilder description(String description) {... } public TaskTestBuilder imageUrls(List<String> imageUrls) {...} public Task build() { return new Task(null, userId, title, description, imageUrls); } } 4. モデルベースのテストフィクスチャ
  59. © 2024 Loglass Inc. @Test void タスクを更新できること () { //

    given User user = new User(1, "name", "[email protected]"); userRepository.insert(user); Task task = new TaskTestBuilder().userId(user1Id).build(); taskRepository.insert(task); // when … // then … } 4. モデルベースのテストフィクスチャ テスト観点に関係しない項目をテスト から隠す • Javaのビルダーパターンを使 い項目をデフォルト値で埋める ようにする
  60. © 2024 Loglass Inc. 4. モデルベースのテストフィクスチャ Task userId title description

    imageUrls ??? ??? ??? User ??? ??? ??? テスト観点に関係しない項目をテストから隠す • Javaのビルダーパターンを使い項目をデフォルト値で埋めるようにする
  61. © 2024 Loglass Inc. 4. モデルベースのテストフィクスチャ Task userId User ???

    ??? ??? テスト観点に関係しない項目をテストから隠す • Javaのビルダーパターンを使い項目をデフォルト値で埋めるようにする 不要な項目 を隠せた!
  62. © 2024 Loglass Inc. テスト観点に関係しないデータを テストから隠す @Component public class UserTestSeedData

    { public static Integer user1Id = 1; private final UserRepository userRepository; public UserTestSeedData(UserRepository userRepository) {…} public void create() { User user = new UserTestBuilder() .id(user1Id) .build(); userRepository.insert(user); } } • テスト用のシードデータを 事前に登録し、そのIDを呼び出す ◦ シードデータは全テスト通して 最初の1回のみ走るように定義 4. モデルベースのテストフィクスチャ
  63. © 2024 Loglass Inc. @Test void タスクを更新できること () { //

    given Task task = new TaskTestBuilder() .userId(UserTestSeedData.user1Id) .build(); taskRepository.insert(task); // when … // then … } 4. モデルベースのテストフィクスチャ テスト観点に関係しないデータを テストから隠す • テスト用のシードデータを 事前に登録し、そのIDを呼び出す ◦ シードデータは全テスト通して 最初の1回のみ走るように定義
  64. © 2024 Loglass Inc. 改めて比較してみる @Test void タスクを更新できること() { //

    given Task task = new TaskTestBuilder() .userId(UserTestSeedData.user1Id) .build(); taskRepository.insert(task); // when … // then … } 4. モデルベースのテストフィクスチャ @Test void タスクを更新できること() { // given User user = new User(1, "name", "[email protected]"); userRepository.insert(user); Task task = new Task(user.id(), "title", "description", List.of("image1", "image2")); taskRepository.insert(task); // when … // then … }
  65. © 2024 Loglass Inc. テスト用シードデータの罠 • 以下の2つを守って運用すること 1. テスト用シードデータの値をテストから変更しない a.

    例)タスクの更新するテストでユーザーを更新するならユーザーにシードデータを使用 しない 2. テスト観点に関係するデータにテスト用シードデータを使用しない a. 例)ユーザーを更新するテストならユーザーにシードデータを使用しない 4. モデルベースのテストフィクスチャ
  66. © 2024 Loglass Inc. モデルベースのテストフィクスチャまとめ • 統合テストはテストデータの準備がめんどくさい ◦ 依存するデータや項目が多い •

    明示的なGivenと暗黙的なGivenを分けて、暗黙的なGivenを隠蔽できるようにする ◦ テスト観点に関係ない項目→テスト用のビルダーを作る ◦ テスト観点に関係ないデータ→テスト用のシードデータを事前投入する 4. モデルベースのテストフィクスチャ
  67. © 2024 Loglass Inc. 5. まとめ 大規模なリファクタリング への挑戦 アプリケーションの リファクタリング耐性の向上

    2. 統合テストの基礎知識 3. モデルベースの統合テスト 4. モデルベースの テストフィクスチャ
  68. © 2024 Loglass Inc. 5. まとめ 大規模なリファクタリング への挑戦 アプリケーションの リファクタリング耐性の向上

    2. 統合テストの基礎知識 3. モデルベースの統合テスト 4.モデルベースの テストフィクスチャ 統合テストは長く少ない テストケースで記述しよう
  69. © 2024 Loglass Inc. 5. まとめ 大規模なリファクタリング への挑戦 アプリケーションの リファクタリング耐性の向上

    2. 統合テストの基礎知識 3. モデルベースの統合テスト 4.モデルベースの テストフィクスチャ GivenとThenをモデルベースに記述しよう 統合テストは長く少ない テストケースで記述しよう
  70. © 2024 Loglass Inc. 5. まとめ 統合テストは長く少ない テストケースで記述しよう 明示的、暗黙的なGivenを区別して、 効率的にテストデータを準備しよう

    大規模なリファクタリング への挑戦 アプリケーションの リファクタリング耐性の向上 2. 統合テストの基礎知識 3. モデルベースの統合テスト 4.モデルベースの テストフィクスチャ GivenとThenをモデルベースに記述しよう
  71. © 2024 Loglass Inc. Q. Application層にロジックがあったらどうする?テストケースは増やすべき? • A. 以下の選択肢から選びましょう ◦

    ドメイン層にロジックを移動する ◦ 単純なデータ変換の関数に切り出して単体テスト ▪ DTO <-> ドメインモデルの変換など ◦ 本当にテストをすべきか考える ▪ throw NotFoundException(“タスクがありませんでした”)はテストすべき? • 一番おすすめしないことは統合テストのテストケースを追加すること 6. おまけ(時間が余った時用のFAQ)
  72. © 2024 Loglass Inc. Q. Repositoryのテストは書く? • A. CI全体が遅くなっているなら書かない。許容できる遅さなら書いてもいい ◦

    Application層の統合テストで呼び出されているなら書くニーズは少ないかもしれない 6. おまけ(時間が余った時用のFAQ)
  73. © 2024 Loglass Inc. Q. ログラスでControllerの統合テストを書かないのはなぜ? • A. Controllerはロジックを持つことがほぼなく、統合テストを書くメリットがあまり ないため

    ◦ さらにログラスではトランザクションをApplicaiton層で管理しているため、テスト実行後に ロールバックして冪等性を担保するやり方がやりづらい ◦ ビューモデルへの変換はそこだけ切り出して純粋な関数としてテストすることも可能 6. おまけ(時間が余った時用のFAQ)
  74. © 2024 Loglass Inc. Q. 統合テストでモック/スタブは絶対使ってはいけない? • A. 外部から観察可能なプロセス外依存でモック/スタブを使うことはOK ◦

    例えばメールサービスなどは外部から観察可能な振る舞いなので統合テスト中はモックを 使ってメールを打ったかどうか検証することがおすすめ ▪ 詳しくは「単体テストの考え方/使い方」の5.4章を確認してみてください💪 6. おまけ(時間が余った時用のFAQ)
  75. © 2024 Loglass Inc. Q. CIの統合テストが遅いです。どうすればいい? • A. テスト用のシードデータの投入が遅いなら、データはモデルベースで作りつつも、CI の度に作り直さないように事前にデータを揃えておくのがいいかも

    ◦ 例: Dockerのイメージを作っておいてテスト時はどのイメージをベースにテストする ◦ 例: テスト用のDBを別立てしてそれをつないでテストする • 一番おすすめしないことはCIで回さないでリリース時に一度だけ回すような運用にし てしまうこと ◦ リリース間際でたくさんテストが落ちまくるが影響がわかりづらいので開発生産性がガタ落ち します。 6. おまけ(時間が余った時用のFAQ)
  76. © 2024 Loglass Inc. Q. それでも遅いです。どうすればいい? • A1. 2つの振る舞いをまとめて検証するのがいいかも ◦

    例: タスクの更新と取得のユースケースをテストしたいとき、タスクの更新の検証をタスクの取 得のユースケースを使って検証し、更新と取得をまとめて検証する ◦ 特にRepositoryの統合テストを書く時はこの手法を強くおすすめします • A2. 無駄なデータがないか目を凝らしてください ◦ 並び順を検証するなら大抵のケースで3つデータがあれば十分であり、統合テストで検証した い観点にあった最低限のデータでテストするようにしてください。絶対にパフォーマンステスト 的な観点を入れないでください。 6. おまけ(時間が余った時用のFAQ)