1. 概要

このチュートリアルでは、 Spockフレームワークのモック、スタブ、スパイの違いについて説明します。 インタラクションベースのテストに関連してフレームワークが提供するものを説明します。

Spock は、JavaおよびGroovyのテストフレームワークであり、ソフトウェアアプリケーションの手動テストのプロセスを自動化するのに役立ちます。 独自のモック、スタブ、スパイを導入し、通常は追加のライブラリを必要とするテスト用の組み込み機能が付属しています。

まず、スタブを使用するタイミングを説明します。 次に、モックを実行します。 最後に、最近導入されたSpyについて説明します。

2. Mavenの依存関係

始める前に、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>

1.3-RC1-groovy-2.5バージョンのSpockが必要になることに注意してください。 Spy は、SpockFrameworkの次の安定バージョンで導入されます。 現在、Spyはバージョン1.3の最初のリリース候補で利用可能です。

Spockテストの基本構造の要約については、GroovyとSpockを使用したテストに関する紹介記事を確認してください。

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

相互作用ベースのテストは、オブジェクトの動作、具体的には、オブジェクトが互いにどのように相互作用するかをテストするのに役立つ手法です。 このために、モックとスタブと呼ばれるダミーの実装を使用できます。

もちろん、モックとスタブの独自の実装を非常に簡単に作成することは確かに可能です。 問題は、本番コードの量が増えると発生します。 このコードを手作業で記述して維持することは困難になります。 これが、予想される相互作用を簡単に説明する簡潔な方法を提供するモッキングフレームワークを使用する理由です。 Spockには、モック、スタブ、スパイのサポートが組み込まれています。

ほとんどのJavaライブラリと同様に、Spockはインターフェイスのモックに JDK動的プロキシを使用し、クラスのモックに ByteBuddyまたは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は等号を使用します:

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メソッドに含まれるロジックのみをテストするため、外部依存関係をスタブ化する必要があります。

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メソッドは、で呼び出されたときに常に2つのアイテムのリストを返します。 [‘offer-id’、’offer-id-2’] リスト。 [] Groovy リストを作成するためのショートカット。

テスト方法全体は次のとおりです。

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 であるため、一般的に、契約について説明します。

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()メソッドでEventPublisherをモックする必要があります。 したがって、基本的に、新しいインスタンスフィールドを作成し、 Mock(Class)関数を使用してそれをモックします。

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’、’b’を渡し、eventPublisherが’a’と’b’の文字列で2つのイベントを公開することを期待します。

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')
}

最後の次にセクションでのアサーションを詳しく見てみましょう。

1 * eventPublisher.publish('a')

itemService は、引数として「a」を指定して eventPublisher.publish(String)を呼び出すことを期待しています。

スタブでは、引数の制約について説明しました。 同じルールがモックに適用されます。 eventPublisher.publish(String)がnullでも空でもない引数で2回呼び出されたことを確認できます:

2 * eventPublisher.publish({ it != null && !it.isEmpty() })

5.3. モックとスタブの組み合わせ

Spockでは、 モックはスタブと同じように動作する可能性があります。したがって、モックされたオブジェクトに対して、特定のメソッド呼び出しに対して、特定のデータを返す必要があると言えます。

ItemProviderMock(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')]

指定されたセクションからスタブを書き換えることができます。

1 * itemProvider.getItems(['item-id']) >> [new Item('item-id', 'name')]

したがって、一般的に、この行は次のようになります。 itemProvider.getItemsは[‘item-‘id’]引数で一度呼び出され、指定された配列を返します。

モックがスタブと同じように動作できることはすでに知っています。 引数の制約、複数の値を返すこと、および副作用に関するすべてのルールは、Mockにも適用されます。

6. スポックのスパイクラス

スパイは既存のオブジェクトをラップする機能を提供します。これは、呼び出し元と実際のオブジェクトとの間の会話をリッスンできますが、元のオブジェクトの動作は保持できることを意味します。 基本的に、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メソッドが1回だけ呼び出されたことを確認しました。 さらに、メソッド呼び出しが実際のオブジェクトに渡されたため、コンソールにprintlnの出力が表示されます:

I've published: item-id

Spy のメソッドでスタブを使用すると、実際のオブジェクトメソッドが呼び出されないことに注意してください。 一般的に、スパイの使用は避けてください。必要な場合は、仕様に基づいてコードを再配置する必要がありますか?

7. 良い単体テスト

最後に、モックされたオブジェクトを使用するとテストがどのように改善されるかについて簡単に説明します。

  • 決定論的なテストスイートを作成します
  • 副作用はありません
  • 私たちのユニットテストは非常に高速になります
  • 単一のJavaクラスに含まれるロジックに焦点を当てることができます
  • 私たちのテストは環境に依存しません

8. 結論

この記事では、Groovyのスパイ、モック、スタブについて詳しく説明しました。 。 このテーマに関する知識は、テストをより速く、より信頼性が高く、読みやすくします。

すべての例の実装は、Githubプロジェクトにあります。