Spring Bootユニットテストと統合テストの詳細な解を実現します。


本教程で提供したツールをどのように使うかを学び、Spring Boot環境でセルテストと統合テストを作成します。
1.概要
本論文では、ユニットテストをどのように作成するかを知り、Spring Boot環境に集積する。インターネットでこのテーマに関する教程を大量に見つけることができますが、一つのページで必要なすべての情報を見つけるのは難しいです。初期開発者はユニットテストと統合テストの概念を混同していることがよく分かります。特にSpring生態系について話しています。異なる注釈の使い分けを説明してみます。
2.ユニットテストvs.統合テスト
ウィキペディアはユニットテストということです。
コンピュータプログラミングでは、ユニットテストは、ソースコードの単一のユニット、1つまたは複数のコンピュータプログラムモジュールのセット、および関連する制御データ、使用プロセス、および操作プロセスをテストして、それらが適切に使用されるかどうかを判定するソフトウェアテスト方法である。
統合テスト
統合テスト(統合テストと呼ばれる場合もあります。I&Tと略されます。)はソフトウェアテストの段階です。この段階では、各ソフトウェアモジュールを組み合わせてテストを行います。
簡単に言えば、私たちはユニットテストをする時、コードユニットをテストしただけです。毎回一つの方法だけをテストして、テストコンポーネントと相互作用する他のすべてのコンポーネントを含みません。
一方、統合テストでは、各コンポーネント間の統合をテストします。セルテストのために、これらのコンポーネントの挙動は必要とされるものと一致することが分かりましたが、それらがどうやって一緒に働くかは分かりません。これは集積テストの役割です。
3.Javaユニットテスト
すべてのJava開発者は、JUnitが実行ユニットテストのメインフレームであることを知っています。期待を断言するために多くの注釈が提供されています。
Hamcrestはソフトウェアテストのための付加的な枠組みです。Hamcrestは既存のmatcherクラスを使ってコード中の条件をチェックすることができます。またカスタムmatcherによって実現されます。JUnitでHamcrest matcherを使うには、assertThat文を使って、あと一つまたは複数のmatcherを使わなければなりません。
ここでは、この2つのフレームを使った簡単なテストが見られます。

import static org.hamcrest.CoreMatchers.allOf;
import static org.hamcrest.CoreMatchers.anyOf;
import static org.hamcrest.CoreMatchers.both;
import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.everyItem;
import static org.hamcrest.CoreMatchers.hasItems;
import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.CoreMatchers.sameInstance;
import static org.hamcrest.CoreMatchers.startsWith;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNotSame;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertSame;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;

import java.util.Arrays;

import org.hamcrest.core.CombinableMatcher;
import org.junit.Test;

public class AssertTests {
 @Test
 public void testAssertArrayEquals() {
  byte[] expected = "trial".getBytes();
  byte[] actual = "trial".getBytes();
  assertArrayEquals("failure - byte arrays not same", expected, actual);
 }

 @Test
 public void testAssertEquals() {
  assertEquals("failure - strings are not equal", "text", "text");
 }

 @Test
 public void testAssertFalse() {
  assertFalse("failure - should be false", false);
 }

 @Test
 public void testAssertNotNull() {
  assertNotNull("should not be null", new Object());
 }

 @Test
 public void testAssertNotSame() {
  assertNotSame("should not be same Object", new Object(), new Object());
 }

 @Test
 public void testAssertNull() {
  assertNull("should be null", null);
 }

 @Test
 public void testAssertSame() {
  Integer aNumber = Integer.valueOf(768);
  assertSame("should be same", aNumber, aNumber);
 }

 // JUnit Matchers assertThat
 @Test
 public void testAssertThatBothContainsString() {
  assertThat("albumen", both(containsString("a")).and(containsString("b")));
 }

 @Test
 public void testAssertThatHasItems() {
  assertThat(Arrays.asList("one", "two", "three"), hasItems("one", "three"));
 }

 @Test
 public void testAssertThatEveryItemContainsString() {
  assertThat(Arrays.asList(new String[] { "fun", "ban", "net" }), everyItem(containsString("n")));
 }

 // Core Hamcrest Matchers with assertThat
 @Test
 public void testAssertThatHamcrestCoreMatchers() {
  assertThat("good", allOf(equalTo("good"), startsWith("good")));
  assertThat("good", not(allOf(equalTo("bad"), equalTo("good"))));
  assertThat("good", anyOf(equalTo("bad"), equalTo("good")));
  assertThat(7, not(CombinableMatcher.<Integer> either(equalTo(3)).or(equalTo(4))));
  assertThat(new Object(), not(sameInstance(new Object())));
 }

 @Test
 public void testAssertTrue() {
  assertTrue("failure - should be true", true);
 }
}
4.私たちのケースを紹介します。
簡単なプログラムを書きましょう。その目的は漫画のために基本的な検索エンジンを提供することです。
4.1.Maven依存
まず、私達の工程に依存するいくつかの追加が必要です。

<dependency>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-starter-test</artifactId>
 <scope>test</scope>
</dependency>
<dependency>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
 <groupId>org.projectlombok</groupId>
 <artifactId>lombok</artifactId>
 <version>1.16.20</version>
 <scope>provided</scope>
</dependency>
4.2.Modelを定義する
私達のモデルはとても簡単です。二つの種類だけがあります。MangaとMangaResoultです。
4.2.1.Manga類
Mangaクラスは、システムが検索したMangaの例を示しています。Lombookを使ってサンプルコードを減らす。

package com.mgiglione.model;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Getter @Setter @NoArgsConstructor @AllArgsConstructor @Builder
public class Manga {
  private String title;
  private String description;
  private Integer volumes;
  private Double score;
}
4.2.2.MangaResoult
MangaResult類はManga Listを含めた包装類です。

package com.mgiglione.model;
import java.util.List;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter @Setter @NoArgsConstructor
public class MangaResult {
  private List<Manga> result;
}
4.3.Serviceを実現する
このServiceを実現するために、Jikan Moeによって提供される無料のAPIインターフェースを使用します。
Restit TemplateはAPIをREST呼び出しを開始するためのSpringクラスです。

package com.mgiglione.service;

import java.util.List;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;

import com.mgiglione.model.Manga;
import com.mgiglione.model.MangaResult;

@Service
public class MangaService {

  Logger logger = LoggerFactory.getLogger(MangaService.class);
  private static final String MANGA_SEARCH_URL="http://api.jikan.moe/search/manga/";
  
  @Autowired
  RestTemplate restTemplate;
  
  public List<Manga> getMangasByTitle(String title) {
    return restTemplate.getForEntity(MANGA_SEARCH_URL+title, MangaResult.class).getBody().getResult();
  }
}
4.4.Controllerを実現する
次のステップは、二つのエンドポイントが暴露されたREST Controllerを書くことであり、一つは同期であり、一つは非同期であり、それはテストの目的にのみ使用される。このControllerは上で定義されたServiceを使用しています。

package com.mgiglione.controller;

import java.util.List;
import java.util.concurrent.CompletableFuture;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Async;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;

import com.mgiglione.model.Manga;
import com.mgiglione.service.MangaService;

@RestController
@RequestMapping(value = "/manga")
public class MangaController {

  Logger logger = LoggerFactory.getLogger(MangaController.class);
  
  @Autowired
  private MangaService mangaService;  
  
  @RequestMapping(value = "/async/{title}", method = RequestMethod.GET)
  @Async
  public CompletableFuture<List<Manga>> searchASync(@PathVariable(name = "title") String title) {
    return CompletableFuture.completedFuture(mangaService.getMangasByTitle(title));
  }
  
  @RequestMapping(value = "/sync/{title}", method = RequestMethod.GET)
  public @ResponseBody <List<Manga>> searchSync(@PathVariable(name = "title") String title) {
    return mangaService.getMangasByTitle(title);
  }
}
4.5.システムの起動とテスト

mvn spring-boot:run
次に、Let's try it:

curl http://localhost:8080/manga/async/ken
curl http://localhost:8080/manga/sync/ken
出力例:

{ 
  "title":"Rurouni Kenshin: Meiji Kenkaku Romantan",
  "description":"Ten years have passed since the end of Bakumatsu, an era of war that saw the uprising of citizens against the Tokugawa shogunate. The revolutionaries wanted to create a time of peace, and a thriving c...",
  "volumes":28,
  "score":8.69
},
{ 
  "title":"Sun-Ken Rock",
  "description":"The story revolves around Ken, a man from an upper-class family that was orphaned young due to his family's involvement with the Yakuza; he became a high school delinquent known for fighting. The only...",
  "volumes":25,
  "score":8.12
},
{ 
  "title":"Yumekui Kenbun",
  "description":"For those who suffer nightmares, help awaits at the Ginseikan Tea House, where patrons can order much more than just Darjeeling. Hiruko is a special kind of a private investigator. He's a dream eater....",
  "volumes":9,
  "score":7.97
}
5.Spring Bootアプリケーションのユニットテスト
Spring Bootは、テストを簡単にするための強力なクラスを提供します。
Spring Bootに基づいて動作するテストクラスにこの注釈を指定することができます。
従来のSpring TestContect Fraamewark以外に、以下の機能も提供されています。
  • @Contect Configration(loader=...)に特別な声明がない場合は、Spring BootContact LoaderをデフォルトのContect Loaderとして使用する。
  • は、ネストされた@Configrationを使用せずに関連クラスを明示せずに指定した場合、@Spring BootConfigrationを自動的に検索します。
  • により、Propertiesを使用して、Evironment属性を定義することができます。
  • は、定義されたまたはランダムポート上で完全に動作するWebサーバを起動する機能を含む、異なるWeb環境モードをサポートする。
  • は、Webサーバ上で完全に実行されるWebテストで使用するために、TestRect Templateおよび/またはWebTest Client Beanを登録する。
  • ここでは二つのコンポーネントしかテストが必要です。MangaServiceとMangaController
    5.1.MangaServiceのユニットテストを行う
    MangaServiceをテストするためには、外部のコンポーネントから分離する必要があります。この例では、外部のコンポーネントだけが必要です。RertTemplateは、遠隔APIを呼び出すためにそれを使用します。
    私たちが行うべきことは、RestTemplate Beanをシミュレートし、常に固定された所定の応答で応答させることである。Spring TestはMockitoライブラリを結合して拡張しています。@MockBean注釈を通して、シミュレーションBeanを配置することができます。
    
    package com.mgiglione.service.test.unit;
    import static org.mockito.ArgumentMatchers.any;
    import static org.mockito.Mockito.when;
    import java.io.IOException;
    import java.util.List;
    import org.junit.Test;
    import org.junit.runner.RunWith;
    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.http.HttpStatus;
    import org.springframework.http.ResponseEntity;
    import org.springframework.test.context.junit4.SpringRunner;
    import org.springframework.web.client.RestTemplate;
    import static org.assertj.core.api.Assertions.assertThat;
    
    import com.mgiglione.model.Manga;
    import com.mgiglione.model.MangaResult;
    import com.mgiglione.service.MangaService;
    import com.mgiglione.utils.JsonUtils;
    
    @RunWith(SpringRunner.class)
    @SpringBootTest
    public class MangaServiceUnitTest {
      
      @Autowired
      private MangaService mangaService;
      
      // MockBean is the annotation provided by Spring that wraps mockito one
      // Annotation that can be used to add mocks to a Spring ApplicationContext.
      // If any existing single bean of the same type defined in the context will be replaced by the mock, if no existing bean is defined a new one will be added.
      @MockBean
      private RestTemplate template;
      
      @Test
      public void testGetMangasByTitle() throws IOException {
        // Parsing mock file
        MangaResult mRs = JsonUtils.jsonFile2Object("ken.json", MangaResult.class);
        // Mocking remote service
        when(template.getForEntity(any(String.class), any(Class.class))).thenReturn(new ResponseEntity(mRs, HttpStatus.OK));
        // I search for goku but system will use mocked response containing only ken, so I can check that mock is used.
        List<Manga> mangasByTitle = mangaService.getMangasByTitle("goku");
        assertThat(mangasByTitle).isNotNull()
          .isNotEmpty()
          .allMatch(p -> p.getTitle()
            .toLowerCase()
            .contains("ken"));
      }
      
    }
    5.2.Manga Controllerに対してユニットテストを行う
    MangaServiceのユニットテストで行われたように、私たちはコンポーネントを分離する必要があります。この場合には,MangaService Beanをシミュレートする必要がある。
    それから、もう一つの問題があります。Controllerの部分はHttpRequestを管理するシステムの一部です。したがって、このような行為をシミュレートするシステムが必要です。完全なHTTPサーバを起動しないでください。
    MockMvcは、この操作を行うSpring類です。これは異なる方法で設定できます。
  • Standarlone Contectを使用する
  • WebAppleication Contectを使用する
  • は、テストクラスで@Spring BootTest、@AutoConfigreMockMvcを使用することにより、Springにすべてのコンテキストをロードさせ、自動組立
  • を実現する。
  • は、自動組立
  • を実現するために、テストクラスで@WebMvcTest注釈を使用してWeb層コンテキストをロードさせる。
    
    package com.mgiglione.service.test.unit;
    
    import static org.hamcrest.Matchers.is;
    import static org.mockito.ArgumentMatchers.any;
    import static org.mockito.Mockito.when;
    import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.asyncDispatch;
    import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
    import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
    import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
    import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.request;
    import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
    import static org.springframework.test.web.servlet.setup.MockMvcBuilders.standaloneSetup;
    
    import java.util.ArrayList;
    import java.util.List;
    
    import org.junit.Before;
    import org.junit.Test;
    import org.junit.runner.RunWith;
    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.http.MediaType;
    import org.springframework.test.context.junit4.SpringRunner;
    import org.springframework.test.web.servlet.MockMvc;
    import org.springframework.test.web.servlet.MvcResult;
    import org.springframework.web.context.WebApplicationContext;
    
    import com.mgiglione.controller.MangaController;
    import com.mgiglione.model.Manga;
    import com.mgiglione.service.MangaService;
    
    @SpringBootTest
    @RunWith(SpringRunner.class)
    public class MangaControllerUnitTest {
    
      MockMvc mockMvc;
      
      @Autowired
      protected WebApplicationContext wac;
      
      @Autowired
      MangaController mangaController;
      
      @MockBean
      MangaService mangaService;
      
      /**
       * List of samples mangas
       */
      private List<Manga> mangas;
      
      @Before
      public void setup() throws Exception {
        this.mockMvc = standaloneSetup(this.mangaController).build();// Standalone context
        // mockMvc = MockMvcBuilders.webAppContextSetup(wac)
        // .build();
        Manga manga1 = Manga.builder()
          .title("Hokuto no ken")
          .description("The year is 199X. The Earth has been devastated by nuclear war...")
          .build();
        Manga manga2 = Manga.builder()
          .title("Yumekui Kenbun")
          .description("For those who suffer nightmares, help awaits at the Ginseikan Tea House, where patrons can order much more than just Darjeeling. Hiruko is a special kind of a private investigator. He's a dream eater....")
          .build();
        mangas = new ArrayList<>();
        mangas.add(manga1);
        mangas.add(manga2);
      }
      
      @Test
      public void testSearchSync() throws Exception {
        // Mocking service
        when(mangaService.getMangasByTitle(any(String.class))).thenReturn(mangas);
        mockMvc.perform(get("/manga/sync/ken").contentType(MediaType.APPLICATION_JSON))
          .andExpect(status().isOk())
          .andExpect(jsonPath("$[0].title", is("Hokuto no ken")))
          .andExpect(jsonPath("$[1].title", is("Yumekui Kenbun")));
      }
    
      @Test
      public void testSearchASync() throws Exception {
        // Mocking service
        when(mangaService.getMangasByTitle(any(String.class))).thenReturn(mangas);
        MvcResult result = mockMvc.perform(get("/manga/async/ken").contentType(MediaType.APPLICATION_JSON))
          .andDo(print())
          .andExpect(request().asyncStarted())
          .andDo(print())
          // .andExpect(status().is2xxSuccessful()).andReturn();
          .andReturn();
        // result.getRequest().getAsyncContext().setTimeout(10000);
        mockMvc.perform(asyncDispatch(result))
          .andDo(print())
          .andExpect(status().isOk())
          .andExpect(jsonPath("$[0].title", is("Hokuto no ken")));
      }
    }
    コードで見たように、最初のソリューションを選択するのは、一番軽い量の一つであり、Springコンテキストでロードされたオブジェクトに対してより良い管理ができます。
    非同期テストでは、まずサービスを呼び出して、async Displatch方法を起動して非同期挙動をシミュレートしなければならない。
    6.Spring Bootアプリケーションの集積テスト
    統合試験については、下流の通信を提供して、私たちの主要なコンポーネントを確認したいです。
    6.1.Manga Serviceを統合テストする
    このテストもとても簡単です。私たちは何もシミュレーションする必要がありません。目的はリモートManga APIを呼び出すことです。
    
    package com.mgiglione.service.test.integration;
    
    import static org.assertj.core.api.Assertions.assertThat;
    
    import java.util.List;
    
    import org.junit.Test;
    import org.junit.runner.RunWith;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.test.context.SpringBootTest;
    import org.springframework.test.context.junit4.SpringRunner;
    
    import com.mgiglione.model.Manga;
    import com.mgiglione.service.MangaService;
    
    @RunWith(SpringRunner.class)
    @SpringBootTest
    public class MangaServiceIntegrationTest {
    
      @Autowired
      private MangaService mangaService;
      
      @Test
      public void testGetMangasByTitle() {
          List<Manga> mangasByTitle = mangaService.getMangasByTitle("ken");
          assertThat(mangasByTitle).isNotNull().isNotEmpty();
      }
    }
    6.2.Manga Controllerを統合テストする
    このテストはセルテストとよく似ていますが、このケースではMangaServiceのシミュレーションは不要です。
    
    package com.mgiglione.service.test.integration;
    import static org.hamcrest.Matchers.hasItem;
    import static org.hamcrest.Matchers.is;
    import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.asyncDispatch;
    import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
    import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
    import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
    import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.request;
    import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
    import static org.springframework.test.web.servlet.setup.MockMvcBuilders.standaloneSetup;
    
    import org.junit.Before;
    import org.junit.Test;
    import org.junit.runner.RunWith;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.test.context.SpringBootTest;
    import org.springframework.http.MediaType;
    import org.springframework.test.context.junit4.SpringRunner;
    import org.springframework.test.web.servlet.MockMvc;
    import org.springframework.test.web.servlet.MvcResult;
    import org.springframework.web.context.WebApplicationContext;
    
    import com.mgiglione.controller.MangaController;
    
    @SpringBootTest
    @RunWith(SpringRunner.class)
    public class MangaControllerIntegrationTest {
    
      // @Autowired
      MockMvc mockMvc;
      
      @Autowired
      protected WebApplicationContext wac;
      
      @Autowired
      MangaController mangaController;
      
      @Before
      public void setup() throws Exception {
        this.mockMvc = standaloneSetup(this.mangaController).build();// Standalone context
        // mockMvc = MockMvcBuilders.webAppContextSetup(wac)
        // .build();
      }
      
      @Test
      public void testSearchSync() throws Exception {
        mockMvc.perform(get("/manga/sync/ken").contentType(MediaType.APPLICATION_JSON))
          .andExpect(status().isOk())
          .andExpect(jsonPath("$.*.title", hasItem(is("Hokuto no Ken"))));
      }
      
      @Test
      public void testSearchASync() throws Exception {
        MvcResult result = mockMvc.perform(get("/manga/async/ken").contentType(MediaType.APPLICATION_JSON))
          .andDo(print())
          .andExpect(request().asyncStarted())
          .andDo(print())
          .andReturn();
        mockMvc.perform(asyncDispatch(result))
          .andDo(print())
          .andExpect(status().isOk())
          .andExpect(jsonPath("$.*.title", hasItem(is("Hokuto no Ken"))));
      }
    }
    7.結論
    Spring Boot環境におけるセルテストと集積テストの主な違いを知りました。Hamcrestのように試験編纂を簡略化するための枠組みを理解しました。もちろん、私の@Spring BootTestにすべてのコードを見つけることもできます。
    GitHub倉庫
    作者:Marco Giglione
    以上が本文の全部です。皆さんの勉強に役に立つように、私たちを応援してください。