業務ではJavaを使用した開発が多いが、単体テストを書いていてMockやStubといったものを使用したことがないことに気がついたため、その使用例をまとめてみる。


Mock, Stubとは

厳密な定義はわからないが、こちらの記事がとても参考に。相互作用に着目するテストで使いたくなるのがMock、状態に着目するテストで欲しくなるのがStubということみたい。

この記事のタイトル “Mocks Aren’t Stubs”にもかかわらず、モックとスタブの違いは本当は一番の問題ではない。最も興味深いのは、相互作用スタイル対状態スタイルというところだ。相互作用中心のテストを行うテスターは全てのサブオブジェクトについてモックを作る。状態中心のテスターは、実際のオブジェクトを使うのが現実的でないものについてのみスタブを作る。例えば、外部サービスやコストのかかるもの、状態中心のやり方では扱いにくいキャッシュのようなものだ。

JUnitでMockitoを使用したテスト

JUnitをMock、Stub作成のためのライブラリであるMockitoと組み合わせて使用してみた。参考にしたのはこちらこちら。使用例は上の記事に合わせ、倉庫からの商品の引当ロジック(注文に対して在庫が充分に存在するかを判断)のテストとした。

状態中心のテスト(Stub使用)

public class OrderStateTest {

    private static final String TALISKER = "tarisker";

    @Test
    public void testOrderIsFilledIfEnoughInWarehouse() throws Exception {
        // setup
        Warehouse warehouse = mock(Warehouse.class);
        when(warehouse.getInventory(TALISKER)).thenReturn(50);
        Order order = new Order(TALISKER, 20);
        // execute
        order.fill(warehouse);
        // verify
        assertThat(order.isFilled(), is(true));
    }

    @Test
    public void testOrderDoesNotRemoveIfNotEnoughInWarehouse() throws Exception {
        // setup
        Warehouse warehouse = mock(Warehouse.class);
        when(warehouse.getInventory(TALISKER)).thenReturn(10);
        Order order = new Order(TALISKER, 20);
        // execute
        order.fill(warehouse);
        // verify
        assertThat(order.isFilled(), is(false));
    }
}

相互作用中心のテスト(Mock使用)

public class OrderInteractionTest {

    private static final String TALISKER = "tarisker";

    @Test
    public void testFillingRemovesInventoryIfInStock() throws Exception {
        // setup
        Order order = new Order(TALISKER, 20);
        Warehouse warehouse = mock(Warehouse.class);
        when(warehouse.getInventory(TALISKER)).thenReturn(50);
        // execute
        order.fill(warehouse);
        // verify
        assertThat(order.isFilled(), is(true));
        verify(warehouse, times(1)).getInventory(TALISKER);
        verify(warehouse, times(1)).remove(TALISKER, 20);
    }

    @Test
    public void testFillingDoesNotRemoveIfNotEnoughInStock() throws Exception {
        // setup
        Order order = new Order(TALISKER, 20);
        Warehouse warehouse = mock(Warehouse.class);
        when(warehouse.getInventory(TALISKER)).thenReturn(10);
        // execute
        order.fill(warehouse);
        // verify
        assertThat(order.isFilled(), is(false));
        verify(warehouse, times(1)).getInventory(TALISKER);
        verify(warehouse, never()).remove(eq(TALISKER), anyInt());
    }
}
  • 状態に着目したテストはよく見る感じ。
  • 相互作用に着目したテストは、例がシンプルすぎたせいか少し良さを感じ辛かった。実際の使用例を見てみたい。例えばVisitorパターンを実装したクラスで、もしvisit順も知りたいとかいう場合は、Mockとしてテストすると簡単そう?
  • また、相互作用に着目したテストは内部実装に依存したテストとなる。

Spring FrameworkとMockを使用したテスト

個人的にJavaを使用した開発ではフレームワークとしてSpringを使用することが多いため、SpringにおけるMock利用についても確認した。やりたいのは「Mockオブジェクトを依存性注入」。

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
public class MockExampleTest {

     @Configuration
     @ComponentScan
     public static class MockExampleConfig {
         // MockitoによるMock化
         // このようにmethodではなくannotationで設定することもできる
         @Mock
         private Warehouse warehouse;

         public MockExampleConfig() {
             MockitoAnnotations.initMocks(this);
         }

         // Mock化されたBean
         @Bean
         public Warehouse warehouse() {
             return warehouse;
         }

         // MockオブジェクトがセットされたBean
         @Bean
         public WarehouseUtilizer warehouseUtilizer() {
             WarehouseUtilizer utilizer = new WarehouseUtilizer();
             utilizer.setWarehouse(warehouse());
             return utilizer;
         }
    }

    private static final String TALISKER = "tarisker";
    @Autowired Warehouse warehouse;
    @Autowired WarehouseUtilizer utilizer;
    @Autowired WarehouseAutowired autowired;

    // Mockオブジェクトによる引当成功時のテスト
    @Test
    public void testOrderIsFilledIfEnoughInWarehouse() throws Exception {
        // setup
        when(warehouse.getInventory(TALISKER)).thenReturn(50);
        Order order = new Order(TALISKER, 20);
        // execute
        order.fill(warehouse);
        // verify
        assertThat(order.isFilled(), is(true));
    }

    // Mockオブジェクトによる引当失敗時のテスト
    @Test
    public void testOrderDoesNotRemoveIfNotEnoughInWarehouse() throws Exception {
        // setup
        when(warehouse.getInventory(TALISKER)).thenReturn(10);
        Order order = new Order(TALISKER, 20);
        // execute
        order.fill(warehouse);
        // verify
        assertThat(order.isFilled(), is(false));
    }

    // ConfigurationでMockオブジェクトがセットされたBean
    @Test
    public void testUtilizer() throws Exception {
        when(warehouse.getInventory(TALISKER)).thenReturn(10);
        assertThat(utilizer.getInventory(TALISKER), is(10));
    }

    // MockオブジェクトがAutowiredされたBean
    @Test
    public void testAutowired() throws Exception {
        when(warehouse.getInventory(TALISKER)).thenReturn(10);
        assertThat(autowired.getInventory(TALISKER), is(10));
    }
}
  • Configurationにconstructorを明示的に設定するという少しトリッキーな方法。だけど調べた感じ一番シンプルにやりたいことができている。
  • もちろん他のBeanへのAutowired等も可能。
  • この方法が使えるのはinterfaceに対してだけという記述もあったが、試す限りでは通常のクラスに対しても使用できている。

Apache CamelとMockを使用したテスト

あまりメジャーではないかもだけど、データ連携用のJavaフレームワークとしてApache Camelというものがあり、たまに使っている。「置かれたテキストファイルをガーッと読み込み、処理して、別の形式、プロトコルで他に投げる」とかそういうことが簡単に書ける。Apache Camelの概要は日本語の記事だとこちらがわかりやすい。ここではApache CamelでのテストにおけるMock使用法について確認した。やりたいことは「BeanコンポーネントのMock化」

@RunWith(CamelCdiRunner.class)
@Beans(alternatives=MockBodySetterBean.class)
public class MockRouteTest {

    @EndpointInject(uri="mock:result")
    protected MockEndpoint resultEndpoint;
    @Produce(uri="direct:start")
    protected ProducerTemplate producerTemplate;

    @Named("bodySetterBean")
    public static class BodySetterBean {
        public String set() {
            return "ThisIsActualBean";
        }
    }
    @Alternative
    @Named("bodySetterBean")
    public static class MockBodySetterBean {
        public String set() {
            return "ThisIsMockBean";
        }
    }

    @Test
    public void testMockBean() throws Exception {
        // expect
        resultEndpoint.expectedBodiesReceived("ThisIsMockBean");
        // execute
        producerTemplate.sendBody("start");
        // verify
        resultEndpoint.assertIsSatisfied();
    }

    public static class MockRouteConfig extends RouteBuilder {
        @Override
        public void configure() throws Exception {
            from("direct:start")
            .bean("bodySetterBean")                            // Call bean by String
            //.bean(BodySetterBean.class)                      // Call bean by Class -> Cannot be mocked
            //.transform().simple("${bean:bodySetterBean}")    // Call bean by simple expression
            .to("mock:result")
            ;
        }
    }
}
  • 最近作られたCDI Testingの仕組みに則って記述。
  • BeanコンポーネントのMock化は結局何らかのMock用のクラスを自作するしかなさそう。
  • ちなみにRoute自体のMock化ならこちら。もしくはSpring Frameworkと組み合わせてよければ、@MockEndpointsAndSkip@EndpointInjectの合わせ技で書くとすごいラク。
@RunWith(CamelSpringJUnit4ClassRunner.class)
@ContextConfiguration(
    loader = CamelSpringDelegatingTestContextLoader.class
)
//@MockEndpoints("direct:next|direct:prev")      // 正規表現でMock化したいRouteの指定。このRouteは実行される。
@MockEndpointsAndSkip("direct:next|direct:prev") // この場合実行されない。いわばStub的。
public class MockSpringTest {

    @Produce(uri="direct:start")
    ProducerTemplate producerTemplate;
    @EndpointInject(uri="mock:direct:next")
    MockEndpoint mockNextEndpoint;
    @EndpointInject(uri="mock:direct:prev")
    MockEndpoint mockPrevEndpoint;

    ...

}

使用したサンプルのリポジトリ