はじめに
SpringBootでテストを、となった際にテストサイズを考えsmall testのUnitテストを実装したいという場面があるが、small testをどのように実装するのかについて記載した記事をあまり見かけないので、自身の備忘録としても残しておく。
※ググると大抵dependencyにspring test
系を持たせているものが見つかるが、ミニマムのunit testを実装する上ではそれらは不要。
この記事を読むと分かる事は以下。
- SpringBootにおけるMockitoを用いたsmall testを導入する際の依存関係(
pom.xml
の内容) - DI(
@Autowired
)されているフィールドの扱い方などテストコードの基本的な書き方 - 細かいテストの記述方法やテクニック
small testとは
そもそもsmall testとはどういうもの?という話になるが、small testとは以下のようなテストとして定義できる。
- ネットワークへアクセスしない
- データベースへアクセスしない
- ファイルシステムへアクセスしない
- 外部システムへアクセスしない
- シングルスレッドで動作する
- システムプロパティへのアクセスをしない
カバレッジの話
上記のsmall testの概念に加えて、ソースコードのテストにおいて切り離せない話でカバレッジがあるが、
いずれのカバレッジでもsmall testは実現できるので、ここではどのカバレッジで実装するかは扱わない。
JUnit5×Mockitoでsmall test
SpringBootにおけるMockitoでのsmall testを導入する際の依存関係(pom.xml
の内容)
プロジェクト(パッケージ)管理ツールにmavenを使っている場合、pom.xmlは以下のようになる。
※色々省略しているが、samll testであれば基本的にspring-boot-starter-test
などの依存は不要。
<project xmlns="・・・">
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.5</version>
</parent>
<properties>
<junit-jupiter.version>5.7.1</junit-jupiter.version>
<mockito.version>3.7.7</mockito.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- jdbc, json, lombokなどなど -->
<dependency>
<groupId>com.googlecode.json-simple</groupId>
<artifactId>json-simple</artifactId>
<version>1.1.1</version>
<exclusions>
<exclusion>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-inline</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
ここでは
- static methodのMock化が行えるように
mockito-inline
を依存に追加 - JUnitの依存を持つ
json-simple
のJUnit依存をexclusionに指定し除外
※自動的に依存が追加されるような場合はexclusion
を使い依存を明示的に除外する
をしている。
ここで注意として、MockitoであればPowerMockを使いたい場面もあるかもしれないが、現時点でJunit5では使えないよう・・・。
DI(@Autowired
)されているフィールドの扱い方などテストコードの基本的な書き方
まずMockitoを使うため、@ExtendWith
でMockitoExtension.class
を宣言し継承する。
SpringでDI(@Autowired
)されている各フィールドは@Mock
でmock化する。
また、それらをtest対象のクラスにinjectするために、test対象クラスに対しては@InjectMocks
を記載しmockを注入する。
基本的なパターンとしては以下のような実装になる。
※Mockitoの使い方・構文についてはここを参照。
// 省略
@ExtendWith(MockitoExtension.class)
class SampleTest {
@Mock
private Hoge mockHoge;
@Mock
private Fuga mockFuga;
@InjectMocks
private Sample testClass;
@Test
void testFoo() {
doReturn("hoge").when(mockHoge).getHoge();
assertEquals("hoge", testClass.foo());
}
// 省略
}
細かいテストの記述方法やテクニック
static methodのmock化の方法
MockedStatic
を利用する。
// 省略
@ExtendWith(MockitoExtension.class)
class MockStaticSampleTest {
@Mock
private Hoge mockHoge;
@InjectMocks
private MockStaticSample testClass;
@Test
void testGetCompleteDate() {
try (MockedStatic<DateUtil> mocked = mockStatic(DateUtil.class)) {
mocked.when(() -> DateUtil.getNumbersOnlyFormat(anyString()).thenReturn("19700101");
assertEquals("19700101", testClass.getCompleteDate());
mocked.verify(() -> DateUtil.getNumbersOnlyFormat(anyString());
}
}
// 省略
}
テスト対象のメソッドで、自身のクラスのメソッド(private以外)を呼び出すような場合にテストを簡単にするテクニック(spy
の利用)
以下のようにテスト対象のメソッド内で、自身の他のメソッド(private以外)が呼ばれるような場合、spy
を使うと一部だけmock化ができテストを書きやすくなる。
具体的には、自身のメソッドを呼び出しその戻り値を使って分岐をするようなメソッドをテストする際に、カバレッジをC2など複雑なものを採用すると、自身のメソッドの返り値まで考えて複雑な実装しなければならず大変になるが、それを簡単にする事ができる。
// 省略
class PrivateContainSample {
SampleContentsDto foo() {
SampleDto dto = generate(commonDto);
// 何かの処理
}
SampleDto generate(SampleCommonDto dot) {
// 何かの処理
}
// 省略
}
// 省略
@ExtendWith(MockitoExtension.class)
class MockStaticSampleTest {
@Mock
private Hoge mockHoge;
@InjectMocks
private PrivateContainSample testClass;
private PrivateContainSample sptTestClass;
@BeforeEach
void setUp() {
spyTest = spy(testClass);
}
@Test
void testFoo() {
doReturn(new SampleDto()).when(sptTestClass).generate(any(SampleCommonDto.class));
assertNotNull(spyTestClass.foo());
}
// 省略
}
spy
で記述した部分(generate
)だけがmock化されるので、foo
はmock化されず通常通りテストが実施できる。
共通のmockの定義方法・引数が可変長のもののmock化方法
テスト対象のクラス全体で使われているメソッドで、1つの共通の戻り値があればいいような場合は、@BeforeEach
でmock化してしまうと便利。
また、可変長引数の場合はMockito.<String[]>any()
のように記述する。
// 省略
@ExtendWith(MockitoExtension.class)
class SampleTest {
@Mock
private Hoge mockHoge;
@Mock
private CommonMessage mockCommonMessage;
@InjectMocks
private Sample testClass;
@BeforeEach
void setUp() {
doReturn(new Message()).when(mockCommonMessage).getMessage(Mockito.<String[]>any());
}
@Test
void testFoo() {
// 何かのテスト
}
// 省略
}
MockMultipartFileを使ったmock化の方法
/**
* Create MockMultipartFile used for JUnit test
*
* @param fileName File name used for testing
* @return MockMultipartFile
*/
private MockMultipartFile createTestFile(String fileName) throws Exception {
MockMultipartFile multipartFile;
String path = new File(".").getAbsoluteFile().getParent();
File file = new File(path + "/src/test/resources/hogehoge/" + fileName + ".xlsx");
try (FileInputStream input = new FileInputStream(file)) {
multipartFile = new MockMultipartFile("file", file.getName(), CONTENT_TYPE, IOUtils.toByteArray(input));
}
return multipartFile;
}