Jersey Test + MockitoでJAX-RSリソースクラスの単体テスト

Jersey TestでJAX-RSリソースクラスの単体テストするときに、Mockitoでモックのビジネスロジックを差し込みます。

環境

  • Payara Web ML 4.1.1.161
  • Jersey 2.22.1

pom.xml

  • JUnit
  • Jersey Test
  • Jersey Testの実行環境
  • Mockito

が必要です。

jersey-test-framework-provider-grizzly2を依存性に含めると、Jersey Testと実行環境が両方入ります。

    <properties>
        <jersey.version>2.22.1</jersey.version>
        <jee-webapi.version>7.0</jee-webapi.version>
    </properties>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.glassfish.jersey</groupId>
                <artifactId>jersey-bom</artifactId>
                <version>${jersey.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <dependency>
            <groupId>org.glassfish.jersey.media</groupId>
            <artifactId>jersey-media-json-jackson</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>javax</groupId>
            <artifactId>javaee-web-api</artifactId>
            <version>${jee-webapi.version}</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.glassfish.jersey.test-framework.providers</groupId>
            <artifactId>jersey-test-framework-provider-grizzly2</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.mockito</groupId>
            <artifactId>mockito-core</artifactId>
            <version>1.10.19</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

テスト対象のリソースクラスなど

package sample;

import javax.ws.rs.ApplicationPath;
import javax.ws.rs.core.Application;

@ApplicationPath("/api")
public class MyApplication extends Application {
     // 中身は空
}
package sample;

import java.util.List;
import javax.enterprise.context.RequestScoped;
import javax.inject.Inject;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.MediaType;

@Path("/hello")
@Produces(value = MediaType.APPLICATION_JSON)
@RequestScoped
public class HelloResource {

    // テスト時はここをモックに差し替える
    @Inject
    private HelloLogic helloLogic;

    @GET
    public Response getAll() throws Exception {
        List<HelloDto> list = helloLogic.selectAll();
        return Response.ok(list).build();
    }
}
package sample;

public class HelloDto {
    private String name;
    // setter/getter/コンストラクタ省略
}
package sample;

import java.util.List;
import javax.enterprise.context.RequestScoped;

@RequestScoped
public class HelloLogic {
    public List<HelloDto> selectAll() {
        // 本来は何らかの複雑な処理があると考えてください
        return Arrays.asList(new HelloDto("AAA"), new HelloDto("BBB"), new HelloDto("CCC"));
    }
}

テストクラスの作成

package sample;

import org.glassfish.hk2.api.Factory;
import org.glassfish.hk2.utilities.Binder;
import org.glassfish.hk2.utilities.binding.AbstractBinder;
import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.test.JerseyTest;
import org.junit.Test;

import javax.ws.rs.core.Application;
import javax.ws.rs.core.GenericType;
import java.util.Arrays;
import java.util.List;

import static org.junit.Assert.*;
import static org.hamcrest.CoreMatchers.*;
import static org.mockito.Mockito.*;

public class HelloResourceTest extends JerseyTest {

    /**
      * HelloLogicのモックのファクトリークラス
      */
    private static class MockHelloLogicFactory implements Factory<HelloLogic> {
        @Override
        public HelloLogic provide() {
            // mock() / when() / thenReturn() はMockitoのメソッド
            HelloLogic mockLogic = mock(HelloLogic.class);
            when(mockLogic.selectAll()).thenReturn(Arrays.asList(
                    new HelloDto("仮のA"),
                    new HelloDto("仮のB")
            ));
            return mockLogic;
        }

        @Override
        public void dispose(ProductQueryDao instance) {
            // 何もしない
        }
    }

    @Override
    protected Application configure() {
        Binder binder = new AbstractBinder() {
            @Override
            protected void configure() {
                // bindFactory() + to() でモックに差し替え
                bindFactory(MockHelloLogicFactory.class)
                        .to(HelloLogic.class);
            }
        };
        return new ResourceConfig()
                .packages(true, MyApplication.class.getPackage().getName())
                .register(binder);
    }

    @Test
    public void 全件取得でき件数が2件() {
        List<HelloDto> list = target("hello")
                .request()
                .get(new GenericType<List<HelloDto>>(){});
        assertThat(list.size(), is(2));
    }
}

ポイントは、ファクトリークラスを作成することと、bindFactory()メソッドを利用すること。

下記の@backpaper0さんの資料にあるbind()メソッドは、Javadoc

Does NOT bind the service type itself as a contract type.

とあり、Mockitoで作ったmockLogicを引数に指定すると、例外でテストがこけた。

bind()の引数はインタフェースのみ指定可能なのかも?

参考資料

Mockito 初めの一歩 - Qiita

java - Mocking EJB's with Mockito and Jersey Test Framework - Stack Overflow

http://backpaper0.github.io/ghosts/jaxrs-getting-started-and-practice.html#/12/8

AbstractBinder (HK2 API module 2.2.0-b11-SNAPSHOT API)

Factory (HK2 API module 2.2.0-b11-SNAPSHOT API)