1. 序章

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

  • 偽造(または MockUp API)
  • Deencapsulationユーティリティクラス
  • 1つのモックのみを使用して複数のインターフェイスをモックする方法
  • 期待と検証を再利用する方法

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

2. Mavenの依存関係

まず、jmockit依存関係をプロジェクトに追加する必要があります。

<dependency> 
    <groupId>org.jmockit</groupId> 
    <artifactId>jmockit</artifactId> 
    <version>1.41</version>
</dependency>

次に、例を続けます。

3. プライベートメソッド/内部クラスのモッキング

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

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

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 {...}
}

3.1. モックアップで偽造

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

MockupのAPIを使用してprivateMethod()を再定義する方法を見てみましょう。

@RunWith(JMockit.class)
public class AdvancedCollaboratorTest {

    @Tested
    private AdvancedCollaborator mock;

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

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

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

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

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

3.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 アノテーションを使用し、セッターを持たないクラスの依存関係を設定したり、コンテナークラスのパブリックインターフェイスに依存せずに内部クラスを単体テストしたりできます。

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

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

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

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

たとえば、インターフェースリスト比較可能の2つの方法のモックを作成します。

@RunWith(JMockit.class)
public class AdvancedCollaboratorTest<MultiMock
  extends List<String> & Comparable<List<String>>> {
    
    @Mocked
    private MultiMock multiMock;
    
    @Test
    public void testOnClass() {
        new Expectations() {{
            multiMock.get(5); result = "foo";
            multiMock.compareTo((List<String>) any); result = 0;
        }};
        assertEquals("foo", multiMock.get(5));
        assertEquals(0, multiMock.compareTo(new ArrayList<>()));
    }

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

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

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

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

5. 期待と検証の再利用

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

例で説明します( JMockit 101の記事のModel、Collaborator 、および Performer のクラスを使用しています):

@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”とを返すようにすべてのテストの期待値を準備していることがわかります。 X174X] collaborator.collaborate ()は、常に“ foo” を引数として期待し、trueを返します。 minTimes = 0 ステートメントを配置して、テストで実際に使用していないときに失敗が表示されないようにします。

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

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

6. 結論

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

JMockitについてはさらに多くの記事を掲載する可能性がありますので、引き続きご期待ください。

そして、いつものように、このチュートリアルの完全な実装は、GitHubのにあります。

6.1. シリーズの記事

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