SpringBootのテストスライスアノテーションについて

# 概要

SpringBootのテストスライスアノテーションについて、どのような実装になっているのか探ってみた。

# テストスライスアノテーションとは？

まず、ここでのテストスライスアノテーションは、アプリケーションの全量ではなく一部だけをテストしたい、といった場合に利用するアノテーションのことを指す。([このページ](https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#appendix.test-auto-configuration.slices)にSpringBoot公式で提供されている全量が記載されている)

テストスライスアノテーションを利用する動機としては、ユニットテスト実行時間は最小限に抑えつつ、意味のあるユニットテストをしたい、という希望を叶えることにある。
SpringBootで作成するWebアプリケーションであれば、リクエスト・レスポンスのバリデーションや変換を受け持つController層、ビジネスロジックを持つService層、DB等へのアクセスを受け持つRepository層に分けて作成を行う。
この前提のもと、例としてController層のテストを考える。`@SpringBootTest`で全てのユニットテストを書けば、本番稼働の状況と限りなく近づくが、テスト実行時間は増える。一方、Controller層で依存しているものを全てモックに差し替えてテストを行った場合、テスト実行時間は短いが、本番稼働の状況からは遠のく。
`@WebMvcTest`を利用すれば、リクエストのバリデーションやJsonデシリアライズ、レスポンスのJsonシリアライズやエラーハンドリングについて本番相当の部品でテストでき、実行時間も`@SpringBootTest`よりは短くて済む。

# テストスライスアノテーションの中身

テストスライスアノテーションは実際どのように実装されているのかみていく。例として`@DataJpaTest`をみていくが、基本的な構造は他のテストスライスアノテーションでもほぼ一緒である。

## DataJpaTest

SpringBoot2.7.5だと下記のよう。

```java 
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@BootstrapWith(DataJpaTestContextBootstrapper.class)
@ExtendWith(SpringExtension.class)
@OverrideAutoConfiguration(enabled = false)
@TypeExcludeFilters(DataJpaTypeExcludeFilter.class)
@Transactional
@AutoConfigureCache
@AutoConfigureDataJpa
@AutoConfigureTestDatabase
@AutoConfigureTestEntityManager
@ImportAutoConfiguration
public @interface DataJpaTest {
```

特徴的なところをみていく。

### `@BootstrapWith(DataJpaTestContextBootstrapper.class)`

`@BootstrapWith`ではテスト実行時のアプリケーションコンテキストを作成するにあたって、どこを起点に作成するかを決める。
`DataJpaTestContextBootstrapper`は `SpringBootTestContextBootstrapper`を継承しているが、`SpringBootTestContextBootstrapper`では`@DataJpaTest`を付与したパッケージから遡って行って、`@SpringBootConfiguration`が付与されたクラスを起点に選ぶ。なお、`@SpringBootConfiguration`は`@SpringBootApplication`に内包されている。

```java:SpringBootTestContextBootstrapper.java
	protected Class<?>[] getOrFindConfigurationClasses(MergedContextConfiguration mergedConfig) {
		Class<?>[] classes = mergedConfig.getClasses();
		if (containsNonTestComponent(classes) || mergedConfig.hasLocations()) {
			return classes;
		}
		Class<?> found = new AnnotatedClassFinder(SpringBootConfiguration.class) // <- 起点である SpringBootConfiguration.class を探している
				.findFromClass(mergedConfig.getTestClass());
		Assert.state(found != null, "Unable to find a @SpringBootConfiguration, you need to use "
				+ "@ContextConfiguration or @SpringBootTest(classes=...) with your test");
		logger.info("Found @SpringBootConfiguration " + found.getName() + " for test " + mergedConfig.getTestClass());
		return merge(found, classes);
	}
```

そのため、`@DataJpaTest`では通常`@SpringBootApplication`が付与されたクラスを起点にアプリケーションコンテキストを作成することになる。


:::note warn
`@SpringBootConfiguration`へのたどり方は、対象のテストクラスから1つずつパッケージを上がっていって見つける。
そのため、テストクラスが`com.example.slicetest.demo`にあったとしたら、`@SpringBootApplication`を付与したクラスは `com.example.slicetest`, `com.example`, `com`のいずれかに配置されている必要がある。
もし、`com.example.slicetest.application`などに配置されているとエラーになる。
:::

### `@TypeExcludeFilters(DataJpaTypeExcludeFilter.class)`

`@SpringBootApplication`でアプリケーションコンテキストを作る際に、bean登録しないクラスを選別するための設定。

`DataJpaTypeExcludeFilter`の中身は下記。
```java:DataJpaTypeExcludeFilter
public final class DataJpaTypeExcludeFilter extends StandardAnnotationCustomizableTypeExcludeFilter<DataJpaTest> {

	DataJpaTypeExcludeFilter(Class<?> testClass) {
		super(testClass);
	}

}
```

上記のように`DataJpaTypeExcludeFilter`は`StandardAnnotationCustomizableTypeExcludeFilter`を継承しているが、特に新しく設定を追加してはいない。

`StandardAnnotationCustomizableTypeExcludeFilter`をそのまま利用すると、`@SpringBootApplication`を付与したクラスからの`@ComponentScan`はされなくなる。
**つまり、`@Controller`、`@Service`、`@Component`、`@Repository`によるBean定義はアプリケーションコンテキストに入らなくなり、`@Configuration`を付与したクラスでのBean定義も効かなくなる。**

:::note info
`DataJpaTypeExcludeFilter`と同列である、`WebMvcTypeExcludeFilter`では下記のようにComponentScanの対象となるアノテーションやクラスを明示するようにしている。
(ExcludeFilterという名前であったので、てっきり除外するものを列挙しているのだろうと思ったが、逆にscanするものを列挙してそれ以外は省く、という発想であった)

```java
		Set<Class<?>> includes = new LinkedHashSet<>();
		includes.add(ControllerAdvice.class);
		includes.add(JsonComponent.class);
		includes.add(WebMvcConfigurer.class);
		includes.add(WebMvcRegistrations.class);
		includes.add(javax.servlet.Filter.class);
		includes.add(FilterRegistrationBean.class);
		includes.add(DelegatingFilterProxyRegistrationBean.class);
		includes.add(HandlerMethodArgumentResolver.class);
		includes.add(HttpMessageConverter.class);
		includes.add(ErrorAttributes.class);
		includes.add(Converter.class);
		includes.add(GenericConverter.class);
		includes.add(HandlerInterceptor.class);
```
:::

### `@AutoConfigure~~~~`

`DataJpaTest`では下記が設定されている。
- `@AutoConfigureCache`
- `@AutoConfigureDataJpa`
- `@AutoConfigureTestDatabase`
- `@AutoConfigureTestEntityManager`

上記アノテーションによって、`META-INF/spring/~~~.imports`ファイルに含まれたクラスをauto configureする。
ファイル名とauto configureするクラスの対応は下記の通り。
- org.springframework.boot.test.autoconfigure.core.AutoConfigureCache.imports
    - org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration
- org.springframework.boot.test.autoconfigure.orm.jpa.AutoConfigureDataJpa.imports
    - org.springframework.boot.autoconfigure.data.jpa.JpaRepositoriesAutoConfiguration
    - org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration
    - org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
    - org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration
    - org.springframework.boot.autoconfigure.jdbc.JdbcTemplateAutoConfiguration
    - org.springframework.boot.autoconfigure.liquibase.LiquibaseAutoConfiguration
    - org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration
    - org.springframework.boot.autoconfigure.sql.init.SqlInitializationAutoConfiguration
    - org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration
- org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase.imports
    - org.springframework.boot.test.autoconfigure.jdbc.TestDatabaseAutoConfiguration
    - org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
- org.springframework.boot.test.autoconfigure.orm.jpa.AutoConfigureTestEntityManager.imports
    - org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManagerAutoConfiguration


:::note info
`@DataJpaTest`は`@Repository`を付与したクラスをアプリケーションコンテキストに入れるが、その設定はどこにあるのかというと、`JpaRepositoriesAutoConfiguration`にて行っている。
:::

# まとめ

`DataJpaTest`を中心にテストスライスアノテーションの実装について調査した。
調べた結果、テストスライスアノテーションの肝となる箇所は、
- `@TypeExcludeFilters`でどのアノテーション、クラスをアプリケーションコンテキストに入れるか
- `@AutoConfigure~~`で何をauto configureするか

であるということが分かった。


# 参考

https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#features.testing.spring-boot-applications.user-configuration-and-slicing