@DataJpaTestでRepositoryのテスト

テスト書いてますか?私は好きです!嫌いな人は多いみたいですけど...

Springのテスト部分のリファレンス読んで忘れないうちにまとめよう精神が働いたので書きます。 今回は、@DataJpaTestというレポジトリのテストを書くためのアノテーションを紹介します。

@DataJpaTestアノテーションは、デフォルトでインメモリDBの設定をしたり、@EntityがついたクラスをBean登録したり、@RepositoryついたクラスをBean登録するなどSpring Data JPAレポジトリの設定してくれたりします。 @DataJpaTestのソースコードをのぞいてみるとこんな感じの設定が行われているようです。

...
@AutoConfigureDataJpa
@AutoConfigureTestDatabase
@AutoConfigureTestEntityManager
...

Data JPAテストは、トランザクショナルで各テストの終わりにロールバックします。 もし、こうしたくない場合は、次のように書いてください。

@Transactional(propagation = Propagation.NOT_SUPPORTED)

また、data JPAテストでは、標準的なJPA EntityManagerの代わりにTestEntityManagerのBeanがインジェクトされます。

ソースコード

GitHub

pom.xml

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    <dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <scope>runtime</scope>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
    <exclusions>
        <exclusion>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        </exclusion>
        </exclusions>
    </dependency>
    <dependency>
        <groupId>org.junit.platform</groupId>
    <artifactId>junit-platform-launcher</artifactId>
    <version>1.2.0</version>
    <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-engine</artifactId>
    <version>5.2.0</version>
    <scope>test</scope>
    </dependency>

    <dependency>
        <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-params</artifactId>
    <version>1.2.0</version>
    <scope>test</scope>
    </dependency>
</dependencies>
  • Java8
  • SpringBoot 2.0.3.RELEASE
  • H2
  • JUnit5

Brand.java

@Entity
public class Brand implements Serializable {
    enum Gender {
        MAN, WOMAN, UNISEX
    }
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Integer id;
    @Column(nullable = false)
    private String name;
    @Enumerated(EnumType.STRING)
    private Gender gender;
    public Brand(String name, Gender gender) {
        this.name = name;
        this.gender = gender;
    }
    ... getter, setter
}

id、ブランド名、ブランドの対象性別をもつBrandエンティティで話を進めていきます。

BrandRepository.java(今回のテスト対象)

@Repository
public interface BrandRepository extends JpaRepository<Brand, Integer> {
    List<Brand> findByGender(Gender gender);
    Integer countByGender(Gender gender);
}

性別でブランドを検索する findByGenderメソッドと性別ごとのブランド数を検索する countByGenderメソッドを準備しました。

BrandRepositoryTest.java

// SpringExtention.classは、Junit 5上でSpring TestContext Frameworkを使えるようにしている
@ExtendWith(SpringExtension.class)
class BrandRepositoryTest {

    @Nested
    @DataJpaTest
    class FindByGender {
        @Autowired
    private TestEntityManager entityManager;
        @Autowired
        private BrandRepository brandRepository;

    @BeforeEach
    void beforeEach() {
            entityManager.persist(new Brand("STOF", Gender.UNISEX));
        entityManager.persist(new Brand("ETHOSENS", Gender.MAN));
        entityManager.persist(new Brand("dulcamara", Gender.UNISEX));
    }

    @Test
    void man() {
        List<Brand> brands = brandRepository.findByGender(Gender.MAN);
        org.assertj.core.api.Assertions.assertThat(brands)
            .extracting(Brand::getName, Brand::getGender)
            .containsExactly(Tuple.tuple("ETHOSENS", Gender.MAN));
        }

    @Test
    void woman() {
            List<Brand> brands = brandRepository.findByGender(Gender.WOMAN);
        org.assertj.core.api.Assertions.assertThat(brands)
                    .hasSize(0);
    }
    }

    @Nested
    @DataJpaTest
    class CountByGender {
        @Autowired
    private TestEntityManager entityManager;

    @Autowired
    private BrandRepository brandRepository;

    @BeforeEach
    void beforeEach() {
        entityManager.persist(new Brand("STOF", Gender.UNISEX));
        entityManager.persist(new Brand("ETHOSENS", Gender.MAN));
        entityManager.persist(new Brand("dulcamara", Gender.UNISEX));
        }

    @Test
    void man() {
        int count = brandRepository.countByGender(Gender.MAN);
        Assertions.assertEquals(1, count);
    }

    @Test
    void woman() {
        int count = brandRepository.countByGender(Gender.WOMAN);
        Assertions.assertEquals(0, count);
    }
    }
}

検証には一部AssertJを使ってます。AssertJについては、こちらで簡単に触れてます。

BrandRepositoryTest.java実行する

実行時のログを抜粋して紹介します。

...
2018-07-05 00:20:30.440  INFO 5456 --- [           main] o.s.j.d.e.EmbeddedDatabaseFactory        : Starting embedded database: url='jdbc:h2:mem:393e83d1-908e-4fb0-91d2-8becd045235b;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=false', username='sa'
...

H2データベースを使ってることがわかります。

...
Hibernate: drop table brand if exists
Hibernate: drop sequence if exists hibernate_sequence
Hibernate: create sequence hibernate_sequence start with 1 increment by 1
Hibernate: create table brand (id integer not null, gender varchar(255), name varchar(255) not null, primary key (id))
...

brandテーブルを作ってます。

...
Hibernate: insert into brand (gender, name, id) values (?, ?, ?)
Hibernate: insert into brand (gender, name, id) values (?, ?, ?)
Hibernate: insert into brand (gender, name, id) values (?, ?, ?)
...

beforeEachの処理が行われている様子です。

...
Hibernate: select count(brand0_.id) as col_0_0_ from brand brand0_ where brand0_.gender=?
...
Hibernate: select brand0_.id as id1_0_, brand0_.gender as gender2_0_, brand0_.name as name3_0_ from brand brand0_ where brand0_.gender=?
...

countByGenderとfindByGenderが実行されていることがわかります。

...
Hibernate: drop table brand if exists
Hibernate: drop sequence if exists hibernate_sequenceでテーブルとhibernate_sequenceが削除されていることがわかります。

テストが終わるとテーブルとhibernate_sequenceを削除しています。 簡単にレポジトリのテストができることがわかっていただけたかと思います。

hibernate_sequenceは最後にしか消されないので、ずっと連番が振られる続けるので、自動採番のIDの値の検証する場合はお気をつけてください。テストごとに初期データをinsertするの美しくないのでうまい方法見つけたいです。

参考

43.3.11 Auto-configured Data JPA Tests

追記

2018/7/12 create tableやdrop tableをFlywayを使って行なっているため、application.propertiesに以下のように書いているとテーブルが作成されずに「xxxテーブルがありません」みたいに怒られますので、Flywayは使えるようにしてください。

spring.flyway.enabled=false