dropwizard-testingが便利でした

  • 16
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

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を行うときにもこの仕組みは利用できると思います。