Groovy Mocks

Groovy has excellent built-in support for a range of mocking alternatives. Before considering those, let's review some relevant terms.

Groovyには、さまざまなモックの代わりになる素晴らしい標準装備があります。それらのことについてよく考える前にいくつかの関連のある用語を復習しましょう。

Terms

用語

Collaborator

An ordinary Groovy or Java class that's instance or class methods are to be called.
Calling them can be time consuming or produce side effects that are unwanted when testing (e.g. database operations).
通常のGroovyやJavaクラスのインスタンスやクラスメソッドから呼びだされます。
それらを呼ぶことは、テストの際には望まれていない時間の消費や副作用を引き起こします。(例:データベースの操作)

Caller

A Groovy Object that calls methods on the Collaborator, i.e. collaborates with it.
GroovyオブジェクトはCollaboratorからメソッドを呼びます。(例:collaborates with it. )

Mock

An object that can be used to augment the Collaborator. Method calls to the Collaborator will be handled by the Mock, showing a demanded behavior. Method calls are expected to occur strictly in the demanded sequence with a given range of cardinality. The use of a Mock implicitly ends with verifying the expectations.
オブジェクトはCollaboratorを増やすために使用されます。メソッドは 要求された 振る舞いを表すMockから扱うためにCollaboratorを呼びだします。メソッドコールは与えられた 範囲 の数から 要求された シーケンスを 厳密に 実行されることが 予期 されます。

Stub

Much like a Mock but the expectation about sequences of method calls on the Collaborator is loose, i.e. calls may occur out of the demanded order as long as the ranges of cardinality are met. The use of a Stub does not end with an implicit verification since the stubbing effect is typically asserted on the Caller. An explicit call to verify can be issued to assert all demanded method calls have been effected with the specified cardinality.
Mockと同様ですが、Collaboratorの一連のメソッドコールの 期待 は比較的 ルーズ です。例えば、コールは値の 範囲 が満たされる限り、 要求された 順番通りに 起こらない かもしれません。スタブ効果がCallerから通常アサートされるので、Stubは暗黙の検証で 終わりません検証する ために明白な呼び出しが特定の値とともに全ての 要求され 影響を受けたメソッドコールをアサートするために発行されます。

An extended example

一歩進んだサンプル

System under test

システムのテスト

We will explore a system under test inspired from the JBehave currency example.
JBehaveの通貨のサンプルからインスパイアされたシステムのテストを見てみようと思います。

Our system makes use of a base currency class used to represent the currency of a particular country:
わたしたちのシステムは基本的な通貨クラスを使って表すある特定の国の通貨の使い方を作ります。

class Currency {
    public static final Currency USD = new Currency("USD")
    public static final Currency EUR = new Currency("EUR")
    private String currencyCode
    private Currency(String currencyCode) {
        this.currencyCode = currencyCode
    }
    public String toString() {
        return currencyCode
    }
}

and a base exchange rate class which encapsulates buying and selling rates for a currency:
そして基本となる為替レートクラス(通貨のために売買レートをカプセル化している)は以下のようになる:

class ExchangeRate {
    final double fromRate
    final double toRate
    public ExchangeRate(double fromRate, double toRate) {
        this.fromRate = fromRate
        this.toRate = toRate
    }
}

We will make use of an exchange rate service collaborator to retrieve the exchange rates for a particular country:
特定の国の交換レートを取得する交換レートサービスコラボレータの使い方を示します。

interface ExchangeRateService {
    ExchangeRate retrieveRate(Currency c)
}

Our class under test is a currency converter. It makes use of the following exception:
このクラスは通貨コンバータです。そしてこれには次のような例外が発生します。

class InvalidAmountException extends Exception {
    public InvalidAmountException(String message) {super(message);}
}

and conforms to the following interface:
そして次のインターフェイスと一致します。

interface CurrencyConverter {
    double convertFromSterling(double amount, Currency toCurrency) throws InvalidAmountException
    double convertToSterling(double amount, Currency fromCurrency) throws InvalidAmountException
}

Here is our class under test.
これがクラスです。

class SterlingCurrencyConverter implements CurrencyConverter {
    private ExchangeRateService exchangeRateService

    public SterlingCurrencyConverter(ExchangeRateService exchangeRateService) {
        this.exchangeRateService = exchangeRateService;
    }

    private double convert(double amount, double rate) throws InvalidAmountException {
        if (amount < 0) {
            throw new InvalidAmountException("amount must be non-negative")
        }
        return amount * rate
    }

    public double convertFromSterling(double amount, finance.Currency toCurrency) throws InvalidAmountException {
        return convert(amount, exchangeRateService.retrieveRate(toCurrency).fromRate)
    }

    public double convertToSterling(double amount, finance.Currency fromCurrency) throws InvalidAmountException {
        return convert(amount, exchangeRateService.retrieveRate(fromCurrency).toRate)
    }
}

Mocking using Map coercion

Mocking using Map coercion

When using Java, Dynamic mocking frameworks are very popular. A key reason for this is that it is hard work creating custom hand-crafted mocks using Java.
Javaを使うとき動的なモックフレームワークはとても一般的です。主な理由は自作のモックを作るのはとても骨の折れる作業だからです。

Such frameworks can be used easily with Groovy if you choose (as shown in this extended example) but creating custom mocks is much easier in Groovy.
そのようなフレームワークはGroovyで簡単に使うことができます(拡張された例を紹介します)。しかし自作のモックはGroovyではとても簡単に作ることができます。

You can often get away with simple maps or closures to build your custom mocks.
シンプルなMapやClosureでカスタムのモックを作ることができるでしょう。

Let's consider maps first.
まず最初にMapについて考えてみましょう。

By using maps or expandos, we can incorporate desired behaviour of a collaborator very easily as shown here:
MapやExpandoを使うことで、ここに示すようにCollaboratorの要求された動作を具現化することができます。

def service = [retrieveRate:{ new ExchangeRate(1.45, 0.57) }] as ExchangeRateService
def sterlingConverter = new SterlingCurrencyConverter(service)
double convertedAmount = sterlingConverter.convertFromSterling(10.0, Currency.USD);
assert convertedAmount == 14.50

For more details, see Developer Testing using Maps and Expandos instead of Mocks.

Mocking using Closure coercion

Alternatively, we can use closures:

service = { new ExchangeRate(1.55, 0.56) } as ExchangeRateService
sterlingConverter = new SterlingCurrencyConverter(service)
convertedAmount = sterlingConverter.convertFromSterling(10.0, Currency.USD);
assert convertedAmount == 15.50

For more details, see Developer Testing using Closures instead of Mocks.

Mocking using MockFor and StubFor

If we need the full power of a dynamic mocking framework, Groovy has a built-in framework which makes use of meta-programming to define the behaviour of the collaborator. An example is shown here:

import groovy.mock.interceptor.*

def mockContextClass = new MockFor(DummyExchangeRateService)
mockContextClass.demand.retrieveRate { new ExchangeRate(1.65, 0.55) }
class DummyExchangeRateService implements ExchangeRateService {
ExchangeRate retrieveRate(Currency currency){}
}
mockContextClass.use {
def dummyService = new DummyExchangeRateService()
sterlingConverter = new SterlingCurrencyConverter(dummyService)
convertedAmount = sterlingConverter.convertFromSterling(10.0, Currency.USD)
assert convertedAmount == 16.50
}

This approach works well for testing Groovy classes. In the current versions of Groovy (Groovy 1.5 and 1.6 beta1 at the time of writing this page), the behavior that you define with demand clauses represents the behavior of all of the instances of the mocked class type. For more details, see Using MockFor and StubFor.

Instance-style MockFor and StubFor

You can also use MockFor and StubFor in a more traditional style by creating instances as follows:

mockContext1 = new MockFor(ExchangeRateService)
mockContext1.demand.retrieveRate { new ExchangeRate(1.75, 0.54) }
def dummyService1 = mockContext1.proxyInstance()
def sterlingConverter1 = new SterlingCurrencyConverter(dummyService1)
convertedAmount1 = sterlingConverter1.convertFromSterling(10.0, Currency.USD)
mockContext1.verify dummyService1
assert convertedAmount1 == 17.50

mockContext2 = new MockFor(ExchangeRateService)
mockContext2.demand.retrieveRate(1){ new ExchangeRate(1.85, 0.53) }
def dummyService2 = mockContext2.proxyInstance()
def sterlingConverter2 = new SterlingCurrencyConverter(dummyService2)
convertedAmount2 = sterlingConverter2.convertFromSterling(10.0, Currency.USD)
mockContext2.verify dummyService2
assert convertedAmount2 == 18.50

This approach let's you have multiple instances of the same class all with different behaviors. Also, note that you have to explicitly call the verify method here if you want to check that the demanded behavior was in fact observed. Also, you can use this technique for testing Java classes but you need to call proxyDelegateInstance() instead of proxyInstance().