現在、テストが含まれていないアプリケーションは見つからないため、このトピックは初心者の開発者にとってこれまで以上に意味のあるものになります。テストなしでは成功しません。原則としてどのような種類のテストが使用されるかを検討してから、単体テストについて知っておくべきことをすべて詳しく調べていきます。
テストの種類
テストとは何ですか? Wikipedia によると、「ソフトウェア テストには、1 つ以上の関心のあるプロパティを評価するためのソフトウェア コンポーネントまたはシステム コンポーネントの実行が含まれます。」言い換えれば、これは特定の状況におけるシステムの正確性をチェックすることです。さて、一般的にどのような種類のテストがあるかを見てみましょう。- 単体テスト — システムの各モジュールを個別にチェックすることを目的としたテスト。これらのテストは、システムの最小の原子部分 (モジュールなど) に適用する必要があります。
- システム テスト — アプリケーションのより大きな部分またはシステム全体の動作をチェックするための高レベルのテスト。
- 回帰テスト — 新機能やバグ修正がアプリケーションの既存の機能に影響を与えるかどうか、または古いバグが導入されるかどうかを確認するために使用されるテスト。
- 機能テスト — アプリケーションの一部が仕様書やユーザーストーリーなどに記載されている要件を満たしているかどうかを確認します。
機能テストの種類:
- ホワイトボックス テスト — システムの内部実装を把握しながら、アプリケーションの一部が要件を満たしているかどうかをチェックします。
- ブラックボックス テスト — システムの内部実装を知ることなく、アプリケーションの一部が要件を満たしているかどうかをチェックします。
- パフォーマンス テスト — 特定の負荷の下でシステムまたはシステムの一部がどのように動作するかを判断するために作成されたテスト。
- 負荷テスト — 標準負荷下でのシステムの安定性をチェックし、アプリケーションが正常に動作する最大負荷を見つけることを目的としたテストです。
- ストレス テスト — 非標準的な負荷の下でアプリケーションのパフォーマンスをチェックし、システム障害が発生する前の最大負荷を決定することを目的としたテストです。
- セキュリティ テスト — システムのセキュリティ (ハッカー、ウイルス、機密データへの不正アクセス、その他の危険な攻撃から) をチェックするために使用されるテスト。
- ローカリゼーション テスト — アプリケーションのローカリゼーションのテスト。
- ユーザビリティ テスト — 使いやすさ、わかりやすさ、魅力、学習しやすさをチェックすることを目的としたテスト。
- ユニット — このセクションでは、アプリケーションのさまざまな層に適用される単体テストについて説明します。アプリケーション ロジックの分割可能な最小単位をテストします。たとえば、クラスですが、ほとんどの場合はメソッドです。これらのテストは通常、テスト対象を外部ロジックから可能な限り分離しようとします。つまり、アプリケーションの残りの部分が期待どおりに実行されているという錯覚を起こそうとします。
これらのテストは、小さな部分をテストし、非常に軽量であり、多くのリソース (つまり RAM と時間) を消費しないため、常に (他のタイプよりも) 多くのテストが必要です。
- 統合 — このセクションでは、統合テストについて説明します。このテストでは、システムのより大きな部分をチェックします。つまり、いくつかのロジック (いくつかのメソッドまたはクラス) を組み合わせるか、外部コンポーネントとの対話の正確さをチェックします。これらのテストは単体テストよりも重いため、通常は小さくなります。
統合テストの例としては、データベースに接続し、データベースを操作するためのメソッドの動作の正確性をチェックすることが考えられます。
- UI — このセクションは、ユーザー インターフェイスの動作をチェックするテストを指します。アプリケーションのすべてのレベルのロジックが関与するため、エンドツーエンド テストとも呼ばれます。一般に、それらは最も面倒で、最も必要な (使用される) パスをチェックする必要があるため、その数ははるかに少なくなります。
上の図では、三角形のさまざまな部分のサイズが異なることがわかります。実際の作業では、さまざまな種類のテストの数にほぼ同じ割合が存在します。
今日は、最も一般的なテストである単体テストについて詳しく見ていきます。自尊心のあるすべての Java 開発者は、単体テストを基本レベルで使用できるはずです。
単体テストの重要な概念
テスト カバレッジ (コード カバレッジ) は、アプリケーションがどの程度テストされているかを示す主な尺度の 1 つです。これは、テストの対象となるコードの割合 (0 ~ 100%) です。実際には、多くの人がこの割合を目標として追求しています。これは、テストが必要のない場所にテストが適用され始めることを意味するため、これには私は同意しません。たとえば、追加のロジックを使用せずに、サービスに標準の CRUD (作成/取得/更新/削除) 操作があるとします。これらのメソッドは、リポジトリを操作するレイヤーに作業を委任する純粋な仲介者です。この状況では、指定されたメソッドが DAO メソッドを呼び出すかどうかを除いて、テストするものは何もありませんが、それは冗談です。通常、テスト カバレッジを評価するには、JaCoCo、Cobertura、Clover、Emma などの追加ツールが使用されます。このトピックの詳細な調査については、次のツールを使用します。 TDD はテスト駆動開発の略です。このアプローチでは、他の作業を行う前に、特定のコードをチェックするテストを作成します。これはブラックボックス テストであることがわかります。入力が何であるか、出力が何であるべきかがわかっています。これにより、コードの重複を避けることができます。テスト駆動開発は、アプリケーションの各機能のテストを設計および開発することから始まります。TDD アプローチでは、最初にコードの動作を定義してテストするテストを作成します。TDD の主な目標は、コードをより理解しやすく、よりシンプルにし、エラーをなくすことです。このアプローチは次の内容で構成されます。- テストを書きます。
- テストを実行します。当然のことながら、必要なロジックがまだ実装されていないため、失敗します。
- テストを成功させるコードを追加します (テストを再度実行します)。
- コードをリファクタリングします。
テストの段階
テストは 3 つの段階で構成されます。- テストデータ(フィクスチャ)を指定します。
- テスト対象のコードを実行します (テストされたメソッドを呼び出します)。
- 結果を確認し、予想される結果と比較します。
テスト環境
さて、本題です。Java で使用できるテスト環境 (フレームワーク) がいくつかあります。これらの中で最も人気のあるのは JUnit と TestNG です。ここでのレビューでは、以下を使用します。JUnit テストは、テストのみに使用されるクラス内のメソッドです。通常、クラスにはテストするクラスと同じ名前が付けられ、最後に「Test」が追加されます。たとえば、CarService -> CarServiceTest です。Maven ビルド システムは、そのようなクラスをテスト スコープに自動的に含めます。実際、このクラスはテスト クラスと呼ばれます。基本的なアノテーションを簡単に見てみましょう。- @Test は、メソッドがテストであることを示します (基本的に、このアノテーションが付いているメソッドは単体テストです)。
- @Before は、各テストの前に実行されるメソッドを示します。たとえば、クラスにテスト データを設定したり、入力データを読み取ったりする場合などです。
- @After は、各テストの後に呼び出されるメソッドをマークするために使用されます (データを消去したり、デフォルト値を復元したりするためなど)。
- @BeforeClass は、@Before と同様にメソッドの上に配置されます。ただし、このようなメソッドは、指定されたクラスのすべてのテストの前に 1 回だけ呼び出されるため、静的である必要があります。これは、テスト データベースのスピンアップなど、よりリソースを大量に消費する操作を実行するために使用されます。
- @AfterClass は @BeforeClass の逆です。指定されたクラスに対して 1 回実行されますが、すべてのテストの後でのみ実行されます。これは、永続的なリソースをクリアしたり、データベースから切断したりするために使用されます。
- @Ignore は、メソッドが無効であり、テスト実行全体で無視されることを示します。これは、基本メソッドが変更され、その変更に対応するためにテストがまだ作り直されていない場合など、さまざまな状況で使用されます。このような場合には、@Ignore("Some description") のように説明を追加することも望ましいです。
- @Test(expected = Exception.class) は否定的なテストに使用されます。これらは、エラーが発生した場合にメソッドがどのように動作するかを検証するテストです。つまり、テストではメソッドが何らかの例外をスローすることが予想されます。このようなメソッドは @Test アノテーションによって示されますが、キャッチするエラーも示されます。
- @Test(timeout = 100) は、メソッドが 100 ミリ秒以内に実行されることをチェックします。
- @Mock は、モック オブジェクトを割り当てるためにフィールドの上で使用されます (これは JUnit アノテーションではなく、代わりに Mockito から来ています)。必要に応じて、特定の状況に対するモックの動作をテスト メソッドで直接設定します。
- @RunWith(MockitoJUnitRunner.class) はクラスの上に配置されます。このアノテーションは、クラス内のテストを呼び出すように JUnit に指示します。MockitoJUnitRunner、JUnitPlatform、SpringRunner など、さまざまなランナーがあります。JUnit 5 では、@RunWith アノテーションが、より強力な @ExtendWith アノテーションに置き換えられました。
- assertEquals(Object Expects, Objectactuals) — 渡されたオブジェクトが等しいかどうかをチェックします。
- assertTrue(boolean flag) — 渡された値が true かどうかをチェックします。
- assertFalse(boolean flag) — 渡された値が false かどうかをチェックします。
- assertNll(Object object) — 渡されたオブジェクトが null かどうかをチェックします。
- assertSame(Object firstObject, Object SecondObject) — 渡された値が同じオブジェクトを参照しているかどうかを確認します。
- assertThat(T t, マッチャー
マッチャー) — t が matcher で指定された条件を満たすかどうかをチェックします。
実際のテスト
次に、上記の資料を具体的な例で見てみましょう。サービスの更新メソッドをテストします。デフォルトを使用しているため、DAO レイヤーは考慮しません。テスト用のスターターを追加しましょう。<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>2.2.2.RELEASE</version>
<scope>test</scope>
</dependency>
そしてここにサービスクラスがあります:
@Service
@RequiredArgsConstructor
public class RobotServiceImpl implements RobotService {
private final RobotDAO robotDAO;
@Override
public Robot update(Long id, Robot robot) {
Robot found = robotDAO.findById(id);
return robotDAO.update(Robot.builder()
.id(id)
.name(robot.getName() != null ? robot.getName() : found.getName())
.cpu(robot.getCpu() != null ? robot.getCpu() : found.getCpu())
.producer(robot.getProducer() != null ? robot.getProducer() : found.getProducer())
.build());
}
}
8 行目 — 更新されたオブジェクトをデータベースから取得します。行 9 ~ 14 — ビルダーを通じてオブジェクトを作成します。受信オブジェクトにフィールドがある場合は、それを設定します。そうでない場合は、データベース内の内容をそのまま残します。次にテストを見てみましょう。
@RunWith(MockitoJUnitRunner.class)
public class RobotServiceImplTest {
@Mock
private RobotDAO robotDAO;
private RobotServiceImpl robotService;
private static Robot testRobot;
@BeforeClass
public static void prepareTestData() {
testRobot = Robot
.builder()
.id(123L)
.name("testRobotMolly")
.cpu("Intel Core i7-9700K")
.producer("China")
.build();
}
@Before
public void init() {
robotService = new RobotServiceImpl(robotDAO);
}
1 行目 — 私たちのランナー。4 行目 — モックを置き換えることによってサービスを DAO 層から分離します。11 行目 — クラスのテスト エンティティ (モルモットとして使用するエンティティ) を設定します。22 行目 — サービス オブジェクトを設定します。これをテストします。
@Test
public void updateTest() {
when(robotDAO.findById(any(Long.class))).thenReturn(testRobot);
when(robotDAO.update(any(Robot.class))).then(returnsFirstArg());
Robot robotForUpdate = Robot
.builder()
.name("Vally")
.cpu("AMD Ryzen 7 2700X")
.build();
Robot resultRobot = robotService.update(123L, robotForUpdate);
assertNotNull(resultRobot);
assertSame(resultRobot.getId(),testRobot.getId());
assertThat(resultRobot.getName()).isEqualTo(robotForUpdate.getName());
assertTrue(resultRobot.getCpu().equals(robotForUpdate.getCpu()));
assertEquals(resultRobot.getProducer(),testRobot.getProducer());
}
ここでは、テストに 3 つの明確な区分があることがわかります: 行 3 ~ 9 — フィクスチャの指定。行 11 — テスト対象のコードを実行します。13 ~ 17 行目 — 結果の確認。詳細: 行 3 ~ 4 — DAO モックの動作を設定します。5 行目 — 標準に基づいて更新するインスタンスを設定します。11 行目 — メソッドを使用し、結果のインスタンスを取得します。13 行目 — null でないことを確認します。行 14 — 結果の ID と指定されたメソッド引数を比較します。15 行目 — 名前が更新されたかどうかを確認します。16 行目 — CPU の結果を参照します。17 行目 — このフィールドはインスタンスで指定していないため、同じままにする必要があります。ここでその状態を確認します。実行してみましょう:テストはグリーンです!安堵のため息がつきます :) 要約すると、テストによってコードの品質が向上し、開発プロセスがより柔軟で信頼性の高いものになります。何百ものクラスファイルを含むソフトウェアを再設計するのにどれだけの労力がかかるか想像してみてください。これらすべてのクラスに対して単体テストを作成すると、自信を持ってリファクタリングできます。そして最も重要なことは、開発中にバグを簡単に発見できるようにすることです。みなさん、今日はこれで終わりです。「いいね!」をして、コメントを残してください:)