JUnitで抽象クラスをテストする

1概要

このチュートリアルでは、非抽象メソッドによる抽象クラスの単体テストに対するさまざまなユースケースと可能な代替ソリューションを分析します。

  • 抽象クラスのテストは、ほとんどの場合、具象実装のパブリックAPIを使用する必要があります。したがって、自分が何をしているのかわからない限り、以下の手法を適用しないでください。

2 Mavenの依存関係

Mavenの依存関係から始めましょう。

<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-engine</artifactId>
    <version>5.1.0</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-core</artifactId>
    <version>2.8.9</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.powermock</groupId>
    <artifactId>powermock-module-junit4</artifactId>
    <version>1.7.4</version>
    <scope>test</scope>
    <exclusions>
        <exclusion>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>org.powermock</groupId>
    <artifactId>powermock-api-mockito2</artifactId>
    <version>1.7.4</version>
    <scope>test</scope>
</dependency>

これらのライブラリの最新バージョンはhttps://search.maven.org/classic/#search%7Cga%7C1%7C(g%3A%22org.junit.jupiter%22%20AND%20a%3A%22junitで見つけることができます。 -jupiter-engine%22)%20OR%20(g%3A%22org.mockito%22%20AND%20a%3A%22mockito-core%22)%20OR%20(g%3A%22org.powermock%22%20AND %20a%3A%22 powermock-module-junit 4%22)%20OR%20(g%3A%22org.powermock%22%20AND%20a%3A%22powermock-api-mockito2%22)[Maven Central]。

PowermockはJunit 5では完全にはサポートされていません。

また、 powermock-module-junit4 は、セクション5に示す1つの例にのみ使用されています。

3独立した非抽象的手法

パブリック非抽象メソッドを持つ抽象クラスがある場合を考えましょう。

public abstract class AbstractIndependent {
    public abstract int abstractFunc();

    public String defaultImpl() {
        return "DEFAULT-1";
    }
}

メソッド defaultImpl() をテストしたいのですが、具体的なクラスを使う方法とMockitoを使う方法の2つの方法があります。

3.1. 具象クラスの使用

__AbstractIndependent __classを拡張する具象クラスを作成し、それを使用してメソッドをテストします。

public class ConcreteImpl extends AbstractIndependent {

    @Override
    public int abstractFunc() {
        return 4;
    }
}
@Test
public void givenNonAbstractMethod__whenConcreteImpl__testCorrectBehaviour() {
    ConcreteImpl conClass = new ConcreteImpl();
    String actual = conClass.defaultImpl();

    assertEquals("DEFAULT-1", actual);
}

この解決方法の欠点は、すべての抽象メソッドをダミーで実装した具象クラスを作成する必要があることです。

3.2. Mockitoを使う

あるいは、 __Mockito __を使ってモックを作成することもできます。

@Test
public void givenNonAbstractMethod__whenMockitoMock__testCorrectBehaviour() {
    AbstractIndependent absCls = Mockito.mock(
      AbstractIndependent.class,
      Mockito.CALLS__REAL__METHODS);

    assertEquals("DEFAULT-1", absCls.defaultImpl());
}

ここで最も重要な部分は、 Mockito.CALLS REAL METHODS を使用してメソッドが呼び出されたときに実際のコードを使用するためのモックの準備です。

4非抽象メソッドから呼び出された抽象メソッド

この場合、非抽象メソッドはグローバル実行フローを定義しますが、抽象メソッドはユースケースに応じてさまざまな方法で書くことができます。

public abstract class AbstractMethodCalling {

    public abstract String abstractFunc();

    public String defaultImpl() {
        String res = abstractFunc();
        return (res == null) ? "Default" : (res + " Default");
    }
}

このコードをテストするには、以前と同じ2つの方法を使用できます。具体的なクラスを作成するか、Mockitoを使用してモックを作成します。

@Test
public void givenDefaultImpl__whenMockAbstractFunc__thenExpectedBehaviour() {
    AbstractMethodCalling cls = Mockito.mock(AbstractMethodCalling.class);
    Mockito.when(cls.abstractFunc())
      .thenReturn("Abstract");
    Mockito.doCallRealMethod()
      .when(cls)
      .defaultImpl();

    assertEquals("Abstract Default", cls.defaultImpl());
}

ここでは、 abstractFunc() にテストに適した戻り値をスタブしています。つまり、非抽象メソッド defaultImpl() を呼び出すと、このスタブが使用されます。

5テスト妨害を伴う非抽象的手法

いくつかのシナリオでは、テストしたいメソッドがテストの妨害を含むプライベートメソッドを呼び出します。

ターゲットメソッドをテストする前に、妨害するテストメソッドをバイパスする必要があります。

public abstract class AbstractPrivateMethods {

    public abstract int abstractFunc();

    public String defaultImpl() {
        return getCurrentDateTime() + "DEFAULT-1";
    }

    private String getCurrentDateTime() {
        return LocalDateTime.now().toString();
    }
}

この例では、 defaultImpl() メソッドがプライベートメソッド getCurrentDateTime() を呼び出します。このプライベートメソッドは実行時に現在時刻を取得します。これは私たちのユニットテストでは避けるべきです。

さて、このプライベートメソッドの標準的な振る舞いを偽造するために、 Mockito はプライベートメソッドを制御できないので使用することすらできません。

代わりに、https://www.baeldung.com/intro-to-powermock[PowerMock]を使用する必要があります( この依存関係のサポートはJUnit 5 では使用できないため、この例はJUnit 4でのみ機能することに注意してください)。

@RunWith(PowerMockRunner.class)
@PrepareForTest(AbstractPrivateMethods.class)
public class AbstractPrivateMethodsUnitTest {

    @Test
    public void whenMockPrivateMethod__thenVerifyBehaviour() {
        AbstractPrivateMethods mockClass = PowerMockito.mock(AbstractPrivateMethods.class);
        PowerMockito.doCallRealMethod()
          .when(mockClass)
          .defaultImpl();
        String dateTime = LocalDateTime.now().toString();
        PowerMockito.doReturn(dateTime).when(mockClass, "getCurrentDateTime");
        String actual = mockClass.defaultImpl();

        assertEquals(dateTime + "DEFAULT-1", actual);
    }
}

この例で重要なビット:

  • __ @ RunWithはPowerMockをテストのランナーとして定義します

  • __ @ PrepareForTest(class) __クラスを準備するPowerMockを終了します。

後の処理

興味深いことに、 PowerMock にプライベートメソッド __getCurrentDateTime()をスタブするように求めています。 __PowerMockはそれを見つけるためにリフレクションを使用します。外部からはアクセスできないからです。

したがって、 defaultImpl() を呼び出すと、実際のメソッドの代わりにプライベートメソッド用に作成されたスタブが呼び出されます。

** 6. インスタンスフィールドにアクセスする非抽象メソッド

抽象クラスはクラスフィールドで実装された内部状態を持つことができます。フィールドの値は、テストされるメソッドに大きな影響を与える可能性があります。

フィールドがパブリックまたは保護されている場合は、テストメソッドから簡単にアクセスできます。

それが非公開の場合は、 PowerMockito を使用する必要があります。

public abstract class AbstractInstanceFields {
    protected int count;
    private boolean active = false;

    public abstract int abstractFunc();

    public String testFunc() {
        if (count > 5) {
            return "Overflow";
        }
        return active ? "Added" : "Blocked";
    }
}

ここで、 testFunc() メソッドは、返される前にインスタンスレベルフィールド count および __active __を使用しています。

testFunc() をテストするとき、 Mockitoを使用して作成されたインスタンスにアクセスすることによって count フィールドの値を変更できます。

一方、プライベートの active フィールドを使用して動作をテストするには、もう一度 PowerMockito とその Whitebox クラスを使用する必要があります。

@Test
public void whenPowerMockitoAndActiveFieldTrue__thenCorrectBehaviour() {
    AbstractInstanceFields instClass = PowerMockito.mock(AbstractInstanceFields.class);
    PowerMockito.doCallRealMethod()
      .when(instClass)
      .testFunc();
    Whitebox.setInternalState(instClass, "active", true);

    assertEquals("Added", instClass.testFunc());
}

PowerMockito.mock() を使用してスタブクラスを作成し、オブジェクトの内部状態を制御するために Whitebox クラスを使用します。

__active フィールドの値が true__に変更されます。

7. 結論

このチュートリアルでは、多くのユースケースをカバーする複数の例を見ました。従うべき設計に応じて、もっと多くのシナリオで抽象クラスを使うことができます。

また、抽象クラスメソッドの単体テストを書くことは、通常のクラスやメソッドと同じくらい重要です。私たちは利用可能な異なるテクニックや異なるテストサポートライブラリを使ってそれらのそれぞれをテストすることができます。

完全なソースコードはhttps://github.com/eugenp/tutorials/tree/master/testing-modules/junit-5[over on GitHub]から入手できます。