1. 概要

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

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

2. Mavenの依存関係

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

<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-engine</artifactId>
    <version>5.8.1</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>

これらのライブラリの最新バージョンは、 MavenCentralにあります。

PowermockはJunit5では完全にはサポートされていません。 また、 powermock-module-junit4 は、セクション5に示されている1つの例にのみ使用されます。

3. 独立した非抽象的方法

publicnon-abstractメソッドを持つ抽象クラスがある場合を考えてみましょう。

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を使用することさえできません。

代わりに、 PowerMock n oteを使用する必要があります。この依存関係は、JUnit 5 ではサポートされていないため、この例はJUnit4でのみ機能します)。

@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() 、の値を変更できますカウントを使用して作成されたインスタンスにアクセスしてフィールドモッキト。 

一方、プライベート 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クラスを使用してオブジェクトの内部状態を制御しています。

アクティブフィールドの値がtrueに変更されます。

7. 結論

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

また、抽象クラスメソッドの単体テストを作成することは、通常のクラスやメソッドと同じくらい重要です。 さまざまな手法または利用可能なさまざまなテストサポートライブラリを使用して、それぞれをテストできます。

完全なソースコードは、GitHubから入手できます。