1. 概要

このチュートリアルでは、テストピラミッドと呼ばれる一般的なソフトウェアテストモデルについて理解します。

マイクロサービスの世界でそれがどのように関連しているかを見ていきます。 その過程で、このモデルに準拠するためのサンプルアプリケーションと関連するテストを開発します。 さらに、モデルを使用することの利点と境界を理解しようとします。

2. 一歩後退しましょう

テストピラミッドのような特定のモデルを理解し始める前に、なぜそれが必要なのかを理解することが不可欠です。

ソフトウェアをテストする必要性は本質的であり、おそらくソフトウェア開発自体の歴史と同じくらい古いものです。 ソフトウェアテストは、手動から自動化、さらにはさらに長い道のりを歩んできました。 ただし、目的は同じです—仕様に準拠したソフトウェアを提供する

2.1. テストの種類

実際には、特定の目的に焦点を当てたいくつかの異なるタイプのテストがあります。 悲しいことに、これらのテストの語彙や理解にはかなりのばらつきがあります。

人気があり、おそらく明確なもののいくつかを確認しましょう:

  • 単体テスト:単体テストは、コードの小さな単位を対象とするテストであり、できれば単独で。 ここでの目的は、コードベースの残りの部分を気にすることなく、テスト可能な最小のコードの動作を検証することです。 これは、依存関係をモックまたはスタブまたはそのような同様の構造に置き換える必要があることを自動的に意味します。
  • 統合テスト:単体テストはコードの内部に焦点を当てていますが、多くの複雑さがコードの外部にあるという事実は残っています。 コードの単位は、データベース、メッセージブローカー、Webサービスなどの外部サービスと連携して機能する必要があります。 統合テストは、外部依存関係と統合しながら、アプリケーションの動作を対象とするテストです。
  • UIテスト:私たちが開発するソフトウェアは、多くの場合、消費者が対話できるインターフェースを介して消費されます。 多くの場合、アプリケーションにはWebインターフェイスがあります。 ただし、APIインターフェイスはますます人気が高まっています。 UIテストは、これらのインターフェースの動作を対象としています。これらのインターフェースは、本質的に高度にインタラクティブであることがよくあります。 現在、これらのテストはエンドツーエンドの方法で実行できます。または、ユーザーインターフェイスを個別にテストすることもできます。

2.2. 手動対。 自動テスト

ソフトウェアテストは、テストの開始以来手動で行われており、今日でも広く実践されています。 ただし、手動テストには制限があることを理解するのは難しくありません。 テストを有効にするには、包括的で頻繁に実行する必要があります。

これは、アジャイル開発の方法論とクラウドネイティブのマイクロサービスアーキテクチャではさらに重要です。 ただし、テスト自動化の必要性ははるかに早く認識されました。

前に説明したさまざまなタイプのテストを思い出すと、単体テストから統合テストとUIテストに移行するにつれて、それらの複雑さと範囲が増大します。 同じ理由で、単体テストの自動化はより簡単で、ほとんどの利点も備えています。 さらに進むと、テストの自動化がますます困難になり、メリットはほぼ間違いなく少なくなります。

特定の側面を除けば、今日の時点でほとんどのソフトウェア動作のテストを自動化することが可能です。 ただし、これは、自動化に必要な労力と比較して、メリットと合理的に比較検討する必要があります。

3. テストピラミッドとは何ですか?

テストの種類とツールに関する十分なコンテキストを収集したので、次はテストピラミッドが正確に何であるかを理解します。 作成する必要のあるテストにはさまざまな種類があることがわかりました。

ただし、タイプごとにいくつのテストを作成するかをどのように決定する必要がありますか? 注意すべき利点または落とし穴は何ですか? これらは、テストピラミッドのようなテスト自動化モデルによって対処される問題の一部です。

Mike Cohn は、彼の著書「 Successing withAgile」でTestPyramidという構成を考案しました。 このは、さまざまなレベルの粒度で作成する必要のあるテストの数を視覚的に表したものです。

テストの範囲を広げるにつれて、それは最も詳細なレベルで最も高くなり、減少し始めるはずであるという考えです。 これはピラミッドの典型的な形を与えるので、名前は次のとおりです。

コンセプトは非常にシンプルでエレガントですが、これを効果的に採用することはしばしば困難です。 モデルの形状とモデルが言及するテストのタイプに固執してはならないことを理解することが重要です。 重要なポイントは次のとおりです。

  • さまざまなレベルの粒度でテストを作成する必要があります
  • スコープが粗くなるにつれて、作成するテストの数を減らす必要があります

4. テスト自動化ツール

さまざまなタイプのテストを作成するために、すべての主流のプログラミング言語で利用できるツールがいくつかあります。 Javaの世界で人気のある選択肢のいくつかを取り上げます。

4.1. ユニットテスト

  • テストフレームワーク:ここでJavaで最も人気のある選択肢は、 JUnit です。これには、JUnit5として知られる次世代リリースがあります。 この分野で人気のある他の選択肢には、 TestNG があります。これは、JUnit5と比較していくつかの差別化された機能を提供します。 ただし、ほとんどのアプリケーションでは、これらの両方が適切な選択です。
  • モッキング:前に見たように、単体テストの実行中に、すべてではないにしても、ほとんどの依存関係を確実に差し引きたいと思います。 このためには、依存関係をモックやスタブのようなテストダブルに置き換えるメカニズムが必要です。 Mockito は、Javaの実際のオブジェクトにモックをプロビジョニングするための優れたフレームワークです。

4.2. 統合テスト

  • テストフレームワーク:統合テストの範囲は単体テストよりも広いですが、エントリポイントは、より高度な抽象化で同じコードであることがよくあります。 このため、単体テストで機能するのと同じテストフレームワークが統合テストにも適しています。
  • モック:統合テストの目的は、実際の統合を使用してアプリケーションの動作をテストすることです。 ただし、テストのために実際のデータベースやメッセージブローカーにアクセスしたくない場合があります。 多くのデータベースおよび同様のサービスは、統合テストを作成するための埋め込み可能バージョンを提供しています。

4.3. UIテスト

  • テストフレームワーク:UIテストの複雑さは、ソフトウェアのUI要素を処理するクライアントによって異なります。 たとえば、Webページの動作は、デバイス、ブラウザ、さらにはオペレーティングシステムによっても異なる場合があります。 Selenium は、Webアプリケーションでブラウザーの動作をエミュレートするための一般的な選択肢です。 ただし、REST APIの場合は、REST-assuredなどのフレームワークの方が適しています。
  • モッキング:ユーザーインターフェイスは、AngularReactなどのJavaScriptフレームワークを使用して、よりインタラクティブになり、クライアント側でレンダリングされるようになっています。 JasmineMochaなどのテストフレームワークを使用して、このようなUI要素を個別にテストする方が合理的です。 明らかに、これはエンドツーエンドのテストと組み合わせて行う必要があります。

5. 実際の原則の採用

これまでに説明した原則を示すために、小さなアプリケーションを開発しましょう。 小さなマイクロサービスを開発し、テストピラミッドに準拠したテストを作成する方法を理解します。

マイクロサービスアーキテクチャは、ドメイン境界の周りに描かれた緩く結合されたサービスのコレクションとしてアプリケーションを構造化するのに役立ちます。 Spring Boot は、ユーザーインターフェイスとデータベースのような依存関係を備えたマイクロサービスをほぼ短時間でブートストラップするための優れたプラットフォームを提供します。

これらを活用して、テストピラミッドの実際のアプリケーションを示します。

5.1. アプリケーションアーキテクチャ

視聴した映画の保存とクエリを可能にする基本的なアプリケーションを開発します。

ご覧のとおり、3つのエンドポイントを公開する単純なRESTコントローラーがあります。

@RestController
public class MovieController {
 
    @Autowired
    private MovieService movieService;
 
    @GetMapping("/movies")
    public List<Movie> retrieveAllMovies() {
        return movieService.retrieveAllMovies();
    }
 
    @GetMapping("/movies/{id}")
    public Movie retrieveMovies(@PathVariable Long id) {
        return movieService.retrieveMovies(id);
    }
 
    @PostMapping("/movies")
    public Long createMovie(@RequestBody Movie movie) {
        return movieService.createMovie(movie);
    }
}

コントローラーは、データのマーシャリングとアンマーシャリングの処理を除いて、適切なサービスにルーティングするだけです。

@Service
public class MovieService {
 
    @Autowired
    private MovieRepository movieRepository;

    public List<Movie> retrieveAllMovies() {
        return movieRepository.findAll();
    }
 
    public Movie retrieveMovies(@PathVariable Long id) {
        Movie movie = movieRepository.findById(id)
          .get();
        Movie response = new Movie();
        response.setTitle(movie.getTitle()
          .toLowerCase());
        return response;
    }
 
    public Long createMovie(@RequestBody Movie movie) {
        return movieRepository.save(movie)
          .getId();
    }
}

さらに、永続層にマップするJPAリポジトリがあります。

@Repository
public interface MovieRepository extends JpaRepository<Movie, Long> {
}

最後に、映画データを保持して渡すための単純なドメインエンティティ:

@Entity
public class Movie {
    @Id
    private Long id;
    private String title;
    private String year;
    private String rating;

    // Standard setters and getters
}

この単純なアプリケーションを使用して、さまざまな粒度と量のテストを検討する準備が整いました。

5.2. ユニットテスト

まず、アプリケーションの簡単な単体テストを作成する方法を理解します。 このアプリケーションから明らかなように、ほとんどのロジックはサービスレイヤーに蓄積される傾向があります。 これは、これを広範囲に、より頻繁にテストすることを義務付けています—単体テストに非常に適しています。

public class MovieServiceUnitTests {
 
    @InjectMocks
    private MovieService movieService;
 
    @Mock
    private MovieRepository movieRepository;
 
    @Before
    public void setUp() throws Exception {
        MockitoAnnotations.initMocks(this);
    }
 
    @Test
    public void givenMovieServiceWhenQueriedWithAnIdThenGetExpectedMovie() {
        Movie movie = new Movie(100L, "Hello World!");
        Mockito.when(movieRepository.findById(100L))
          .thenReturn(Optional.ofNullable(movie));
 
        Movie result = movieService.retrieveMovies(100L);
 
        Assert.assertEquals(movie.getTitle().toLowerCase(), result.getTitle());
    }
}

ここでは、テストフレームワークとしてJUnitを使用し、依存関係をモックするためにMockitoを使用しています。 私たちのサービスは、いくつかの奇妙な要件のために、小文字で映画のタイトルを返すことが期待されていました、そしてそれは私たちがここでテストするつもりです。 そのような単体テストで広範囲にカバーする必要があるそのような動作がいくつかある可能性があります。

5.3. 統合テスト

単体テストでは、永続性レイヤーへの依存関係であるリポジトリをモックしました。 サービスレイヤーの動作を徹底的にテストしましたが、データベースに接続するときに問題が発生する可能性があります。 ここで統合テストが登場します。

@RunWith(SpringRunner.class)
@SpringBootTest
public class MovieControllerIntegrationTests {
 
    @Autowired
    private MovieController movieController;
 
    @Test
    public void givenMovieControllerWhenQueriedWithAnIdThenGetExpectedMovie() {
        Movie movie = new Movie(100L, "Hello World!");
        movieController.createMovie(movie);
 
        Movie result = movieController.retrieveMovies(100L);
 
        Assert.assertEquals(movie.getTitle().toLowerCase(), result.getTitle());
    }
}

ここでいくつかの興味深い違いに注意してください。 現在、依存関係をモックしていません。 ただし、状況によっては、いくつかの依存関係をモックする必要がある場合があります。 さらに、これらのテストはSpringRunnerで実行しています。

これは基本的に、このテストを実行するためのSpringアプリケーションコンテキストとライブデータベースがあることを意味します。 当然のことながら、これは遅くなります! したがって、ここでテストするシナリオの数を大幅に減らします。

5.4. UIテスト

最後に、アプリケーションには消費するRESTエンドポイントがあり、テストする独自のニュアンスがある場合があります。 これはアプリケーションのユーザーインターフェイスであるため、UIテストでカバーすることに焦点を当てます。 ここで、REST-assuredを使用してアプリケーションをテストしましょう。

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class MovieApplicationE2eTests {
 
    @Autowired
    private MovieController movieController;
 
    @LocalServerPort
    private int port;
 
    @Test
    public void givenMovieApplicationWhenQueriedWithAnIdThenGetExpectedMovie() {
        Movie movie = new Movie(100L, "Hello World!");
        movieController.createMovie(movie);
 
        when().get(String.format("http://localhost:%s/movies/100", port))
          .then()
          .statusCode(is(200))
          .body(containsString("Hello World!".toLowerCase()));
    }
}

ご覧のとおり、これらのテストは実行中のアプリケーションで実行され、使用可能なエンドポイントを介してアクセスします。 応答コードなど、HTTPに関連する一般的なシナリオのテストに重点を置いています。 これらは、明らかな理由で実行するのに最も遅いテストになります。

したがって、ここでテストするシナリオを選択することに非常に注意を払う必要があります。 以前のより詳細なテストではカバーできなかった複雑さにのみ焦点を当てる必要があります。

6. マイクロサービスのピラミッドをテストする

これで、さまざまな粒度でテストを記述し、それらを適切に構造化する方法を確認しました。 ただし、主な目的は、より詳細で高速なテストを使用して、アプリケーションの複雑さのほとんどを把握することです。

モノリシックアプリケーションでこれに対処すると、目的のピラミッド構造が得られますが、他のアーキテクチャでは必要ない場合があります

ご存知のように、マイクロサービスアーキテクチャはアプリケーションを受け取り、緩く結合されたアプリケーションのセットを提供します。 そうすることで、アプリケーションに固有の複雑さのいくつかを外部化します。

現在、これらの複雑さはサービス間の通信に現れています。 単体テストでそれらをキャプチャできるとは限らないため、統合テストをさらに作成する必要があります。

これは、私たちが古典的なピラミッドモデルから逸脱していることを意味するかもしれませんが、それは私たちが原則から逸脱していることも意味しません。 可能な限り詳細なテストを使用して、複雑さのほとんどをキャプチャしていることを忘れないでください。 それが明確である限り、完全なピラミッドと一致しない可能性のあるモデルは依然として価値があります。

ここで理解しておくべき重要なことは、モデルは価値を提供する場合にのみ役立つということです。 多くの場合、値はコンテキストに依存します。この場合、コンテキストはアプリケーション用に選択したアーキテクチャです。 したがって、モデルをガイドラインとして使用することは有用ですが、基本的な原則に焦点を当て、最後にアーキテクチャのコンテキストで意味のあるものを選択する必要があります。

7. CIとの統合

自動テストの能力と利点は、継続的インテグレーションパイプラインに統合することで大部分が実現されます。 Jenkins は、ビルドおよびデプロイメントパイプラインを宣言的に定義するための一般的な選択肢です。

Jenkinsパイプラインで自動化したテストを統合できます。 ただし、これによりパイプラインの実行時間が長くなることを理解する必要があります。 継続的インテグレーションの主な目的の1つは、迅速なフィードバックです。 速度を落とすテストを追加し始めると、これは競合する可能性があります。

重要なポイントは、単体テストのように高速なテストを、より頻繁に実行されると予想されるパイプラインに追加することです。 たとえば、コミットごとにトリガーされるパイプラインにUIテストを追加してもメリットがない場合があります。 ただし、これは単なるガイドラインであり、最後に、処理するアプリケーションの種類と複雑さによって異なります。

8. 結論

この記事では、ソフトウェアテストの基本について説明しました。 さまざまなテストタイプと、利用可能なツールの1つを使用してそれらを自動化することの重要性を理解しました。

さらに、テストピラミッドの意味を理解しました。 これは、SpringBootを使用して構築されたマイクロサービスを使用して実装しました。

最後に、特にマイクロサービスのようなアーキテクチャのコンテキストで、テストピラミッドの関連性を確認しました。