JMockitの高度な使い方

JMockitの高度な使用法

1. 前書き

この記事では、JMockitの基本を超えて、次のようないくつかの高度なシナリオを見ていきます。

  • 偽造(またはMockUp API)

  • Deencapsulationユーティリティクラス

  • 1つのモックのみを使用して複数のインターフェイスをモックする方法

  • 期待と検証を再利用する方法

JMockitの基本を知りたい場合は、このシリーズの他の記事を確認してください。 関連するリンクはページの下部にあります。

2. プライベートメソッド/内部クラスのモック

プライベートメソッドまたは内部クラスのモックとテストは、多くの場合、グッドプラクティスとは見なされません。

その背後にある理由は、それらがプライベートである場合、それらはクラスの最も内側の内臓であるため、直接テストするべきではないということですが、特にレガシーコードを扱う場合は、それでもテストする必要があります。

JMockitには、これらを処理する2つのオプションがあります。

  • 実際の実装を変更するためのMockUp API(2番目の場合)

  • Deencapsulationユーティリティクラス。任意のメソッドを直接呼び出す(最初の場合)

以下のすべての例は、次のクラスに対して実行され、最初のクラスと同じ構成のテストクラスで実行されると想定します(コードの繰り返しを避けるため)。

public class AdvancedCollaborator {
    int i;
    private int privateField = 5;

    // default constructor omitted

    public AdvancedCollaborator(String string) throws Exception{
        i = string.length();
    }

    public String methodThatCallsPrivateMethod(int i) {
        return privateMethod() + i;
    }
    public int methodThatReturnsThePrivateField() {
        return privateField;
    }
    private String privateMethod() {
        return "default:";
    }

    class InnerAdvancedCollaborator {...}
}

2.1. MockUpによる偽造

JMockitのMockupAPIは、偽の実装またはmock-upsの作成をサポートします。 通常、mock-upは、偽造されるクラス内のいくつかのメソッドやコンストラクターを対象とし、他のほとんどのメソッドやコンストラクターは変更されません。 これにより、クラスを完全に書き直すことができるため、任意のメソッドまたはコンストラクター(アクセス修飾子付き)をターゲットにできます。

モックアップのAPIを使用してprivateMethod()を再定義する方法を見てみましょう。

@RunWith(JMockit.class)
public class AdvancedCollaboratorTest {

    @Tested
    private AdvancedCollaborator mock;

    @Test
    public void testToMockUpPrivateMethod() {
        new MockUp() {
            @Mock
            private String privateMethod() {
                return "mocked: ";
            }
        };
        String res = mock.methodThatCallsPrivateMethod(1);
        assertEquals("mocked: 1", res);
    }
}

この例では、署名が一致するメソッドの@Mockアノテーションを使用して、AdvancedCollaboratorクラスの新しいMockUpを定義しています。 この後、そのメソッドの呼び出しは、モックされたメソッドに委任されます。

これを使用して、テストを簡素化するために特定の引数または構成を必要とするクラスのコンストラクターをmock-upすることもできます。

@Test
public void testToMockUpDifficultConstructor() throws Exception{
    new MockUp() {
        @Mock
        public void $init(Invocation invocation, String string) {
            ((AdvancedCollaborator)invocation.getInvokedInstance()).i = 1;
        }
    };
    AdvancedCollaborator coll = new AdvancedCollaborator(null);
    assertEquals(1, coll.i);
}

この例では、コンストラクターのモックを作成するには、$initメソッドをモックする必要があることがわかります。 呼び出しが実行されているインスタンスを含む、モックされたメソッドの呼び出しに関する情報にアクセスできるタイプInvocation,の追加の引数を渡すことができます。

2.2. Deencapsulationクラスの使用

JMockitには、テストユーティリティクラスDeencapsulationが含まれています。 その名前が示すように、オブジェクトの状態をカプセル化解除するために使用され、それを使用すると、他の方法ではアクセスできなかったフィールドやメソッドにアクセスすることで、テストを簡素化できます。

メソッドを呼び出すことができます:

@Test
public void testToCallPrivateMethodsDirectly(){
    Object value = Deencapsulation.invoke(mock, "privateMethod");
    assertEquals("default:", value);
}

フィールドを設定することもできます:

@Test
public void testToSetPrivateFieldDirectly(){
    Deencapsulation.setField(mock, "privateField", 10);
    assertEquals(10, mock.methodThatReturnsThePrivateField());
}

フィールドを取得します。

@Test
public void testToGetPrivateFieldDirectly(){
    int value = Deencapsulation.getField(mock, "privateField");
    assertEquals(5, value);
}

そして、クラスの新しいインスタンスを作成します。

@Test
public void testToCreateNewInstanceDirectly(){
    AdvancedCollaborator coll = Deencapsulation
      .newInstance(AdvancedCollaborator.class, "foo");
    assertEquals(3, coll.i);
}

内部クラスの新しいインスタンスでも:

@Test
public void testToCreateNewInnerClassInstanceDirectly(){
    InnerCollaborator inner = Deencapsulation
      .newInnerInstance(InnerCollaborator.class, mock);
    assertNotNull(inner);
}

ご覧のとおり、Deencapsulationクラスは、気密クラスをテストするときに非常に役立ちます。 1つの例として、プライベートフィールドで@Autowiredアノテーションを使用し、それらのセッターを持たないクラスの依存関係を設定したり、コンテナークラスのパブリックインターフェイスに依存せずに内部クラスを単体テストしたりできます。

3. 1つの同じモックで複数のインターフェースをモックする

まだ実装されていないクラスをテストしたいが、それがいくつかのインターフェースを実装することは確かだと仮定しましょう。

通常、実装する前に上記のクラスをテストすることはできませんが、JMockitを使用すると、1つのモックオブジェクトを使用して複数のインターフェースをモックすることにより、事前にテストを準備できます。

これは、ジェネリックを使用して、複数のインターフェイスを拡張する型を定義することで実現できます。 このジェネリック型は、テストクラス全体または1つのテストメソッドのみに対して定義できます。

たとえば、インターフェースListComparableの2つの方法:のモックを作成します。

@RunWith(JMockit.class)
public class AdvancedCollaboratorTest & Comparable>> {

    @Mocked
    private MultiMock multiMock;

    @Test
    public void testOnClass() {
        new Expectations() {{
            multiMock.get(5); result = "foo";
            multiMock.compareTo((List) any); result = 0;
        }};
        assertEquals("foo", multiMock.get(5));
        assertEquals(0, multiMock.compareTo(new ArrayList<>()));
    }

    @Test
    public  & Comparable>>
      void testOnMethod(@Mocked M mock) {
        new Expectations() {{
            mock.get(5); result = "foo";
            mock.compareTo((List) any); result = 0;
        }};
        assertEquals("foo", mock.get(5));
        assertEquals(0, mock.compareTo(new ArrayList<>()));
    }
}

2行目でわかるように、クラス名にジェネリックを使用して、テスト全体の新しいテストタイプを定義できます。 そうすれば、MultiMockがタイプとして使用可能になり、JMockitの任意のアノテーションを使用してそのモックを作成できるようになります。

7から18行目では、テストクラス全体に対して定義されたマルチクラスのモックを使用した例を見ることができます。

1つのテストだけでマルチインターフェイスモックが必要な場合は、メソッドシグネチャでジェネリック型を定義し、その新しいジェネリックの新しいモックをテストメソッドの引数として渡すことでこれを実現できます。 20行目から32行目では、前のテストと同じテスト済みの動作に対してこれを行う例を見ることができます。

4. 期待と検証の再利用

結局、クラスをテストするときに、同じExpectationsVerificationsを何度も繰り返す場合があります。 それを容易にするために、両方を簡単に再利用できます。

例で説明します(JMockit 101の記事のクラスModel, CollaboratorPerformerを使用しています):

@RunWith(JMockit.class)
public class ReusingTest {

    @Injectable
    private Collaborator collaborator;

    @Mocked
    private Model model;

    @Tested
    private Performer performer;

    @Before
    public void setup(){
        new Expectations(){{
           model.getInfo(); result = "foo"; minTimes = 0;
           collaborator.collaborate("foo"); result = true; minTimes = 0;
        }};
    }

    @Test
    public void testWithSetup() {
        performer.perform(model);
        verifyTrueCalls(1);
    }

    protected void verifyTrueCalls(int calls){
        new Verifications(){{
           collaborator.receive(true); times = calls;
        }};
    }

    final class TrueCallsVerification extends Verifications{
        public TrueCallsVerification(int calls){
            collaborator.receive(true); times = calls;
        }
    }

    @Test
    public void testWithFinalClass() {
        performer.perform(model);
        new TrueCallsVerification(1);
    }
}

この例では、15行目から18行目で、model.getInfo()が常に“foo”を返し、collaborator.collaborate()が常に“foo”を期待するように、すべてのテストの期待値を準備していることがわかります。 )sを引数として返し、trueを返します。 minTimes = 0ステートメントを配置して、実際にテストで使用していないときに失敗が表示されないようにします。

また、渡された引数がtrueの場合に、collaborator.receive(boolean)メソッドの検証を簡略化するためにメソッドverifyTrueCalls(int)を作成しました。

最後に、ExpectationsまたはVerificationsクラスのいずれかを拡張するだけで、新しいタイプの特定の期待値と検証を作成することもできます。 次に、33〜43行で行うように、動作を構成し、テストで上記の型の新しいインスタンスを作成する必要がある場合は、コンストラクターを定義します。

5. 結論

JMockitシリーズの今回の記事では、日常のモックやテストで間違いなく役立ついくつかの高度なトピックに触れました。

JMockitでさらに記事を作成する場合がありますので、今後も学習してください。

そして、いつものように、このチュートリアルの完全な実装はover on GitHub.で見つけることができます

5.1. シリーズの記事

シリーズのすべての記事: