JavaRush /Java Blog /Random-JA /Java 単体テスト: テクニック、概念、実践
Константин
レベル 36

Java 単体テスト: テクニック、概念、実践

Random-JA グループに公開済み
今日、テストでカバーされていないアプリケーションはほとんど見つからないため、このトピックは初心者の開発者にとってこれまで以上に重要になります。テストがなければどこにも到達できません。宣伝になりますので、私の過去記事を見ていただければと思います。そのうちのいくつかはテストをカバーしています (それでも、記事は非常に役立ちます)。
  1. MySql の代わりに MariaDB を使用したデータベースの統合テスト
  2. 多言語アプリケーションの実装
  3. ファイルをアプリケーションに保存し、それらに関するデータをデータベースに保存する
原則としてどのようなタイプのテストが使用されるかを検討してから、単体テストについて知っておくべきことをすべて詳しく調べていきます。

テストの種類

テストとは何ですか? Wiki には次のように書かれています。「テストとはシステムをさまざまな状況に置き、その中で観察可能な変化を追跡することによって、システムの基礎となるプロセスを研究する方法です。」言い換えれば、これは特定の状況でシステムが正しく動作するかどうかのテストです。単体テストのすべて: 方法、概念、実践 - 2それでは、どのような種類のテストがあるのか​​見てみましょう。
  1. 単体テストは、システムの各モジュールを個別にテストすることをタスクとするテストです。これらは、システムの最小分割可能な部分、たとえばモジュールであることが望ましい。

  2. システム テストは、アプリケーションのより大きな部分の動作またはシステム全体をテストするための高レベルのテストです。

  3. 回帰テストは、新機能やバグ修正がアプリケーションの既存の機能に影響を与えるかどうか、また古いバグが再発するかどうかを確認するために使用されるテストです。

  4. 機能テストでは、アプリケーションの一部が仕様やユーザーストーリーなどに記載されている要件に準拠しているかどうかを確認します。

    機能テストの種類:

    • システムの内部実装に関する知識を使用して、アプリケーションの一部が要件に準拠しているかどうかを確認する「ホワイト ボックス」テスト。
    • システムの内部実装に関する知識がなくても、アプリケーションの一部が要件に準拠しているかどうかを確認する「ブラック ボックス」テスト。
  5. パフォーマンス テストは、特定の負荷下でシステムまたはその一部が実行される速度を決定するために作成されるテストの一種です。
  6. 負荷テスト- 標準負荷下でのシステムの安定性をチェックし、アプリケーションが正しく動作する最大のピークを見つけることを目的としたテストです。
  7. ストレス テストは、標準外の負荷の下でアプリケーションの機能をチェックし、システムがクラッシュしない最大のピークを決定するために設計されたテストの一種です。
  8. セキュリティテスト- システムのセキュリティ (ハッカーによる攻撃、ウイルス、機密データへの不正アクセス、その他の日常生活の楽しみから) をチェックするために使用されるテスト。
  9. ローカリゼーション テストは、アプリケーションのローカリゼーション テストです。
  10. ユーザビリティテストは、ユーザーの使いやすさ、わかりやすさ、魅力、学習しやすさをチェックすることを目的としたテストの一種です。
  11. これはすべて良いことのように聞こえますが、実際にはどのように機能するのでしょうか? それは簡単です: Mike Cohn のテスト ピラミッドが使用されます: 単体テストのすべて: 方法、概念、実践 - 4これはピラミッドの簡略化されたバージョンです: 現在は、より小さな部分に分割されています。しかし、今日は倒錯せず、最も単純なオプションを検討します。
    1. 単体- アプリケーションのさまざまな層で使用される単体テスト。アプリケーションの分割可能な最小のロジック (たとえば、クラスですが、ほとんどの場合はメソッド) をテストします。これらのテストは通常​​、外部ロジックから可能な限り分離しようとします。つまり、アプリケーションの残りの部分が標準モードで動作しているかのように見せかけます。

      これらのテストは、小さな部分をテストし、非常に軽量で、多くのリソース (リソースとは RAM と時間のことです) を消費しないため、常に (他のタイプよりも) 多くのテストが必要です。

    2. 統合- 統合テスト。これは、システムのより大きな部分、つまり、いくつかのロジック部分 (いくつかのメソッドまたはクラス) の組み合わせ、または外部コンポーネントの操作の正確さをチェックします。これらのテストは重いため、通常、単体テストよりも数が少なくなります。

      統合テストの例として、データベースに接続し、データベースで動作するメソッドが正しく動作するかどうかを確認することを検討できます。

    3. UI - ユーザー インターフェイスの動作をチェックするテスト。これらはアプリケーションのすべてのレベルのロジックに影響を与えるため、エンドツーエンドとも呼ばれます。これらは最も重量があり、最も必要な (使用される) パスをチェックする必要があるため、一般に、それらの数ははるかに少なくなります。

      上の図では、三角形のさまざまな部分の面積の比率がわかります。実際の作業では、これらのテストの数でほぼ同じ比率が維持されます。

      今日は、最もよく使用されるテストである単体テストを詳しく見ていきます。自尊心のあるすべての Java 開発者は、単体テストを基本レベルで使用できるはずだからです。

    単体テストの重要な概念

    テスト カバレッジ(コード カバレッジ) は、アプリケーション テストの品質の主な評価の 1 つです。これは、テストでカバーされたコードの割合 (0 ~ 100%) です。実際には、多くの人がこの割合を追い求めていますが、必要のないところにテストを追加し始めるため、私はこれに同意しません。たとえば、私たちのサービスには、追加のロジックを必要としない標準の CRUD (作成/取得/更新/削除) 操作があります。これらのメソッドは、リポジトリと連携するレイヤーに作業を委任する純粋な仲介者です。この状況では、テストするものは何もありません。おそらく、このメソッドがタオのメソッドを呼び出すかどうかですが、これは深刻ではありません。テスト カバレッジを評価するには、通常、JaCoCo、Cobertura、Clover、Emma などの追加ツールが使用されます。この問題の詳細な調査については、いくつかの適切な記事を参照してください。 TDD (テスト駆動開発) - テスト駆動開発。このアプローチでは、まず、特定のコードをチェックするテストを作成します。それはブラック ボックス テストであることがわかります。私たちは入力に何があるかを知っており、出力で何が起こるべきかを知っています。これにより、コードの重複が回避されます。テスト駆動開発は、アプリケーションの小さな機能ごとにテストを設計および開発することから始まります。TDD アプローチでは、まず、コードが何を行うかを定義して検証するテストが開発されます。TDD の主な目標は、コードをより明確、単純、そしてエラーのないものにすることです。 単体テストのすべて: 方法、概念、実践 - 6このアプローチは次のコンポーネントで構成されます。
    1. 私たちはテストを書いています。
    2. 合格したかどうかに関係なく、テストを実行します (すべてが赤になっていることがわかります。心配しないでください。これは本来あるべき姿です)。
    3. このテストを満たす必要があるコードを追加します (テストを実行します)。
    4. コードをリファクタリングします。
    単体テストはテスト自動化ピラミッドの最小要素であるという事実に基づいて、TDD は単体テストに基づいています。単体テストの助けを借りて、あらゆるクラスのビジネス ロジックをテストできます。 BDD (行動駆動型開発) - 行動による開発。このアプローチは TDD に基づいています。より具体的には、開発に関わるすべての人向けにシステムの動作を説明する、明確な言語 (通常は英語) で書かれた例を使用します。この用語は主にテスターとビジネス アナリストに影響を与えるため、これ以上深くは説明しません。 テスト ケース- テスト対象のコードの実装を検証するために必要な手順、特定の条件、パラメーターを説明するスクリプト。 フィクスチャは、テスト対象のメソッドを正常に実行するために必要なテスト環境の状態です。これは、使用される条件下でのオブジェクトとその動作の事前に設定されたセットです。

    テスト段階

    テストは 3 つの段階で構成されます。
    1. テストするデータ (フィクスチャ) を指定します。
    2. テスト対象のコードを使用する (テスト対象のメソッドを呼び出す)。
    3. 結果を確認し、予想される結果と比較します。
    単体テストのすべて: 方法、概念、実践 - 7テストのモジュール性を確保するには、アプリケーションの他の層から分離する必要があります。これは、スタブ、モック、スパイを使用して実行できます。 モックはカスタマイズ可能なオブジェクト (たとえば、各テストに固有) で、受信する予定の応答の形式でメソッド呼び出しの期待値を設定できます。期待値チェックは、Mock オブジェクトの呼び出しを通じて実行されます。 スタブ- テスト中に呼び出しに対する固定的な応答を提供します。また、呼び出しに関する情報 (パラメーターや呼び出しの数など) を保存することもできます。これらは、独自の用語「スパイ ( Spy )」で呼ばれることもあります。時々、スタブモックという用語が混同されます。違いは、スタブは何もチェックせず、特定の状態をシミュレートするだけであるということです。モックは期待を持つオブジェクトです。たとえば、特定のクラス メソッドを特定の回数呼び出す必要があるとします。言い換えれば、テストがスタブによって中断されることはありませんが、モックによって中断される可能性があります。

    テスト環境

    それでは、本題に入りましょう。Java で使用できるテスト環境 (フレームワーク) がいくつかあります。その中で最も人気のあるのは JUnit と TestNG です。レビューでは、以下を使用します。 単体テストのすべて: 方法、概念、実践 - 8JUnit テストは、テストのみに使用されるクラスに含まれるメソッドです。通常、クラスにはテスト対象のクラスと同じ名前が付けられ、最後に +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) - メソッドはクラスの上に配置されます。これはテストを実行するためのボタンです。ランナーは異なる場合があります。たとえば、MockitoJUnitRunner、JUnitPlatform、SpringRunner などがあります)。JUnit 5 では、@RunWith アノテーションが、より強力な @ExtendWith アノテーションに置き換えられました。結果を比較するためのいくつかの方法を見てみましょう。
    • assertEquals(Object expecteds, Object actuals)— 送信されたオブジェクトが等しいかどうかをチェックします。
    • assertTrue(boolean flag)— 渡された値が true を返すかどうかをチェックします。
    • assertFalse(boolean flag)— 渡された値が false を返すかどうかをチェックします。
    • assertNull(Object object)– オブジェクトが null かどうかを確認します。
    • assertSame(Object firstObject, Object secondObject)— 渡された値が同じオブジェクトを参照しているかどうかを確認します。
    • assertThat(T t, Matcher<T> matcher)— t がマッチャーで指定された条件を満たすかどうかをチェックします。
    また、assertj の便利な比較フォームもあります。assertThat(firstObject).isEqualTo(secondObject) 残りは上記のさまざまなバリエーションであるため、ここでは基本的なメソッドについて説明しました。

    テストの練習

    それでは、具体的な例を使用して上記の資料を見てみましょう。サービスの更新方法をテストします。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 - moka dao の動作の設定 5 - インスタンスの設定標準に基づいて更新することを確認します 11 - メソッドを使用し、結果のインスタンスを取得します 13 - ゼロでないことを確認します 14 - 結果 ID と指定されたメソッド引数を確認します 15 - 名前が更新されたかどうかを確認します 16 - 見てくださいCPU 17 による結果 - これを更新インスタンス フィールドに設定していないため、同じままになるはずです。確認してみましょう。 単体テストのすべて: 方法、概念、実践 - 9開始しましょう: 単体テストのすべて: テクニック、概念、実践 - 10テストは緑色です。息を吐き出すことができます)) それでは、要約しましょう:テストはコードの品質を向上させ、開発プロセスをより柔軟で信頼性の高いものにします。何百ものクラスファイルを含むソフトウェアを再設計するときにどれだけの労力を費やさなければならないかを想像してみてください。これらすべてのクラスに対して単体テストを作成したら、自信を持ってリファクタリングできます。そして最も重要なことは、開発中にエラーを簡単に発見できるようにすることです。皆さん、今日はこれで終わりです。「いいね!」をたくさん送って、コメントを書いてください))) 単体テストのすべて: 方法、概念、実践 - 11
コメント
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION