Slide 1

Slide 1 text

© 2024 Loglass Inc. 2024.10.27 株式会社ログラス 佐藤有斗 リファクタリングへの耐性が高い モデルベースの統合テストの紹介

Slide 2

Slide 2 text

© 2024 Loglass Inc. 佐藤 有斗(ゆいと) 株式会社ログラス プロダクト開発部 エンジニア 株式会社ログラスに2020年12月に入社。 主に新規事業の立ち上げを担当。得意分野は型と自動テスト。 KotlinでたまにOSSを作ったり、他のOSSにコントリビュート したりしてます。 Yuito Sato 自己紹介

Slide 3

Slide 3 text

© 2024 Loglass Inc. 自己紹介

Slide 4

Slide 4 text

© 2024 Loglass Inc. 自己紹介 型と自動テストの力を探求する者です

Slide 5

Slide 5 text

© 2024 Loglass Inc. 1. はじめに ○ メインテーマ 2. 統合テストの基礎知識 ○ 統合テストとは何か? ○ 統合テストはなぜ必要なのか? 3. モデルベースの統合テスト ○ モデルベースの統合テストとは何か? ○ なぜGivenとThenをモデルベースに記述するべきなのか? 4. モデルベースのテストフィクスチャ ○ 統合テストのデータ準備めんどくさい問題 ○ 明示的なGiven、暗黙的なGiven アジェンダ

Slide 6

Slide 6 text

© 2024 Loglass Inc. 1. はじめに

Slide 7

Slide 7 text

© 2024 Loglass Inc. モデルベースの統合テストで、 リファクタリング耐性を上げて、 大規模なリファクタリングに挑戦できるようにする 1. はじめに

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

© 2024 Loglass Inc. 今回使用する言語・ライブラリ 1. はじめに 言語・ライブラリ バージョン Java 21 Spring Boot 3.3.4 Jooq 8.2 PostgreSQL 15 JUnit 5.9.3

Slide 10

Slide 10 text

© 2024 Loglass Inc. 2. 統合テストの基礎知識

Slide 11

Slide 11 text

© 2024 Loglass Inc. 1. はじめに ○ メインテーマ 2. 統合テストの基礎知識 ○ 統合テストとは何か? ○ 統合テストはなぜ必要なのか? 3. モデルベースの統合テスト ○ モデルベースの統合テストとは何か? ○ なぜGivenとThenをモデルベースに記述するべきなのか? 4. モデルベースのテストフィクスチャ ○ 統合テストのデータ準備めんどくさい問題 ○ 明示的なGiven、暗黙的なGiven 2. 統合テストの基礎知識

Slide 12

Slide 12 text

© 2024 Loglass Inc. 2. 統合テストの基礎知識 統合テスト(Integration Test)とは何か? ● 一つ以上のプロセス外のシステムに依存するテスト ○ プロセス外のシステム(ストレージ、メールサービス)と通信しながら実行するテスト

Slide 13

Slide 13 text

© 2024 Loglass Inc. “統合(Integration)テストとは、システムがプロセス外 依存と統合した状態でどのように機能するのかを検証す るテストのことです。” V. Khorikov 著,須田智之訳, “単体テストの考え方/使 い方.” マイナビ, 2022. 2. 統合テストの基礎知識

Slide 14

Slide 14 text

© 2024 Loglass Inc. 2. 統合テストの基礎知識 統合テストと単体テストの違い ● 統合テストは単体テストの3原則を満たせない ○ 単体テストの3原則 1. 1単位の振る舞いを検証すること 2. 実行時間が短いこと 3. 他のテストケースから隔離された状態で実行されること

Slide 15

Slide 15 text

© 2024 Loglass Inc. 2. 統合テストの基礎知識 テストピラミッド 統合テスト 単体テスト E2E 統合するものの数(エンドユーザーの観点への近さ) テストケースの数

Slide 16

Slide 16 text

© 2024 Loglass Inc. オニオンアーキテクチャで考えてみる 2. 統合テストの基礎知識 Domain Application Presentation Infrastructure DB

Slide 17

Slide 17 text

© 2024 Loglass Inc. オニオンアーキテクチャで考えてみる 2. 統合テストの基礎知識 Task, TaskRepository UpdateTaskUseCase TaskController TaskJdbcRepository PostgreSQL

Slide 18

Slide 18 text

© 2024 Loglass Inc. Repositoryについての補足 2. 統合テストの基礎知識 package com.example.sample.domain.tasks; public interface TaskRepository { void insert(Task task); void update(Task task); Optional 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 findById(Integer id) { … }

Slide 19

Slide 19 text

© 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 findById(Integer id) { … } package com.example.sample.domain.tasks; public interface TaskRepository { void insert(Task task); void update(Task task); Optional findById(Integer id); } Repositoryについての補足 2. 統合テストの基礎知識 Repository自体は Domain層に配置し、 実際のDBアクセスは Infrastructure層の 具象クラスで実装する

Slide 20

Slide 20 text

© 2024 Loglass Inc. オニオンアーキテクチャで考えてみる 2. 統合テストの基礎知識 Domain Application Infrastructure DB Presentation テストケースの数 ・ ロジックの複雑性

Slide 21

Slide 21 text

© 2024 Loglass Inc. オニオンアーキテクチャで考えてみる 2. 統合テストの基礎知識 Domain Application Infrastructure DB Presentation テストケースの数 ・ ロジックの複雑性

Slide 22

Slide 22 text

© 2024 Loglass Inc. 統合テストはケースを絞ったDBとの統合を含んだテスト 2. 統合テストの基礎知識 Domain Application Presentation Infrastructure DB 統合テスト テストケースの数 ・ ロジックの複雑性

Slide 23

Slide 23 text

© 2024 Loglass Inc. DB(プロセス外)との統合があればこれも統合テスト 2. 統合テストの基礎知識 Domain Application Infrastructure DB 統合テスト Presentation テストケースの数 ・ ロジックの複雑性

Slide 24

Slide 24 text

© 2024 Loglass Inc. ログラスでは基本はApplication 〜 DBまでの統合テストを作ることが多い 2. 統合テストの基礎知識 Domain Application Infrastructure DB 統合テスト Presentation テストケースの数 ・ ロジックの複雑性

Slide 25

Slide 25 text

© 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 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)); } }

Slide 26

Slide 26 text

© 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 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

Slide 27

Slide 27 text

© 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 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に直接インサート

Slide 28

Slide 28 text

© 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 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. 統合テストの基礎知識 処理をコール

Slide 29

Slide 29 text

© 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 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から更新後の データを取得し、 アサーション

Slide 30

Slide 30 text

© 2024 Loglass Inc. テストケースをより網羅的に検証するのが単体テスト 2. 統合テストの基礎知識 Domain Application Infrastructure DB Presentation 単体テスト テストケースの数 ・ ロジックの複雑性

Slide 31

Slide 31 text

© 2024 Loglass Inc. Infrastructure層以降をモック/スタブにしたApplication層の単体テスト 2. 統合テストの基礎知識 Domain Application Infrastructure DB Presentation 単体テスト モック/スタブ テストケースの数 ・ ロジックの複雑性

Slide 32

Slide 32 text

© 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")); }

Slide 33

Slide 33 text

© 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. 統合テストの基礎知識 更新対象のデータを 内部的に取得するスタブを定義

Slide 34

Slide 34 text

© 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. 統合テストの基礎知識 処理をコール

Slide 35

Slide 35 text

© 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処理を呼んだか検証

Slide 36

Slide 36 text

© 2024 Loglass Inc. スピードを比較すると統合テストの方が遅い 2. 統合テストの基礎知識

Slide 37

Slide 37 text

© 2024 Loglass Inc. 統合テストは遅いのでケースを絞り、単体テストで網羅的にテストをするのが基本 2. 統合テストの基礎知識 Domain Application Infrastructure DB Presentation 単体テスト 統合テスト テストケースの数 ・ ロジックの複雑性

Slide 38

Slide 38 text

© 2024 Loglass Inc. 統合テストは遅いのでケースを絞り、単体テストで網羅的にテストをするのが基本 2. 統合テストの基礎知識 Domain Application Infrastructure DB Presentation 単体テスト 統合テスト ハッピーパス(正常系)の1ケースのみを 検証するのがおすすめ テストケースの数 ・ ロジックの複雑性

Slide 39

Slide 39 text

© 2024 Loglass Inc. DB 統合テストのケースを減らすためにロジックをDomain層に集めるべし 2. 統合テストの基礎知識 Domain Application Infrastructure Presentation 単体テスト 統合テスト テストケースの数 ・ ロジックの複雑性

Slide 40

Slide 40 text

© 2024 Loglass Inc. 2. 統合テストの基礎知識 統合テストはなぜ必要なのか? ● リファクタリングへの耐性が高いため 1. 統合テストはモックやスタブの依存が少なく、故に実装の詳細への依存が少ない i. 観察可能な振る舞いのみを検証しやすい 2. 複数のモジュールを統合した上で全体的な動きを検証できる

Slide 41

Slide 41 text

© 2024 Loglass Inc. 2. 統合テストの基礎知識 リファクタリングへの耐性が高いとは何か? ● リファクタリングをした時にテストコードが壊れづらいこと(偽陽性が出づらい) ○ リファクタリングとは、「振る舞いを変えずに実装の詳細を変更すること」 ● リファクタリングへの耐性が高い = 実装の詳細への依存が少ない ○ テストコードに実装の詳細への依存がなければリファクタリングしてもテストは壊れない

Slide 42

Slide 42 text

© 2024 Loglass Inc. 2. 統合テストの基礎知識 観察可能な振る舞いとは何か? ● クライアントが目標を達成するために使う公開された状態 or 操作

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

© 2024 Loglass Inc. 2. 統合テストの基礎知識 なぜ統合テストはリファクタリングへの耐性が高いのか? 1. => モックやスタブ(実装の詳細になることが多い)を使わずにテストをしているから ○ 観察可能な振る舞いを検証しているケース UpdateTaskUseCase ID1のタスクを更新したい! (given, when) TaskRepository テストコード から見えない ID1のタスクが 更新された値になっている! (then)

Slide 47

Slide 47 text

© 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 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)

Slide 48

Slide 48 text

© 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 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)

Slide 49

Slide 49 text

© 2024 Loglass Inc. 2. 統合テストの基礎知識 なぜ統合テストはリファクタリングへの耐性が高いのか? 1. => モックやスタブ(実装の詳細になることが多い)を使わずにテストをしているから ○ 実装の詳細に依存しているケース TaskRepositoryのupdate が意図した引数で呼ばれた! ID1でTaskRepositoryの findByを呼んだらID1のタスクを 返してほしい。 その上でID1のタスクを更新したい UpdateTaskUseCase TaskRepository テストコード から実装の詳細 が見えてしまう

Slide 50

Slide 50 text

© 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のタスクを更新したい

Slide 51

Slide 51 text

© 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 が意図した引数で呼ばれた!

Slide 52

Slide 52 text

© 2024 Loglass Inc. 2. 統合テストの基礎知識 なぜApplication層の統合テストはリファクタリングへの耐性が高いのか? 2. => 複数のモジュールを統合した上で全体的な動きを検証できる ○ 実装の詳細に依存していなければ内部の実装はリファクタリング可能 Domain Application Infrastructure DB Presentation 統合テスト この範囲がリファクタ可能

Slide 53

Slide 53 text

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

Slide 54

Slide 54 text

© 2024 Loglass Inc. 3. モデルベースの統合テスト

Slide 55

Slide 55 text

© 2024 Loglass Inc. 1. はじめに ○ メインテーマ 2. 統合テストの基礎知識 ○ 統合テストとは何か? ○ 統合テストはなぜ必要なのか? 3. モデルベースの統合テスト ○ モデルベースの統合テストとは何か? ○ なぜGivenとThenをモデルベースに記述するべきなのか? 4. モデルベースのテストフィクスチャ ○ 統合テストのデータ準備めんどくさい問題 ○ 明示的なGiven、暗黙的なGiven 3. モデルベースの統合テスト

Slide 56

Slide 56 text

© 2024 Loglass Inc. 3. モデルベースの統合テスト モデルベースの統合テストとは 前提条件(Given)と検証(Then)を モデルベースに記述したテスト                    (とここでは定義します)

Slide 57

Slide 57 text

© 2024 Loglass Inc. 3. モデルベースの統合テスト モデルってなに? Domain Application Presentation Infrastructure ● Domain層に属しているシステム固有のビジネス ロジックを持ったデータ ○ 今回の例でいうとタスク ○ DB上のテーブルとは切り離された概念

Slide 58

Slide 58 text

© 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()); }

Slide 59

Slide 59 text

© 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. モデルベースの統合テスト モデルベースの統合テスト モデルとリポジトリを使って データ準備 モデルとリポジトリを使って データを検証

Slide 60

Slide 60 text

© 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 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)); }

Slide 61

Slide 61 text

© 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 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. モデルベースの統合テスト モデルベースではない 統合テスト テーブルに対して直接 書き込み、データを準備している テーブルに対して直接 参照してデータを検証している

Slide 62

Slide 62 text

© 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”

Slide 63

Slide 63 text

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

Slide 64

Slide 64 text

© 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(…); } … } … }

Slide 65

Slide 65 text

© 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. ドメインに則したデータを作り、ドメインに則したデータで検証する ありえないテストデータで テストしてしまう可能性がある

Slide 66

Slide 66 text

© 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()); } 空文字でタスクを作ろうとすると この時点でエラーになる。

Slide 67

Slide 67 text

© 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 分割

Slide 68

Slide 68 text

© 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 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)); テーブルに依存したテスト

Slide 69

Slide 69 text

© 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 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. モデルベースの統合テスト

Slide 70

Slide 70 text

© 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()); } モデルベースの場合は テーブルに依存していないので テストがそのまま成功する

Slide 71

Slide 71 text

© 2024 Loglass Inc. 3. モデルベースの統合テスト モデルベースの統合テストまとめ ● モデルベースの統合テストはGivenとThenをモデルベースで書いた統合テスト ● モデルベースの統合テストの効能 ○ ドメインに則したデータを作り、ドメインに則したデータで検証する ○ テーブルという実装の詳細への依存を除去してリファクタリングへの耐性を上げる

Slide 72

Slide 72 text

© 2024 Loglass Inc. 4. モデルベースのテストフィクスチャ

Slide 73

Slide 73 text

© 2024 Loglass Inc. 1. はじめに ○ メインテーマ 2. 統合テストの基礎知識 ○ 統合テストとは何か? ○ 統合テストはなぜ必要なのか? 3. モデルベースの統合テスト ○ モデルベースの統合テストとは何か? ○ なぜGivenとThenをモデルベースに記述するべきなのか? 4. モデルベースのテストフィクスチャ ○ 統合テストのデータ準備めんどくさい問題 ○ 明示的なGiven、暗黙的なGiven 4. モデルベースのテストフィクスチャ

Slide 74

Slide 74 text

© 2024 Loglass Inc. 4. モデルベースのテストフィクスチャ 統合テストのデータ準備めんどくさい問題 ● 統合テストをするためのデータ準備が難しい ○ 整合性を取るのが難しい ○ データ準備のためのコードが多く、保守しづらいコードに

Slide 75

Slide 75 text

© 2024 Loglass Inc. 4. モデルベースのテストフィクスチャ ● 統合テストをするためのデータ準備が難しい ○ 整合性を取るのが難しい→Givenをモデルベースに記述することで解決(3章) ○ データ準備のためのコードが多く、保守しづらいコードに 統合テストのデータ準備めんどくさい問題

Slide 76

Slide 76 text

© 2024 Loglass Inc. 4. モデルベースのテストフィクスチャ ● 統合テストをするためのデータ準備が難しい ○ 整合性を取るのが難しい→Givenをモデルベースに記述することで解決(3章) ○ データ準備のためのコードが多く、保守しづらいコードに ■ →明示的なGiven、暗黙的なGivenを区別したテストフィクスチャを整備して解決す る(4章) 統合テストのデータ準備めんどくさい問題

Slide 77

Slide 77 text

© 2024 Loglass Inc. 4. モデルベースのテストフィクスチャ 統合テストはデータ準備のためのコードが多く、保守しづらいコードになりやすい Task userId title description imageUrls ??? ??? ??? User ??? ??? ???

Slide 78

Slide 78 text

© 2024 Loglass Inc. 4. モデルベースのテストフィクスチャ 統合テストはデータ準備のためのコードが多く、保守しづらいコードになりやすい Task userId title description imageUrls ??? ??? ??? User ??? ??? ??? 設定する項目が多い

Slide 79

Slide 79 text

© 2024 Loglass Inc. 4. モデルベースのテストフィクスチャ 統合テストはデータ準備のためのコードが多く、保守しづらいコードになりやすい Task userId title description imageUrls ??? ??? ??? User ??? ??? ??? 依存するデータが多い

Slide 80

Slide 80 text

© 2024 Loglass Inc. 4. モデルベースのテストフィクスチャ 明示的なGiven、暗黙的なGiven ● テスト観点に関係するGiven→明示的なGiven ● テスト観点に関係しないGiven→暗黙的なGiven

Slide 81

Slide 81 text

© 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 … }

Slide 82

Slide 82 text

© 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. モデルベースのテストフィクスチャ テスト観点 テスト観点: タスクを更新する (タスクの中身やユーザーはどうでもいい。 タスクの細かいロジックは単体テストで検証する)

Slide 83

Slide 83 text

© 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

Slide 84

Slide 84 text

© 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

Slide 85

Slide 85 text

© 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

Slide 86

Slide 86 text

© 2024 Loglass Inc. テスト観点に関係しない項目をテストから 隠す ● Javaのビルダーパターンを使い 項目をデフォルト値で埋めるようにする public class TaskTestBuilder { private Integer userId = 1; private String title = "title"; private String description = "description"; private List imageUrls = List.of("image1", "image2"); public TaskTestBuilder userId(Integer userId) { … } public TaskTestBuilder title(String title) { … } public TaskTestBuilder description(String description) {... } public TaskTestBuilder imageUrls(List imageUrls) {...} public Task build() { return new Task(null, userId, title, description, imageUrls); } } 4. モデルベースのテストフィクスチャ

Slide 87

Slide 87 text

© 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のビルダーパターンを使 い項目をデフォルト値で埋める ようにする

Slide 88

Slide 88 text

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

Slide 89

Slide 89 text

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

Slide 90

Slide 90 text

© 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. モデルベースのテストフィクスチャ

Slide 91

Slide 91 text

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

Slide 92

Slide 92 text

© 2024 Loglass Inc. テスト観点に関係しないデータをテストから隠す 4. モデルベースのテストフィクスチャ Task userId User ??? ??? ??? ● テスト用のシードデータを事前に登録し、そのIDを呼び出す

Slide 93

Slide 93 text

© 2024 Loglass Inc. テスト観点に関係しないデータをテストから隠す 4. モデルベースのテストフィクスチャ ● テスト用のシードデータを事前に登録し、そのIDを呼び出す Task userId 不要な項目 を隠せた!

Slide 94

Slide 94 text

© 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 … }

Slide 95

Slide 95 text

© 2024 Loglass Inc. テスト観点に関係しないデータや項目が消えて 読みやすく、保守性が高まった! 4. モデルベースのテストフィクスチャ

Slide 96

Slide 96 text

© 2024 Loglass Inc. テスト用シードデータの罠 ● 以下の2つを守って運用すること 1. テスト用シードデータの値をテストから変更しない a. 例)タスクの更新するテストでユーザーを更新するならユーザーにシードデータを使用 しない 2. テスト観点に関係するデータにテスト用シードデータを使用しない a. 例)ユーザーを更新するテストならユーザーにシードデータを使用しない 4. モデルベースのテストフィクスチャ

Slide 97

Slide 97 text

© 2024 Loglass Inc. モデルベースのテストフィクスチャまとめ ● 統合テストはテストデータの準備がめんどくさい ○ 依存するデータや項目が多い ● 明示的なGivenと暗黙的なGivenを分けて、暗黙的なGivenを隠蔽できるようにする ○ テスト観点に関係ない項目→テスト用のビルダーを作る ○ テスト観点に関係ないデータ→テスト用のシードデータを事前投入する 4. モデルベースのテストフィクスチャ

Slide 98

Slide 98 text

© 2024 Loglass Inc. 5. まとめ

Slide 99

Slide 99 text

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

Slide 100

Slide 100 text

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

Slide 101

Slide 101 text

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

Slide 102

Slide 102 text

© 2024 Loglass Inc. 5. まとめ 統合テストは長く少ない テストケースで記述しよう 明示的、暗黙的なGivenを区別して、 効率的にテストデータを準備しよう 大規模なリファクタリング への挑戦 アプリケーションの リファクタリング耐性の向上 2. 統合テストの基礎知識 3. モデルベースの統合テスト 4.モデルベースの テストフィクスチャ GivenとThenをモデルベースに記述しよう

Slide 103

Slide 103 text

© 2024 Loglass Inc. 5. まとめ 統合テストを改善し、大規模なリファクタリングに挑戦しましょう!

Slide 104

Slide 104 text

© 2024 Loglass Inc. 6. おまけ(時間が余った時用のFAQ)

Slide 105

Slide 105 text

© 2024 Loglass Inc. Q. Application層にロジックがあったらどうする?テストケースは増やすべき? ● A. 以下の選択肢から選びましょう ○ ドメイン層にロジックを移動する ○ 単純なデータ変換の関数に切り出して単体テスト ■ DTO <-> ドメインモデルの変換など ○ 本当にテストをすべきか考える ■ throw NotFoundException(“タスクがありませんでした”)はテストすべき? ● 一番おすすめしないことは統合テストのテストケースを追加すること 6. おまけ(時間が余った時用のFAQ)

Slide 106

Slide 106 text

© 2024 Loglass Inc. Q. Repositoryのテストは書く? ● A. CI全体が遅くなっているなら書かない。許容できる遅さなら書いてもいい ○ Application層の統合テストで呼び出されているなら書くニーズは少ないかもしれない 6. おまけ(時間が余った時用のFAQ)

Slide 107

Slide 107 text

© 2024 Loglass Inc. Q. ログラスでControllerの統合テストを書かないのはなぜ? ● A. Controllerはロジックを持つことがほぼなく、統合テストを書くメリットがあまり ないため ○ さらにログラスではトランザクションをApplicaiton層で管理しているため、テスト実行後に ロールバックして冪等性を担保するやり方がやりづらい ○ ビューモデルへの変換はそこだけ切り出して純粋な関数としてテストすることも可能 6. おまけ(時間が余った時用のFAQ)

Slide 108

Slide 108 text

© 2024 Loglass Inc. Q. 統合テストでモック/スタブは絶対使ってはいけない? ● A. 外部から観察可能なプロセス外依存でモック/スタブを使うことはOK ○ 例えばメールサービスなどは外部から観察可能な振る舞いなので統合テスト中はモックを 使ってメールを打ったかどうか検証することがおすすめ ■ 詳しくは「単体テストの考え方/使い方」の5.4章を確認してみてください💪 6. おまけ(時間が余った時用のFAQ)

Slide 109

Slide 109 text

© 2024 Loglass Inc. Q. CIの統合テストが遅いです。どうすればいい? ● A. テスト用のシードデータの投入が遅いなら、データはモデルベースで作りつつも、CI の度に作り直さないように事前にデータを揃えておくのがいいかも ○ 例: Dockerのイメージを作っておいてテスト時はどのイメージをベースにテストする ○ 例: テスト用のDBを別立てしてそれをつないでテストする ● 一番おすすめしないことはCIで回さないでリリース時に一度だけ回すような運用にし てしまうこと ○ リリース間際でたくさんテストが落ちまくるが影響がわかりづらいので開発生産性がガタ落ち します。 6. おまけ(時間が余った時用のFAQ)

Slide 110

Slide 110 text

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

Slide 111

Slide 111 text

© 2024 Loglass Inc. Q. それでも遅いです。どうすればいい? A. CTOにお願いして、予算を増やして並列実行しよう 🙏 6. おまけ(時間が余った時用のFAQ)

Slide 112

Slide 112 text

© 2024 Loglass Inc. 今日のコード https://github.com/YuitoSato/java-spring-boot-jooq 6. おまけ(時間が余った時用のFAQ)

Slide 113

Slide 113 text

© 2024 Loglass Inc. 質問ある方はぜひ X でご連絡くださいー! https://x.com/Yuiiitoto

Slide 114

Slide 114 text

© 2024 Loglass Inc.