Help us understand the problem. What is going on with this article?

dropwizard-testingが便利でした

More than 3 years have passed since last update.

Dropwizardでテストする際、dropwizard-testingを使うといくらか便利だったので記述しておきます。

maven設定

pom.xml
    <dependency>
      <groupId>io.dropwizard</groupId>
      <artifactId>dropwizard-testing</artifactId>
      <version>${dropwizard.version}</version>
      <scope>test</scope>
    </dependency>

Unit Test

Representationクラスのテスト

RepresentationはJacksonで変換対象になるクラスになります。

テスト対象のクラスはこちらです。

Saying.java
public class Saying {
    private long id;

    @Length(max = 3)
    private String content;

    public Saying() {
        // Jackson deserialization
    }

    public Saying(long id, String content) {
        this.id = id;
        this.content = content;
    }

    @JsonProperty
    public long getId() {
        return id;
    }

    @JsonProperty
    public String getContent() {
        return content;
    }
}

まずは、期待値をファイルに記述しておきます。

fixtures/saying.json
{"id":1,"content":"Hello"}

次にテストクラスです。

SayingTest.java
import static org.fest.assertions.api.Assertions.*;
import io.dropwizard.jackson.Jackson;
import io.dropwizard.testing.FixtureHelpers;

import org.junit.Test;

import com.fasterxml.jackson.databind.ObjectMapper;

public class SayingTest {

    private static final ObjectMapper MAPPER = Jackson.newObjectMapper();

    private static final String FIXTURE = "fixtures/saying.json";

    @Test
    public void deserializesFromJSON() throws Exception {
        final Saying target = new Saying(1, "Hello");
        final Saying expected = MAPPER.readValue(
                FixtureHelpers.fixture(FIXTURE), Saying.class);
        assertThat(target).isEqualsToByComparingFields(expected);
    }

    @Test
    public void serializesToJSON() throws Exception {
        final Saying target = new Saying(1, "Hello");
        final String actual = MAPPER.writeValueAsString(target);
        assertThat(actual).isEqualTo(FixtureHelpers.fixture(FIXTURE));
    }

}

テストケースは基本的にJacksonのObjectMapperで「StringとRepresentationの変換」をします。
FIXTUREの内容をFixtureHelpers.fixture()で取得します。

アサーション部分は、hamcrest ではありません。
これは、 fest-assert という流れるようにアサーションが記述できるライブラリになります。
使わなくてもいいですが、dropwizard-testingが依存しているし、便利なので利用することをお勧めします。

Resourceクラスのテスト

Resourceクラスは、主にURIリクエストからRepresentationを返します。

テスト対象のクラスはこちらです。

PeopleResource.java
@Path("/people")
@Produces(MediaType.APPLICATION_JSON)
public class PeopleResource {

    private final PersonDao peopleDAO;

    public PeopleResource(PersonDao peopleDAO) {
        this.peopleDAO = peopleDAO;
    }

    @GET
    @UnitOfWork
    @Path("/{personId}")
    public Person getPerson(@PathParam("personId") LongParam personId) {
        final Optional<Person> person = peopleDAO.findById(personId.get());
        if (!person.isPresent()) {
            throw new NotFoundException("{status:notfound}");
        }
        return person.get();
    }
}

次にテストクラスです。
Mockitoを使っているのは、dropwizard-testingが依存しているからです。jmockitを使っても構いません。

PeopleResourceTest.java
import static org.fest.assertions.api.Assertions.*;
import static org.mockito.Mockito.*;
import io.dropwizard.testing.junit.ResourceTestRule;

import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Test;

import com.github.ko2ic.core.Person;
import com.github.ko2ic.db.PersonDao;
import com.google.common.base.Optional;

public class PeopleResourceTest {

    private static final PersonDao dao = mock(PersonDao.class);

    @ClassRule
    public static final ResourceTestRule resources = ResourceTestRule.builder()
            .addResource(new PeopleResource(dao)).build();

    private final Person person = new Person(1, "ko2ic", "job");

    @Before
    public void setup() {
        when(dao.findById(Long.parseLong("1"))).thenReturn(
                Optional.fromNullable(person));
    }

    @Test
    public void testGetPerson() {
        assertThat(resources.client().resource("/people/1").get(Person.class))
                .isEqualsToByComparingFields(person);
        verify(dao).findById(Long.parseLong("1"));
    }
}

ポイントは、ResourceTestRuleです。
これは、Jerseyのリソースをテストするためのdropwizardのクラスになります。

Integration Test

モックを使わないでフロントからDBまでのテストの自動化ができれば便利です。
Dropwizardではこれも簡単にできるようにクラスが用意されています。

PeopleResourceIntegrationTest.java
public class PeopleResourceIntegrationTest {

    private static final String FIXTURE_PATH = "fixtures/integration/";

    @ClassRule
    public static final DropwizardAppRule<HelloWorldConfiguration> RULE = new DropwizardAppRule<>(
            HelloWorldApplication.class, "example.yml");

    @Test
    public void testGetPerson() throws JsonParseException,
            JsonMappingException, IOException {
        Client client = new Client();

        ClientResponse response = client.resource(
                String.format("http://localhost:%d/people/2",
                        RULE.getLocalPort())).get(ClientResponse.class);

        assertThat(response.getStatus()).isEqualTo(200);

        String entity = response.getEntity(String.class);
        assertThat(entity).isEqualTo(
                FixtureHelpers.fixture(FIXTURE_PATH + "getPerson.json"));
    }
}

ポイントはDropwizardAppRuleです。
あとは、ほとんどがjerseyのクラスになります。

このテストを動作させるとサーバーが起動して、対象のURLにアクセスした場合の戻り値を検証できます。

このサンプルでは、期待値をファイルに記述しています。

fixtures/integration/getPerson.json
{"id":2,"fullName":"ko2ic second","jobTitle":"jobTitle2"}

データ投入方法

このままテストを動作させても検証エラーになります。データを入れてないからです。
次はデータを入れる処理を追加します。liquibaseのAPIを利用します。
ポイントは、migrations.xmlとdata.xmlです。

    private static Database database;

    private Liquibase migrations;

    @BeforeClass
    public static void beforeClass() throws SQLException, LiquibaseException {
        database = createDatabase();
    }

    @AfterClass
    public static void afterClass() throws DatabaseException, LockException {
        database.close();
        database = null;
    }

    @Before
    public void before() throws DatabaseException, SQLException,
            LiquibaseException {
        migrations = createLiquibase("migrations.xml");
        String context = null;
        migrations.update(context);
    }

    @After
    public void after() throws DatabaseException, LockException {
        migrations.dropAll();
    }

    private Liquibase createLiquibase(String migrations) throws SQLException,
            DatabaseException, LiquibaseException {
        Liquibase liquibase = new Liquibase(migrations,
                new ClassLoaderResourceAccessor(), database);
        return liquibase;
    }

    private static Database createDatabase() throws SQLException,
            DatabaseException {
        DataSourceFactory dataSourceFactory = RULE.getConfiguration()
                .getDataSourceFactory();
        Properties info = new Properties();
        info.setProperty("user", dataSourceFactory.getUser());
        info.setProperty("password", dataSourceFactory.getPassword());
        org.h2.jdbc.JdbcConnection h2Conn = new org.h2.jdbc.JdbcConnection(
                dataSourceFactory.getUrl(), info);
        JdbcConnection conn = new JdbcConnection(h2Conn);
        Database database = DatabaseFactory.getInstance()
                .findCorrectDatabaseImplementation(conn);
        return database;
    }

    @Test
    public void testGetPerson() throws JsonParseException,
            JsonMappingException, IOException, DatabaseException, SQLException,
            LiquibaseException {

        Liquibase data = createLiquibase("data.xml");
        data.update("testGetPerson");
        ・・・
    }

migrations.xmlが本番でも利用するテーブルです。
@Before@Afterでテーブルを作成・削除しているのは、それぞれのテストを完全に独立させたいからです。

src/main/resources/migrations.xml
<?xml version="1.0" encoding="UTF-8"?>

<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
         http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd">

    <changeSet id="1" author="ko2ic">
        <createTable tableName="people">
            <column name="id" type="bigint" autoIncrement="true">
                <constraints primaryKey="true" nullable="false" />
            </column>
            <column name="fullName" type="varchar(255)">
                <constraints nullable="false" />
            </column>
            <column name="jobTitle" type="varchar(255)" />
        </createTable>
    </changeSet>
    <changeSet id="2" author="ko2ic">
        <addColumn tableName="people">
            <column name="deleteFlag" type="boolean" defaultValue="0">
                <constraints nullable="false" />
            </column>
        </addColumn>
    </changeSet>    
</databaseChangeLog>

テスト用のデータを入れるためにdata.xmlを用意しています。
contextにtestGetPersonという名前を付けて対象のテスト(testGetPerson)の始めに呼んでテストで必要なデータをinsertしています。

src/test/resources/data.xml
<?xml version="1.0" encoding="UTF-8"?>

<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
         http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd">

   <changeSet id="1" author="ko2ic" context="testGetPerson">
        <insert tableName="people">
            <column name="id" value="1"/>
            <column name="fullName" value="ko2ic first"/>
            <column name="jobTitle" value="jobTitle1"/>
            <column name="deleteFlag" value="0"/>
        </insert>
        <insert tableName="people">
            <column name="id" value="2"/>
            <column name="fullName" value="ko2ic second"/>
            <column name="jobTitle" value="jobTitle2"/>
            <column name="deleteFlag" value="0"/>
        </insert>
        <insert tableName="people">
            <column name="id" value="3"/>
            <column name="fullName" value="ko2ic third"/>
            <column name="jobTitle" value="jobTitle3"/>
            <column name="deleteFlag" value="0"/>
        </insert>        
    </changeSet>    
</databaseChangeLog>

このサンプルでは、Integration用のテストデータを作るためにliquibaseのAPIを利用しましたが、DaoのUnit Testを行うときにもこの仕組みは利用できると思います。

ko2ic
最近はflutterです。
uzabase
企業活動の意思決定を支える情報インフラの提供
https://www.uzabase.com/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away