Springのテストを速くする工夫

2016/10/09
2021/02/25

Spring BootでControllerを含めたテストをする場合に、MockMvcBuilders.webAppContextSetup() でセットアップして @WebIntegrationTests にすると通常のアプリケーションとしてテストできるが、数が多くなってくると重い。

いくつか改善できそうなポイントを調べたので記録しておく。

なお、極力Springを使わないのがベストだが、今回はそれができないケースを想定しているので除外。

WebIntegrationTestsをやめる

Tomcatを起動するため、やはり遅い。
代わりに @WebAppConfiguration だけにしてみる。

Hibernateが読み込むSQLを小さくする

デフォルトでは、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 が大きいファイルの場合は多少効果があるかもしれない。

Auto-configurationを削減する

例えば 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)

WebAppConfigurationを使わない

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();

ソースコードはこちら

© 2010 ksoichiro