189
205

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Spring Bootでテストを書くときのやりかたまとめ

Last updated at Posted at 2017-10-16

追記

  • 2017/11/2 RestTemplateのテスト追加

この資料について

  • Spring bootを使ったプロジェクトをやっていて、その際にテストどう書くねんってなったからまとめた
  • この資料では下記のテストの仕方をまとめた
  • Service(POJOっぽいやつ)
  • Controller(Mockかして順序とかの担保)
  • Repository(CSVでテストデータ用意してテスト)
  • リクエストパラメータのテスト(バリデーションを実際にしてみてテスト)
  • RestTemplateをつかって外部のAPIたたくクライアントクラスのテスト
  • 機能テスト(実際に叩いてテスト)
  • 基本調べて書いたのでもっとこうしたほうがきれいにかけるやで、っていうのがあればぜひ

実行環境

  • Java 1.8
  • SpringBoot 1.5.7.RELEASE
  • 詳しくは下記のリポジトリ参照

ここでやったことのコード

spring-boot-test-sample

Serviceのテスト

とりあえず動かす

とりあえずJUnitで普通のテストを書いてみる
まずはテスト対象のクラスを定義する

// テスト対象のクラス
@Service
public class DemoService {

    public String greeting(String greet) {
        if (StringUtils.isEmpty(greet)) {
            return "Say something...";
        }

        return "hello";
    }
}

次にテストコードを書いてみる

public class DemoServiceTest {

    private DemoService demoService;

    @Before
    public void setUp() {
        demoService = new DemoService();
    }

    @Test
    public void greeting_あいさつがないとき() {
        assertThat(demoService.greeting(null), is("Say something..."));
    }

    @Test
    public void greeting_あいさつがあるとき() {
        assertThat(demoService.greeting("something"), is("hello"));
    }
}


解説

特に書くことないけどJUnitとhamcrestをつかってテスト書いた

Mock化してテスト

何かしらのクラスをインスタンス化してテストするケースにて、呼ばれた経路だけ担保すればいいようなケースをテストしてみる
ここではcontrollerのテストでやってみる

とりあえず動かす

ちょっと冗長な書き方してるけど気にしないでください。

@RequiredArgsConstructor
@RestController
@RequestMapping("/v1/demo")
@Validated
public class DemoController {

    @Autowired
    private DemoService demoService;

    @Autowired
    private DemoRepository demoRepository;

    @GetMapping
    public String demo(@Valid DemoRequest demoRequest) {
        DemoEntity demoEntity = demoRepository.findByCode(demoRequest.getCode());
        String value = demoEntity.getValue();
        String ret = demoService.greeting(value);
        return ret;
    }
}

テストを書く


public class DemoControllerTest {

    private static String GREET = "hoge";

    @Mock
    private DemoRepository demoRepository;

    @Mock
    private DemoService demoService;

    @InjectMocks
    private DemoController demoController;

    @Before
    public void setup() {
        MockitoAnnotations.initMocks(this);
    }


    @Test
    public void 順番の担保() {
        DemoEntity demoEntity = DemoEntity.builder()
                .code("hoge")
                .value("fuga")
                .updateAt(null)
                .build();
        when(demoRepository.findByCode(anyObject())).thenReturn(demoEntity);
        when(demoService.greeting(anyObject())).thenReturn("hello");
        DemoRequest demoRequest = new DemoRequest();
        demoRequest.setCode(GREET);
        String actual = demoController.demo(demoRequest);

        verify(demoRepository, times(1)).findByCode(GREET);
        verify(demoService, times(1)).greeting("fuga");

        assertThat(actual, is("hello"));
    }

}

解説

  • リクエストされたパラメータでDemoRepositoryからデータ引いてその結果からDemoServiceの関数呼んでデータ返却、みたいな関数のテストを書いた。
  • Mock化するインスタンスには@Mockとつけて、Mockされたインスタンスをつっこむインスタンスには@InjectMocksをつけて宣言する
  • @Beforeの部分で、それぞれ宣言したインスタンスを初期化して実際にMock化、つっこむ、ってところをやってる
  • テストの関数の部分で、こう呼ばれたらこう返す、ってのをwhenとthenReturnで行っている。どの引数で呼ばれるかはあとで行っているのでanyObject()にしてる
  • 何回呼ばれるか、どの引数でよばれるかの確認はverifyで行っている
  • 最終的になんの結果が呼ばれるかはassertThatとisで行っている

Repositoryのテスト(CSVでテストデータ管理する)

テストデータをCSVで用意し、テスト前にそのデータでテーブルを初期化してRepositoryのテストを行う

とりあえず動かす

テスト対象のクラスをつくる

今回findByCodeのテストをしたいのでそれだけ宣言する

package com.example.demo;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Component;

@Component
public interface DemoRepository extends JpaRepository<DemoEntity, String>{
    public DemoEntity findByCode(String code);
}

テスト対象RepositoryのEntityをつくる
@Table(name = "demo")
@Entity
@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class DemoEntity {

    @Id
    private String code;
    private String value;
    private ZonedDateTime updateAt;
}

テストコードを書く

CSVで読み込むために色々やっているのでコメントで軽く解説を残す

package com.example.demo;

import com.github.springtestdbunit.DbUnitTestExecutionListener;
import com.github.springtestdbunit.annotation.DatabaseSetup;
import com.github.springtestdbunit.annotation.DbUnitConfiguration;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.test.context.TestExecutionListeners;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.context.support.DependencyInjectionTestExecutionListener;
import org.springframework.test.context.transaction.TransactionalTestExecutionListener;
import org.springframework.transaction.annotation.Transactional;

import java.time.ZonedDateTime;

import static org.hamcrest.beans.SamePropertyValuesAs.samePropertyValuesAs;
import static org.junit.Assert.assertThat;


@RunWith(SpringRunner.class)
@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@TestExecutionListeners({
        DependencyInjectionTestExecutionListener.class,
        TransactionalTestExecutionListener.class,
        DbUnitTestExecutionListener.class})
@DbUnitConfiguration(
        dataSetLoader = ReplacementCsvDataSetLoader.class // ここでCSVでデータ読み込むReplacementDataSetLoaderのクラスを指定
)
@Transactional
public class DemoRepositoryTest {

    private static final String DATA_FILE_PATH = "/DemoRepository/";

    @Autowired
    private DemoRepository demoRepository;

    @DatabaseSetup(value = DATA_FILE_PATH + "findByCode/")
    @Test
    public void findByCodeTest() {
        DemoEntity expected = DemoEntity.builder()
                .code("hoge")
                .value("fuga")
                .updateAt(ZonedDateTime.parse("2017-01-01T00:00:00+09:00:00[Asia/Tokyo]"))
                .build();
        DemoEntity actual = demoRepository.findByCode("hoge");
        assertThat(actual, samePropertyValuesAs(expected));
    }
}

ReplacementDataSetLoaderを拡張する
package com.example.demo;

import com.github.springtestdbunit.dataset.ReplacementDataSetLoader;

public class ReplacementCsvDataSetLoader extends ReplacementDataSetLoader {
    public ReplacementCsvDataSetLoader() {
        super(new CsvDataSetLoader()); // ここで実装したCsvをloadするLoaderを読み込む
    }
}

Csv用のLoaderを実装する
package com.example.demo;

import com.github.springtestdbunit.dataset.AbstractDataSetLoader;
import org.dbunit.dataset.IDataSet;
import org.dbunit.dataset.csv.CsvURLDataSet;
import org.springframework.core.io.Resource;

public class CsvDataSetLoader extends AbstractDataSetLoader {
    public CsvDataSetLoader() {
    }

    @Override
    protected IDataSet createDataSet(Resource resource) throws Exception {
        return new CsvURLDataSet(resource.getURL());
    }
}

テストデータ

demoというテーブル名なのでresourcesにdemo.csvファイルを作成

code,value,update_at
hoge,fuga,2017-01-01 00:00:00
fuga,hoge,2017-01-01 00:00:00
table-ordering.txt

使用するテーブルを定義するファイル

demo

解説

それぞれ解説する

実装とEntity
  • 特筆すべき事はとくにない。普通にRepositoryとEntityつくっただけ
  • 強いて言うならZonedDataTimeはhibernateのバージョンが5.2以上じゃないと動かないので依存を少しいじらないといけない
    *spring-boot-starter-data-jpaの現行のバージョン(1.5.3くらい)だとhibernateのバージョンは5.0系
テストデータをCSVで読み込むところ
  • けっこういろんなファイルをつくったが、自分でごりごり書くような内容はなく、用意されたものを使っただけ
  • ReplacementDataSetLoaderのコンストラクタにてAbstractDataSetLoaderを拡張したLoaderを宣言すれば良い
  • AbstractDataSetLoaderを拡張したCsvDataSetLoaderを自作したが、そこではcreateDataSet関数をOverrideした関数をつくればよく、そこの返却値ではDBUnitで用意されたCSV用のインスタンスを返すだけで良い
  • CSVの読み込み方はディレクトリを指定して、そのディレクトリにあるtable-ordering.txtを読み込み、そこに定義されている同じ階層にあるテーブル名のファイルを読み込んでくれる
テストコード
  • @DatabaseSetupで指定したディレクトリのデータをテーブルにぶっこんでからテストが動く
  • その後は普通にデータを引いてきて、assertしているだけ

リクエストパラメータのテスト

リクエストパラメータをDTOでやってる場合のテストを書いた
注: リクエストパラメータをdemo(@Valid @Size(max = 5)String code)みたいに引数でバリデーション設定している場合はコントローラーとか機能テストじゃないと無理

とりあえず動かす

RequestパラメータのDTO

@Getter
@Setter
public class DemoRequest {

    @NotNull
    @Size(max = 5)
    private String code;
}

テストコード

public class DemoRequestTest {

    private Validator validator;

    @Before
    public void setUp() {
        validator = Validation.buildDefaultValidatorFactory().getValidator();
    }

    @Test
    public void validatorTest_桁あふれ() {
        DemoRequest demoRequest = new DemoRequest();
        demoRequest.setCode("123456");
        Set<ConstraintViolation<DemoRequest>> violationSet = validator.validate(demoRequest);
        assertThat(violationSet.size(), is(1));
        violationSet.forEach(violation -> {
            assertThat(violation.getConstraintDescriptor().getAnnotation(), instanceOf(Size.class));
        });
    }
}

解説

  • テストコード内で実際にvalidateしてその結果をテストしている
  • Validatorをインスタンス化して対象のDTOをvalidateしている
  • validateした結果はConstraintViolationのSetとして返却されるのでその内容をぐるぐる回してテストしている

機能テスト

TestRestTemplateを使ってAPIを実際に動かすテストを書いた

とりあえず動かす

  • Controllerは上記のものを流用
  • DbUnit周りも上記のものを流用
テストコード

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@TestExecutionListeners(
        listeners = {
                TransactionalTestExecutionListener.class,
                DbUnitTestExecutionListener.class},
        mergeMode = TestExecutionListeners.MergeMode.MERGE_WITH_DEFAULTS
)
@DbUnitConfiguration(
        dataSetLoader = ReplacementCsvDataSetLoader.class
)
public class DemoApiFunctionalTest {

    @Autowired
    private TestRestTemplate testRestTemplate;

    @DatabaseSetup(value = "/DemoRepository/findByCode/")
    @Test
    public void demoGetTest_正常系() {

        // 実行
        ResponseEntity actual = testRestTemplate.exchange(
                "/v1/demo?code=hoge", HttpMethod.GET, new HttpEntity<>(null, null), String.class);
        assertThat(actual.getBody().toString(), is("hello"));
        assertThat(actual.getStatusCode(), is(HttpStatus.OK));
    }
}

解説

  • TestRestTemplateを利用して実際にアプリケーションを実行してその返却値をテストした
  • DBにテスト用のデータをいれたいのでDBUnitの設定をしてテストデータをいれた
  • portをランダムにしたいので@SpringBootTestで設定した
  • @TestExecutionのmergeModeでデフォルトのリスナーに追加している

RestTemplateのテスト

とりあえず動かす

実行するAPIの返却値

同一リポジトリにhttp://localhost:8080/v1/demo/api
で以下を返却するAPIをつくった


{
	"code" : "012",
	"name" : "nanika"
}
テスト対象のクラス
@Component
public class DemoClient {

    private final RestTemplate restTemplate;

    @Value("${demo.api.hostname}")
    private String hostname;

    @Value("${demo.api.endpoint}")
    private String endpoint;

    public DemoClient(RestTemplateBuilder restTemplateBuilder) {
        this.restTemplate = restTemplateBuilder
                .setConnectTimeout(1000)
                .setReadTimeout(1000)
                .build();
    }

    public DemoApiResponse get() {
        UriComponents uriComponents = UriComponentsBuilder.fromUriString(hostname + endpoint).build();
        return restTemplate.getForObject(uriComponents.toUri(), DemoApiResponse.class);
    }
}
application.ymlの設定
demo:
  api:
    hostname: "http://localhost:8080"
    endpoint: "/v1/demo/api"
テストコード
@RunWith(SpringRunner.class)
@RestClientTest(DemoClient.class)
public class DemoClientTest {

    @Autowired
    private MockRestServiceServer mockServer;

    @Autowired
    private DemoClient demoClient;

    @Test
    public void testGet() {
        mockServer.expect(requestTo("http://test.api.server/v1/demo/api"))
                .andExpect(method(HttpMethod.GET))
                .andRespond(withSuccess("{\"code\":\"123\",\"name\":\"name\"}", MediaType.APPLICATION_JSON));
        DemoApiResponse expected = new DemoApiResponse("123", "name");

        DemoApiResponse actual = demoClient.get();

        assertThat(actual, samePropertyValuesAs(expected));
    }

}
テストに使うapplication.ymlの設定
demo:
  api:
    hostname: "http://test.api.server"
    endpoint: "/v1/demo/api"

解説

  • DemoClientではRestTemplateをコンストラクタでRestTemplateBuilderを使ってインスタンス化し、application.ymlで設定されたURLにリクエストするようにした
  • テストではMockRestServiceServerを使ってRestTemplateに割り込んでテストデータが返却できるようにした
  • テストの際はtestパッケージ以下のapplication.ymlの設定を読み込むためMock化したURLにリクエストがいくようになっている

まとめ

  • CSVを使ってDBのデータをセットアップするのは面倒だけど、基本用意されているものだけでできるので楽ちん
  • TestRestTemplateがあるから機能テストもさっとできる。DBUnit使えばテストデータもぶっこめる
  • Spring bootのテストなのかそうでないのかで書き方変わるから注意

参考にしたサイト

189
205
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
189
205

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?