Spring BootでControllerを含めたテストをする場合に、MockMvcBuilders.webAppContextSetup()
でセットアップして @WebIntegrationTests
にすると通常のアプリケーションとしてテストできるが、数が多くなってくると重い。
いくつか改善できそうなポイントを調べたので記録しておく。
なお、極力Springを使わないのがベストだが、今回はそれができないケースを想定しているので除外。
Tomcatを起動するため、やはり遅い。
代わりに @WebAppConfiguration
だけにしてみる。
デフォルトでは、src/main/resources/data.sql
というファイルがあればそれが起動時に読み込まれてしまう。
これを、以下のように src/test/resources/data-empty.sql
というファイルを用意して
select 1;
テストクラスに以下のように付与する。
@TestPropertySource(properties = "spring.datasource.data: /data-empty.sql")
すると、デフォルトの data.sql は読み込まれずにこちらのファイルが読み込まれる。
data.sql が大きいファイルの場合は多少効果があるかもしれない。
例えば spring-boot-starter-data-elasticsearch
の dependency を取り込んでいると、それだけで Spring Boot 起動時に Elasticsearch が起動してしまう。
Elasticsearch の機能を使う Controller なら仕方ないが、関係のない Controller のテストでは起動しないようにしたい。
Spring BootのAuto-configurationによってこれが起動してしまうため、Elasticsearch関連のAuto-configurationを除外すればいいが、これだけだと、ElasticsearchRepository
を使っている @Service
のDIが失敗するため、これらも ComponentScan の対象外にする必要がある。
以下のようなアノテーションを定義して
// ElasticsearchRepositoryを使うServiceに対して
// @Serviceの代わりに付与
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Service
public @interface ElasticsearchDependentService {
}
// ElasticsearchDependentServiceを使うControllerに対して
// @Controllerの代わりに付与
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Controller
public @interface ElasticsearchDependentController {
}
// Elasticsearchを使わないアプリケーションクラスに付与。
// com.example.spring.App は SpringBootApplication を付与したクラス。
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Configuration
@ComponentScan(value = "com.example.spring", excludeFilters = {
@ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = App.class),
@ComponentScan.Filter(type = FilterType.ANNOTATION, classes = ElasticsearchDependentService.class),
@ComponentScan.Filter(type = FilterType.ANNOTATION, classes = ElasticsearchDependentController.class)
})
@EnableAutoConfiguration(exclude={ElasticsearchAutoConfiguration.class, ElasticsearchDataAutoConfiguration.class})
@EnableJpaRepositories("com.example.spring.repository")
@EntityScan("com.example.spring.domain")
public @interface NoElasticsearchConfiguration {
}
テスト用のアプリケーションクラスを定義しておく。
@NoElasticsearchConfiguration
public class NoElasticsearchTestApp {
}
このクラスを SpringApplicationConfiguration としてテストクラスで使えば、Elasticsearchを起動せずにテストできる。
@SpringApplicationConfiguration(classes = NoElasticsearchTestApp.class)
Controller が全てスキャンされてしまうのも避けたいが、上記のように ComponentScan の excludeFilter を調整しても @WebAppConfiguration
をつけただけで結局スキャンされてしまう。
@WebAppConfiguration
を使わないと色々DIに失敗するため、自分で @Bean
を定義してやる必要がある。
まず全部の Controller をスキャン対象外にするアノテーションを用意。
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Configuration
@ComponentScan(value = "com.example.spring", excludeFilters = {
@ComponentScan.Filter(type = FilterType.ANNOTATION, classes = SpringBootApplication.class),
@ComponentScan.Filter(type = FilterType.ANNOTATION, classes = ElasticsearchDependentService.class),
@ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Controller.class)
})
@EnableJpaRepositories("com.example.spring.repository")
@EntityScan("com.example.spring.domain")
public @interface StandaloneControllerConfiguration {
}
これを付与した内部クラスを作って、その中で不足しているBeanを登録する。
ここでは ErrorAttributes と テスト対象の Controller。
@SpringApplicationConfiguration(classes = {NoElasticsearchAutoConfigurationConfiguration.class, ProjectControllerNoWebAppTests.Config.class})
public class ProjectControllerNoWebAppTests {
@StandaloneControllerConfiguration
static class Config {
@Bean
ErrorAttributes errorAttributes() {
return new DefaultErrorAttributes();
}
@Bean
ProjectController projectController() {
return new ProjectController();
}
}
@ClassRule
public static final SpringClassRule SPRING_CLASS_RULE = new SpringClassRule();
@Rule
public final SpringMethodRule springMethodRule = new SpringMethodRule();
protected MockMvc mockMvc;
@Autowired
private ProjectController projectController;
@Before
public void setUp() {
mockMvc = MockMvcBuilders.standaloneSetup(projectController).build();
}
ErrorAttributes は、独自にエラーハンドリングをする際に ErrorController を実装したクラスを用意してその中でDIしたりする。
WebAppConfiguration でないと、DIできるクラスがないということで @Autowired
が失敗する。
また、Pageable を使ったメソッドがある場合、インスタンス化できないというエラーで失敗する。
これには PageableHandlerMethodArgumentResolver
を使えばいい。
mockMvc = MockMvcBuilders.standaloneSetup(projectController)
.setCustomArgumentResolvers(new PageableHandlerMethodArgumentResolver())
.build();
ソースコードはこちら。