SpringBootで作ったWebアプリのユニットテスト

こちらを参考に作り始めたのですが、動かすまでは簡単にできたのですが、ユニットテストの方法が良くわからなかったので調べてみました。

あと、テストにはやっぱりSpockを使いたかったので、その辺も調べました。

Webアプリを作成

公式サイトのチュートリアル通りにやれば一直線です。迷うところは特にありません。 Spockも使うつもりだったので最初からGroovyで作っています。

HelloController.groovy

package la.urau

import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestMethod
import org.springframework.web.bind.annotation.RestController

@RestController
class HelloController {
    @RequestMapping(value = "/hello", method = RequestMethod.GET, produces = ["application/json"])
    String hello() {
        """{"result": true}"""
    }
}

Application.groovy

package la.urau

import org.springframework.boot.SpringApplication
import org.springframework.boot.autoconfigure.EnableAutoConfiguration
import org.springframework.context.annotation.ComponentScan
import org.springframework.context.annotation.Configuration
    
@Configuration
@ComponentScan
@EnableAutoConfiguration
class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application, args)
    }
}

ユニットテストを作成

JUnitでのテスト

たぶんこのSpringJUnit4ClassRunnerを利用するのが一番オーソドックスなやり方なんでしょうか。

HelloControllerTest.groovy

package la.urau

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.SpringApplicationConfiguration
import org.springframework.http.MediaType
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner
import org.springframework.test.context.web.WebAppConfiguration
import org.springframework.test.web.servlet.MockMvc
import org.springframework.web.context.WebApplicationContext
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*
import static org.springframework.test.web.servlet.setup.MockMvcBuilders.*

@RunWith(SpringJUnit4ClassRunner)
@SpringApplicationConfiguration(classes = Application)
@WebAppConfiguration
public class HelloControllerTest {

    @Autowired
    WebApplicationContext wac

    MockMvc mockMvc

    @Before
    void setup() {
        mockMvc = webAppContextSetup(wac).build()
    }

    @Test
    void "sample"() {
        mockMvc.perform(get("/hello"))
        .andExpect(status().isOk())
        .andExpect(content().contentType(MediaType.APPLICATION_JSON))
        .andExpect(jsonPath('$.result').value(true))
        .andExpect(jsonPath('$.foo').doesNotExist())
    }
}

@RunWith(SpringJUnit4ClassRunner)により、JUnit動作時にSpringのApplicationContextなんかを生成してくれます。
次に@SpringApplicationConfigurationで、Configurationクラスを指定します(Applicationクラスには、@CompnentScan@EnableAutoConfigurationなんかが指定されているので、これがConfigurationを兼ねている、、、たぶん) 最後に@WebAppConfigurationを付けることで、WebApplicationContextを利用することができ、Webアプリとしてのテストができる、というわけです。

Controllerに対するテストを行うために、MockMvcが用意されています。MockMvcインスタンスWebApplicationContextを使って生成し、MockMvc#performでエンドポイントにアクセスすることで、結果を取得することが出来ます。

あとは結果の検証コードを書くだけで一応テストまで出来ました。

でも・・・

この方法は、SpringのApplicationContextを生成してしまうため、テスト実行に少し時間がかかってしまいます。ユニットテストは何回も繰り返し実行されるものなので、塵も積もれば山となる感じで時間を浪費してしまいます。

そこで、テストコードを以下のように書き換えます。

public class HelloControllerTest {

    MockMvc mockMvc

    @Before
    void setup() {
        mockMvc = standaloneSetup(new HelloController()).build()
   }

    @Test
    void "sample"() {
        mockMvc.perform(get("/hello"))
        .andExpect(status().isOk())
        .andExpect(content().contentType(MediaType.APPLICATION_JSON))
        .andExpect(jsonPath('$.result').value(true))
        .andExpect(jsonPath('$.foo').doesNotExist())
    }
}

動作的には変わりません。/helloを呼び出してその結果を検証するユニットテストですが、不要なContextを生成しない分テスト実行が格段に早くなります(出力されるログの量が段違い・・・)

Spockでのテスト

以上がJUnitによるテストとなるわけですが、やっぱりSpockを使いたい!というわけで、以下は同等のテストをSpockで記述したものです。

class HelloControllerStandaloneSpec extends Specification {

    @Shared MockMvc mockMvc

    def setup() {
        mockMvc = standaloneSetup(new HelloController()).build()
    }

    def "sample"() {
        expect:
        mockMvc.perform(get("/hello"))
        .andExpect(status().isOk())
        .andExpect(content().contentType(MediaType.APPLICATION_JSON))
        .andExpect(jsonPath('$.result').value(true))
        .andExpect(jsonPath('$.foo').doesNotExist())
    }
}

standaloneで動かす場合は特に注意する点はありません。普通にSpockの作法で記述していけば良いでしょう。

ではContextを使用したい場合どうするか?
これは少し追加が必要となります。

まずはbuild.gradleに以下を追加。

 testCompile 'org.spockframework:spock-spring'

その上で、Specクラスは以下のようになります。

@WebAppConfiguration
@ContextConfiguration(loader = WebDelegatingSmartContextLoader, classes = Application)
class HelloControllerIntegrationSpec extends Specification {

    @Autowired
    WebApplicationContext wac

    @Shared MockMvc mockMvc

    def setup() {
        mockMvc = webAppContextSetup(wac).build()
    }

    def "sample"() {
        expect:
        mockMvc.perform(get("/hello"))
        .andExpect(status().isOk())
        .andExpect(content().contentType(MediaType.APPLICATION_JSON))
        .andExpect(jsonPath('$.result').value(true))
        .andExpect(jsonPath('$.foo').doesNotExist())
    }
}

注意すべきは、JUnitバージョンではあった、@RunWith(SpringJUnit4ClassRunner)@SpringApplicationConfigurationがありません。
まずSpockは@RunWith(Sputnik.class)が付いたSpecificationを継承してしまうため、@RunWithを付けることができません。また、ここにあるとおり、@SpringApplicationConfigurationは読んでくれないようです。

なので、書いてあるとおりなのですが、以下のようにすることでwork-aroundしてやります。

@ContextConfiguration(loader = WebDelegatingSmartContextLoader, classes = Application)

これでContextを使いたい場合でもSpockを使ってやることができました。