JUnit 5チュートリアル、パート2:JUnit5を使用したSpringMVCのユニットテスト

Spring MVCは、エンタープライズJavaアプリケーションを構築するための最も人気のあるJavaフレームワークの1つであり、テストに非常に適しています。設計上、Spring MVCは関心の分離を促進し、インターフェースに対するコーディングを促進します。これらの品質は、Springの依存性注入の実装とともに、Springアプリケーションを非常にテスト可能にします。

このチュートリアルは、JUnit 5を使用した単体テストの紹介の後半です。次に、JUnit 5をSpringと統合する方法を示し、次に、Spring MVCコントローラー、サービス、およびリポジトリーのテストに使用できる3つのツールを紹介します。

ダウンロードコードを取得するこのチュートリアルで使用するアプリケーションの例のソースコードをダウンロードします。JavaWorldのためにStevenHainesによって作成されました。

JUnit5とSpring5の統合

このチュートリアルでは、MavenとSpring Bootを使用しているため、最初に行う必要があるのは、MavenPOMファイルにJUnit5の依存関係を追加することです。

  org.junit.jupiter junit-jupiter 5.6.0 test  

パート1で行ったように、この例ではMockitoを使用します。したがって、JUnit 5Mockitoライブラリを追加する必要があります。

  org.mockito mockito-junit-jupiter 3.2.4 test  

@ExtendWithとSpringExtensionクラス

JUnit 5は拡張インターフェースを定義します。これにより、クラスは実行ライフサイクルのさまざまな段階でJUnitテストと統合できます。@ExtendWithテストクラスにアノテーションを追加し、ロードする拡張クラスを指定することで、拡張機能を有効にできます。拡張機能は、さまざまなコールバックインターフェイスを実装できます。これらは、すべてのテストが実行される前、各テストが実行される前、各テストが実行された後、およびすべてのテストが実行された後に、テストライフサイクル全体で呼び出されます。

Springは、SpringExtension「テストコンテキスト」を作成および維持するために、JUnit5ライフサイクル通知をサブスクライブするクラスを定義します。Springのアプリケーションコンテキストにはアプリケーション内のすべてのSpringBeanが含まれており、依存性注入を実行してアプリケーションとその依存関係を結び付けていることを思い出してください。SpringはJUnit5拡張モデルを使用してテストのアプリケーションコンテキストを維持します。これにより、Springを使用した単体テストの記述が簡単になります。

JUnit5ライブラリをMavenPOMファイルに追加した後、を使用しSpringExtension.classてJUnit5テストクラスを拡張できます。

 @ExtendWith(SpringExtension.class) class MyTests { // ... }

この場合の例は、SpringBootアプリケーションです。幸い、@SpringBootTestアノテーションにはすでにアノテーションが含まれている@ExtendWith(SpringExtension.class)ため、含める必要があるのは@SpringBootTest

Mockito依存関係の追加

各コンポーネントを個別に適切にテストし、さまざまなシナリオをシミュレートするために、各クラスの依存関係のモック実装を作成する必要があります。ここでMockitoが登場します。Mockitoのサポートを追加するには、POMファイルに次の依存関係を含めます。

  org.mockito mockito-junit-jupiter 3.2.4 test  

JUnit 5とMockitoをSpringアプリケーションに統合した後、@MockBeanアノテーションを使用してテストクラスでSpring Bean(サービスやリポジトリなど)を定義するだけで、Mockitoを活用できます。これが私たちの例です:

 @SpringBootTest public class WidgetServiceTest { /** * Autowire in the service we want to test */ @Autowired private WidgetService service; /** * Create a mock implementation of the WidgetRepository */ @MockBean private WidgetRepository repository; ... } 

この例ではWidgetRepositoryWidgetServiceTestクラス内にモックを作成しています。Springがこれを確認すると、自動的に接続されるWidgetServiceため、テストメソッドでさまざまなシナリオを作成できます。各テストメソッドはWidgetRepository、要求されたものを返す、WidgetまたはOptional.empty()データが見つからないクエリに対してを返すなど、の動作を構成します。このチュートリアルの残りの部分では、これらのモックBeanを構成するさまざまな方法の例を見ていきます。

SpringMVCサンプルアプリケーション

Springベースの単体テストを作成するには、それらを作成するためのアプリケーションが必要です。幸い、Springシリーズのチュートリアル「MasteringSpring Framework 5、Part 1:SpringMVC」のサンプルアプリケーションを使用できます。そのチュートリアルのサンプルアプリケーションをベースアプリケーションとして使用しました。より強力なRESTAPIを使用して変更し、テストするものがさらにいくつかあるようにしました。

サンプルアプリケーションは、RESTコントローラー、サービスレイヤー、およびSpring DataJPAを使用してH2インメモリデータベースとの間で「ウィジェット」を永続化するリポジトリを備えたSpringMVCWebアプリケーションです。図1は概要です。

スティーブンヘインズ

ウィジェットとは何ですか?

AWidgetは、ID、名前、説明、およびバージョン番号を持つ単なる「もの」です。この場合、ウィジェットにはJPAアノテーションが付けられ、エンティティとして定義されます。WidgetRestController上の実行するアクションにRESTfulなAPIコールを変換するSpring MVCのコントローラですWidgetsWidgetService以下のためのビジネス機能を定義する標準のSpringサービスですWidgets。最後に、これWidgetRepositoryはSpring Data JPAインターフェースであり、Springは実行時に実装を作成します。次のセクションでテストを作成するときに、各クラスのコードを確認します。

Springサービスのユニットテスト

Springサービスをテストする方法を確認することから始めましょう。 これは、MVCアプリケーションでテストするのが最も簡単なコンポーネントだからです。このセクションの例では、チュートリアルの後半で行いますが、新しいテストコンポーネントやライブラリを導入することなく、JUnit5とSpringの統合を調べることができます。

まず、リスト1とリスト2にそれぞれ示されているWidgetServiceインターフェースとWidgetServiceImplクラスを確認します。

リスト1.Springサービス・インターフェース(WidgetService.java)

 package com.geekcap.javaworld.spring5mvcexample.service; import com.geekcap.javaworld.spring5mvcexample.model.Widget; import java.util.List; import java.util.Optional; public interface WidgetService { Optional findById(Long id); List findAll(); Widget save(Widget widget); void deleteById(Long id); }

リスト2.Springサービス実装クラス(WidgetServiceImpl.java)

 package com.geekcap.javaworld.spring5mvcexample.service; import com.geekcap.javaworld.spring5mvcexample.model.Widget; import com.geekcap.javaworld.spring5mvcexample.repository.WidgetRepository; import com.google.common.collect.Lists; import org.springframework.stereotype.Service; import java.util.ArrayList; import java.util.List; import java.util.Optional; @Service public class WidgetServiceImpl implements WidgetService { private WidgetRepository repository; public WidgetServiceImpl(WidgetRepository repository) { this.repository = repository; } @Override public Optional findById(Long id) { return repository.findById(id); } @Override public List findAll() { return Lists.newArrayList(repository.findAll()); } @Override public Widget save(Widget widget) { // Increment the version number widget.setVersion(widget.getVersion()+1); // Save the widget to the repository return repository.save(widget); } @Override public void deleteById(Long id) { repository.deleteById(id); } }

WidgetServiceImpl is a Spring service, annotated with the @Service annotation, that has a WidgetRepository wired into it through its constructor. The findById(), findAll(), and deleteById() methods are all passthrough methods to the underlying WidgetRepository. The only business logic you'll find is located in the save() method, which increments the version number of the Widget when it is saved.

The test class

In order to test this class, we need to create and configure a mock WidgetRepository, wire it into the WidgetServiceImpl instance, and then wire the WidgetServiceImpl into our test class. Fortunately, that's far easier than it sounds. Listing 3 shows the source code for the WidgetServiceTest class.

Listing 3. The Spring service test class (WidgetServiceTest.java)

 package com.geekcap.javaworld.spring5mvcexample.service; import com.geekcap.javaworld.spring5mvcexample.model.Widget; import com.geekcap.javaworld.spring5mvcexample.repository.WidgetRepository; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.test.context.junit.jupiter.SpringExtension; import java.util.Arrays; import java.util.List; import java.util.Optional; import static org.mockito.Mockito.doReturn; import static org.mockito.ArgumentMatchers.any; @SpringBootTest public class WidgetServiceTest { /** * Autowire in the service we want to test */ @Autowired private WidgetService service; /** * Create a mock implementation of the WidgetRepository */ @MockBean private WidgetRepository repository; @Test @DisplayName("Test findById Success") void testFindById() { // Setup our mock repository Widget widget = new Widget(1l, "Widget Name", "Description", 1); doReturn(Optional.of(widget)).when(repository).findById(1l); // Execute the service call Optional returnedWidget = service.findById(1l); // Assert the response Assertions.assertTrue(returnedWidget.isPresent(), "Widget was not found"); Assertions.assertSame(returnedWidget.get(), widget, "The widget returned was not the same as the mock"); } @Test @DisplayName("Test findById Not Found") void testFindByIdNotFound() { // Setup our mock repository doReturn(Optional.empty()).when(repository).findById(1l); // Execute the service call Optional returnedWidget = service.findById(1l); // Assert the response Assertions.assertFalse(returnedWidget.isPresent(), "Widget should not be found"); } @Test @DisplayName("Test findAll") void testFindAll() { // Setup our mock repository Widget widget1 = new Widget(1l, "Widget Name", "Description", 1); Widget widget2 = new Widget(2l, "Widget 2 Name", "Description 2", 4); doReturn(Arrays.asList(widget1, widget2)).when(repository).findAll(); // Execute the service call List widgets = service.findAll(); // Assert the response Assertions.assertEquals(2, widgets.size(), "findAll should return 2 widgets"); } @Test @DisplayName("Test save widget") void testSave() { // Setup our mock repository Widget widget = new Widget(1l, "Widget Name", "Description", 1); doReturn(widget).when(repository).save(any()); // Execute the service call Widget returnedWidget = service.save(widget); // Assert the response Assertions.assertNotNull(returnedWidget, "The saved widget should not be null"); Assertions.assertEquals(2, returnedWidget.getVersion(), "The version should be incremented"); } } 

The WidgetServiceTest class is annotated with the @SpringBootTest annotation, which scans the CLASSPATH for all Spring configuration classes and beans and sets up the Spring application context for the test class. Note that WidgetServiceTest also implicitly includes the @ExtendWith(SpringExtension.class) annotation, through the @SpringBootTest annotation, which integrates the test class with JUnit 5.

The test class also uses Spring's @Autowired annotation to autowire a WidgetService to test against, and it uses Mockito's @MockBean annotation to create a mock WidgetRepository. At this point, we have a mock WidgetRepository that we can configure, and a real WidgetService with the mock WidgetRepository wired into it.

Testing the Spring service

最初のテストメソッドはtestFindById()WidgetServicefindById()メソッドを実行OptionalしますWidget。このメソッドは、を含むを返す必要があります。まず、返しWidgetてほしいを作成しWidgetRepositoryます。次に、MockitoAPIを利用してWidgetRepository::findByIdメソッドを構成します。モックロジックの構造は次のとおりです。

 doReturn(VALUE_TO_RETURN).when(MOCK_CLASS_INSTANCE).MOCK_METHOD 

この場合は、私たちが言っている:リターンOptionalの私たちのWidgetリポジトリの際findById()の方法は、(1としての引数で呼ばれていますlong)。

次に、引数1を指定してWidgetService'sfindByIdメソッドを呼び出します。次に、それが存在し、返されWidgetたものがモックWidgetRepositoryを返すように構成したものであることを検証します。