Java
dropwizard
Liquibase

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