Spockフレームワークでのスタブ、モック、スパイの違い

  • link:/category/testing/ [テスト]

  • Groovy

1. 概要

このチュートリアルでは、* Spockフレームワークの_Mock _、_ Stub_、および_Spy_の違いについて説明します*。 相互作用ベースのテストに関して、フレームワークが提供するものを説明します。
_Spock_は、_Java_および_Groovy_のテストフレームワークであり、ソフトウェアアプリケーションの手動テストのプロセスを自動化するのに役立ちます。 独自のモック、スタブ、スパイを導入し、通常は追加のライブラリを必要とするテスト用の組み込み機能を備えています。
最初に、スタブをいつ使用するかを説明します。 次に、モックを行います。 最後に、最近導入された_Spy_について説明します。

2. Mavenの依存関係

始める前に、https://search.maven.org/classic/#search%7Cga%7C1%7C%20(g%3A%22org.spockframework%22%20AND%20a%3A%22spock-core%を追加しましょう22)%20OR%20(g%3A%22org.codehaus.groovy%22%20AND%20a%3A%22groovy-all%22)[Maven依存関係]:
<dependency>
    <groupId>org.spockframework</groupId>
    <artifactId>spock-core</artifactId>
    <version>1.3-RC1-groovy-2.5</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.codehaus.groovy</groupId>
    <artifactId>groovy-all</artifactId>
    <version>2.4.7</version>
    <scope>test</scope>
</dependency>
Spockの__1.3-RC1-groovy-2.5 __versionが必要になることに注意してください。 _Spy_は、Spock Frameworkの次の安定バージョンで導入されます。 *現在、_Spy_はバージョン1.3の最初のリリース候補版で利用可能です。*
Spockテストの基本構造の要約については、https://www.baeldung.com/groovy-spock [GroovyとSpockを使用したテストに関する入門記事]をご覧ください。

3. 相互作用ベースのテスト

*インタラクションベースのテストは、オブジェクトの動作をテストするのに役立つ手法です。具体的には、オブジェクト同士の相互作用をテストします。 このために、モックとスタブと呼ばれるダミー実装を使用できます。
もちろん、モックとスタブの独自の実装を非常に簡単に書くことができます。 問題は、実稼働コードの量が増えると発生します。 このコードを手で書いて維持することは難しくなります。 これが、モックフレームワークを使用する理由です。モックフレームワークは、予想される相互作用を簡潔に説明する簡潔な方法を提供します。 * Spockには、モック、スタブ、スパイのサポートが組み込まれています。*
ほとんどのJavaライブラリと同様に、Spockはインターフェースのモックにlink:/java-dynamic-proxies[JDK動的プロキシ]を使用し、https://www.baeldung.com/byte-buddy [Byte Buddy]またはhttpsを使用します://www.baeldung.com/cglib [cglib]模擬クラスのプロキシ。 実行時にモック実装を作成します。
Javaには、クラスとインターフェイスをモックするためのさまざまな成熟したライブラリがすでに多数あります。 これらはそれぞれSpock __、__で使用できますが、Spockのモック、スタブ、スパイを使用する必要がある主な理由が1つあります。 これらのすべてをSpockに導入することで、* Groovyのすべての機能を活用して、テストを読みやすく、書きやすく、間違いなくもっと楽しくすることができます!

4. スタブメソッドの呼び出し

場合によっては、*ユニットテストで、クラスのダミーの動作を提供する必要があります*。 これは、外部サービスのクライアント、またはデータベースへのアクセスを提供するクラスの場合があります。 この手法はスタブとして知られています。
*スタブは、テスト済みコードの既存のクラス*依存関係を制御可能に置き換えます。 これは、特定の方法で応答するメソッド呼び出しを行うのに役立ちます。 スタブを使用する場合、メソッドが何回呼び出されるかは気にしません。 代わりに、このデータで呼び出されたときにこの値を返すように言いたいだけです。
ビジネスロジックを使用したサンプルコードに移りましょう。

4.1. テスト中のコード

_Item_というモデルクラスを作成しましょう。
public class Item {
    private final String id;
    private final String name;

    // standard constructor, getters, equals
}
アサーションを機能させるには、_equals(Object other)_メソッドをオーバーライドする必要があります。 *二重等号(==)を使用する場合、Spockはアサーション中に_equals_を使用します:*
new Item('1', 'name') == new Item('1', 'name')
それでは、1つのメソッドで_ItemProvider_インターフェースを作成しましょう。
public interface ItemProvider {
    List<Item> getItems(List<String> itemIds);
}
また、テストするクラスも必要です。 _ItemService:_の依存関係として__ItemProvider ___を追加します
public class ItemService {
    private final ItemProvider itemProvider;

    public ItemService(ItemProvider itemProvider) {
        this.itemProvider = itemProvider;
    }

    List<Item> getAllItemsSortedByName(List<String> itemIds) {
        List<Item> items = itemProvider.getItems(itemIds);
        return items.stream()
          .sorted(Comparator.comparing(Item::getName))
          .collect(Collectors.toList());
    }

}
*コードは特定の実装ではなく抽象に依存する必要があります。*そのため、インターフェイスを使用します。 これには多くの異なる実装があります。 たとえば、ファイルからアイテムを読み取ったり、外部サービスへのHTTPクライアントを作成したり、データベースからデータを読み取ったりできます。
このコードでは、__ getAllItemsSortedByName __method *に含まれるロジックのみをテストするため、外部依存関係をスタブする必要があります。

4.2. テスト対象のコードでスタブオブジェクトを使用する

_ItemProvider_依存関係の_Stub_を使用して、_setup()_メソッドで_ItemService_オブジェクトを初期化しましょう。
ItemProvider itemProvider
ItemService itemService

def setup() {
    itemProvider = Stub(ItemProvider)
    itemService = new ItemService(itemProvider)
}
ここで、特定の引数を指定して呼び出しごとに_itemProvider_がアイテムのリストを返すようにします*:
itemProvider.getItems(['offer-id', 'offer-id-2']) >>
  [new Item('offer-id-2', 'Zname'), new Item('offer-id', 'Aname')]
  • >>オペランドを使用してメソッドをスタブ化します。 _getItems_メソッドは、* _ * [âoff〜id ‘、â€〜offer-id-2’] * _ * list。*で呼び出されると、常に2つのアイテムのリストを返します。リストを作成します。

    テスト方法全体は次のとおりです。
def 'should return items sorted by name'() {
    given:
    def ids = ['offer-id', 'offer-id-2']
    itemProvider.getItems(ids) >> [new Item('offer-id-2', 'Zname'), new Item('offer-id', 'Aname')]

    when:
    List<Item> items = itemService.getAllItemsSortedByName(ids)

    then:
    items.collect { it.name } == ['Aname', 'Zname']
}
引数マッチング制約の使用、スタブ内の値のシーケンスの使用、特定の条件での異なる動作の定義、メソッド応答のチェーンなど、使用できるスタブ機能はさらに多くあります。

5. クラスメソッドのモック

次に、Spockのクラスまたはインターフェイスのモックについて説明します。
時々、依存オブジェクトのメソッドが指定された引数で呼び出されたかどうかを知りたい場合があります*。 オブジェクトの振る舞いに焦点を合わせ、メソッド呼び出しを調べることでオブジェクトがどのように相互作用するかを調べたいと思います。
以下で説明するサンプルコードで相互作用をテストします。

5.1. 相互作用を伴うコード

簡単な例として、アイテムをデータベースに保存します。 成功した後、システム内の新しいアイテムに関するイベントをメッセージブローカーに公開します。
サンプルのメッセージブローカーはRabbitMQまたはKafka__、__soです。一般的に、契約について説明します。
public interface EventPublisher {
    void publish(String addedOfferId);
}
このテストメソッドは、空でないアイテムをデータベースに保存し、イベントを公開します。 この例では、データベースにアイテムを保存することは関係ないため、コメントを追加します。
void saveItems(List<String> itemIds) {
    List<String> notEmptyOfferIds = itemIds.stream()
      .filter(itemId -> !itemId.isEmpty())
      .collect(Collectors.toList());

    // save in database

    notEmptyOfferIds.forEach(eventPublisher::publish);
}

5.2. モックされたオブジェクトとの相互作用の検証

それでは、コードで相互作用をテストしましょう。
まず、* __ setup()__method *で__EventPublisher __をモックする必要があります。 基本的に、新しいインスタンスフィールドを作成し、_Mock(Class)_ functionを使用してモックします。
class ItemServiceTest extends Specification {

    ItemProvider itemProvider
    ItemService itemService
    EventPublisher eventPublisher

    def setup() {
        itemProvider = Stub(ItemProvider)
        eventPublisher = Mock(EventPublisher)
        itemService = new ItemService(itemProvider, eventPublisher)
}
これで、テストメソッドを記述できます。 3つの文字列を渡します:â€、âa、a、âb
def 'should publish events about new non-empty saved offers'() {
    given:
    def offerIds = ['', 'a', 'b']

    when:
    itemService.saveItems(offerIds)

    then:
    1 * eventPublisher.publish('a')
    1 * eventPublisher.publish('b')
}
最後の_then_セクションのアサーションを詳しく見てみましょう。
1 * eventPublisher.publish('a')
__itemService __は、引数として 'a'を指定して_eventPublisher.publish(String)_を呼び出すことを想定しています。
スタブでは、引数の制約について説明しました。 モックにも同じルールが適用されます。 * _eventPublisher.publish(String)_がnullでも空でもない引数で2回呼び出されたことを確認できます。*
2 * eventPublisher.publish({ it != null && !it.isEmpty() })

5.3. モッキングとスタブの組み合わせ

_Spockでは、_ * a _Mock_は_Stub_と同じように動作します。
_ItemProvider_を__Mock(Class)__でオーバーライドして、新しい_ItemService_を作成しましょう。
given:
itemProvider = Mock(ItemProvider)
itemProvider.getItems(['item-id']) >> [new Item('item-id', 'name')]
itemService = new ItemService(itemProvider, eventPublisher)

when:
def items = itemService.getAllItemsSortedByName(['item-id'])

then:
items == [new Item('item-id', 'name')]
_given_セクションからスタブを書き換えることができます。
1 * itemProvider.getItems(['item-id']) >> [new Item('item-id', 'name')]
したがって、一般的に、この行は次のようになります。
モックはスタブと同じように振る舞うことができることをすでに知っています。 引数の制約、複数の値を返すこと、および副作用に関するすべての規則は、_Mock_にも適用されます。

6. Spockでのクラスのスパイ

*スパイは、既存のオブジェクトをラップする機能を提供します。*これは、呼び出し元と実際のオブジェクト間の会話をリッスンできるが、元のオブジェクトの動作を保持できることを意味します。 基本的に、元のオブジェクトへの* __ Spy __delegatesメソッド呼び出し*。
__Mock __と_Stub_とは対照的に、インターフェイスに__Spy ___を作成することはできません。 実際のオブジェクトをラップするため、さらに、コンストラクターの引数を渡す必要があります。 それ以外の場合、型のデフォルトコンストラクターが呼び出されます。

6.1. テスト中のコード

__EventPublisherの簡単な実装を作成しましょう。 LoggingEventPublisher __は、追加されたすべてのアイテムのIDをコンソールに出力します。 インターフェイスメソッドの実装は次のとおりです。
@Override
public void publish(String addedOfferId) {
    System.out.println("I've published: " + addedOfferId);
}

6.2. _Spy_を使用したテスト

  • _Spy(Class)_メソッドを使用して、モックやスタブと同様にスパイを作成します。* _LoggingEventPublisher_には他のクラスの依存関係がないため、コンストラクター引数を渡す必要はありません。

eventPublisher = Spy(LoggingEventPublisher)
では、スパイをテストしましょう。 スパイオブジェクトを使用した__ItemService __の新しいインスタンスが必要です。
given:
eventPublisher = Spy(LoggingEventPublisher)
itemService = new ItemService(itemProvider, eventPublisher)

when:
itemService.saveItems(['item-id'])

then:
1 * eventPublisher.publish('item-id')
__eventPublisher.publish __methodが1回だけ呼び出されることを確認しました。 *さらに、メソッド呼び出しは実際のオブジェクトに渡されたため、コンソールに_println_の出力が表示されます。*
I've published: item-id
_Spy_のメソッドでスタブを使用する場合、実際のオブジェクトメソッドは呼び出されないことに注意してください。 *一般的に、スパイの使用は避けてください。*実行する必要がある場合は、仕様に従ってコードを再配置する必要がありますか?

7. 良いユニットテスト

最後に、モックされたオブジェクトを使用するとテストがどのように改善されるかを簡単に説明します。
  • 確定的なテストスイートを作成します

  • 副作用はありません

  • 単体テストは非常に高速になります

  • 単一のJavaクラスに含まれるロジックに集中できます

  • テストは環境に依存しません

8. 結論

この記事では、Groovy __.__でスパイ、モック、およびスタブを徹底的に説明しました。
すべてのサンプルの実装は、https://github.com/eugenp/tutorials/tree/master/testing-modules/groovy-spock [Githubプロジェクト]にあります。