モックとスタブ-Mockitoでテストダブルを理解する

私が遭遇する一般的なことは、モックフレームワークを使用しているチームがモックしていると想定していることです。

彼らは、モックがジェラルド・メサロスがxunitpatterns.comで分類した「テストダブル」の1つにすぎないことに気づいていません。

テストダブルの種類ごとに、テストで果たす役割が異なることを理解することが重要です。さまざまなパターンやリファクタリングを学習する必要があるのと同じように、各タイプのテストダブルの基本的な役割を理解する必要があります。次に、これらを組み合わせて、テストのニーズを達成できます。

この分類がどのように行われたか、および各タイプがどのように異なるかについて、非常に簡単な歴史をカバーします。

Mockitoのいくつかの短くて簡単な例を使用してこれを行います。

何年もの間、人々はテストを支援するためにシステムコンポーネントの軽量バージョンを書いてきました。一般的に、それはスタブと呼ばれていました。2000年に、記事「Endo-Testing:Unit Testing with Mock Objects」で、モックオブジェクトの概念が紹介されました。それ以来、スタブ、モック、およびその他の多くのタイプのテストオブジェクトは、Meszarosによってテストダブルとして分類されています。

この用語は、MartinFowlerによって「MocksAren'tStubs」で参照されており、「Exploring the Continuum ofTestDoubles」に示されているようにMicrosoftコミュニティ内で採用されています。

これらの重要な論文のそれぞれへのリンクは、参照セクションに示されています。

上の図は、一般的に使用されるタイプのテストダブルを示しています。次のURLは、各パターンとその機能、および代替用語への適切な相互参照を提供します。

//xunitpatterns.com/Test%20Double.html

Mockitoはテストスパイフレームワークであり、習得は非常に簡単です。Mockitoで注目すべき点は、他のモックフレームワークにある場合があるため、テスト前にモックオブジェクトの期待値が定義されていないことです。これにより、モックを開始するときのより自然なスタイル(IMHO)につながります。

以下の例は、Mockitoを使用してさまざまなタイプのテストダブルを実装する簡単なデモンストレーションを提供するためのものです。

ウェブサイトには、Mockitoの使用方法の具体例がはるかにたくさんあります。

//docs.mockito.googlecode.com/hg/latest/org/mockito/Mockito.html

以下は、Mockitoを使用して、Meszarosによって定義された各テストダブルの役割を示すいくつかの基本的な例です。

より多くの例と完全な定義を取得できるように、それぞれのメイン定義へのリンクを含めました。

//xunitpatterns.com/Dummy%20Object.html

これは、すべてのテストダブルの中で最も単純です。これは、テストに関係のないメソッド呼び出しの引数を設定するためだけに使用される実装がないオブジェクトです。

たとえば、以下のコードは、テストにとって重要ではない顧客を作成するために多くのコードを使用しています。

顧客数が1つに戻る限り、テストではどの顧客が追加されるかを気にする必要はありません。

public Customer createDummyCustomer() { County county = new County("Essex"); City city = new City("Romford", county); Address address = new Address("1234 Bank Street", city); Customer customer = new Customer("john", "dobie", address); return customer; } @Test public void addCustomerTest() { Customer dummy = createDummyCustomer(); AddressBook addressBook = new AddressBook(); addressBook.addCustomer(dummy); assertEquals(1, addressBook.getNumberOfCustomers()); } 

実際には、顧客オブジェクトの内容は気にしませんが、必須です。null値を試すことはできますが、コードが正しければ、なんらかの例外がスローされることが予想されます。

@Test(expected=Exception.class) public void addNullCustomerTest() { Customer dummy = null; AddressBook addressBook = new AddressBook(); addressBook.addCustomer(dummy); } 

これを回避するために、単純なMockitoダミーを使用して目的の動作を取得できます。

@Test public void addCustomerWithDummyTest() { Customer dummy = mock(Customer.class); AddressBook addressBook = new AddressBook(); addressBook.addCustomer(dummy); Assert.assertEquals(1, addressBook.getNumberOfCustomers()); } 

呼び出しに渡されるダミーオブジェクトを作成するのは、この単純なコードです。

Customer dummy = mock(Customer.class);

モック構文にだまされないでください。ここで実行される役割は、モックではなくダミーの役割です。

それを際立たせるのはテストダブルの役割であり、それを作成するために使用される構文ではありません。

このクラスは、顧客クラスの単純な代替として機能し、テストを非常に読みやすくします。

//xunitpatterns.com/Test%20Stub.html

テストスタブの役割は、制御された値をテスト対象のオブジェクトに返すことです。これらは、テストへの間接的な入力として説明されています。うまくいけば、例がこれが何を意味するかを明らかにするでしょう。

次のコードを取ります

public class SimplePricingService implements PricingService { PricingRepository repository; public SimplePricingService(PricingRepository pricingRepository) { this.repository = pricingRepository; } @Override public Price priceTrade(Trade trade) { return repository.getPriceForTrade(trade); } @Override public Price getTotalPriceForTrades(Collection trades) { Price totalPrice = new Price(); for (Trade trade : trades) { Price tradePrice = repository.getPriceForTrade(trade); totalPrice = totalPrice.add(tradePrice); } return totalPrice; } 

SimplePricingServiceには、トレードリポジトリである1つのコラボレーションオブジェクトがあります。取引リポジトリは、getPriceForTradeメソッドを介して価格設定サービスに取引価格を提供します。

SimplePricingServiceでbusineesロジックをテストするには、これらの間接入力を制御する必要があります

つまり、テストに合格しなかった入力です。

これを以下に示します。

次の例では、PricingRepositoryをスタブ化して、SimpleTradeServiceのビジネスロジックをテストするために使用できる既知の値を返します。

@Test public void testGetHighestPricedTrade() throws Exception { Price price1 = new Price(10); Price price2 = new Price(15); Price price3 = new Price(25); PricingRepository pricingRepository = mock(PricingRepository.class); when(pricingRepository.getPriceForTrade(any(Trade.class))) .thenReturn(price1, price2, price3); PricingService service = new SimplePricingService(pricingRepository); Price highestPrice = service.getHighestPricedTrade(getTrades()); assertEquals(price3.getAmount(), highestPrice.getAmount()); } 

妨害者の例

テストスタブには、レスポンダーとサボターの2つの一般的なバリエーションがあります。

前の例のように、レスポンダーを使用してハッピーパスをテストします。

妨害工作員は、以下のように例外的な行動をテストするために使用されます。

@Test(expected=TradeNotFoundException.class) public void testInvalidTrade() throws Exception { Trade trade = new FixtureHelper().getTrade(); TradeRepository tradeRepository = mock(TradeRepository.class); when(tradeRepository.getTradeById(anyLong())) .thenThrow(new TradeNotFoundException()); TradingService tradingService = new SimpleTradingService(tradeRepository); tradingService.getTradeById(trade.getId()); } 

//xunitpatterns.com/Mock%20Object.html

モックオブジェクトは、テスト中のオブジェクトの動作を検証するために使用されます。オブジェクトの動作とは、テストの実行時にオブジェクトに対して正しいメソッドとパスが実行されていることを確認することを意味します。

これは、テストしているものすべてに結果を提供するために使用されるスタブの脇役とは大きく異なります。

スタブでは、メソッドの戻り値を定義するパターンを使用します。

when(customer.getSurname()).thenReturn(surname); 

モックでは、次のフォームを使用してオブジェクトの動作を確認します。

verify(listMock).add(s); 

Here is a simple example where we want to test that a new trade is audited correctly.

Here is the main code.

public class SimpleTradingService implements TradingService{ TradeRepository tradeRepository; AuditService auditService; public SimpleTradingService(TradeRepository tradeRepository, AuditService auditService) { this.tradeRepository = tradeRepository; this.auditService = auditService; } public Long createTrade(Trade trade) throws CreateTradeException { Long id = tradeRepository.createTrade(trade); auditService.logNewTrade(trade); return id; } 

The test below creates a stub for the trade repository and mock for the AuditService

We then call verify on the mocked AuditService to make sure that the TradeService calls it's

logNewTrade method correctly

@Mock TradeRepository tradeRepository; @Mock AuditService auditService; @Test public void testAuditLogEntryMadeForNewTrade() throws Exception { Trade trade = new Trade("Ref 1", "Description 1"); when(tradeRepository.createTrade(trade)).thenReturn(anyLong()); TradingService tradingService = new SimpleTradingService(tradeRepository, auditService); tradingService.createTrade(trade); verify(auditService).logNewTrade(trade); } 

The following line does the checking on the mocked AuditService.

verify(auditService).logNewTrade(trade);

This test allows us to show that the audit service behaves correctly when creating a trade.

//xunitpatterns.com/Test%20Spy.html

It's worth having a look at the above link for the strict definition of a Test Spy.

However in Mockito I like to use it to allow you to wrap a real object and then verify or modify it's behaviour to support your testing.

Here is an example were we check the standard behaviour of a List. Note that we can both verify that the add method is called and also assert that the item was added to the list.

@Spy List listSpy = new ArrayList(); @Test public void testSpyReturnsRealValues() throws Exception { String s = "dobie"; listSpy.add(new String(s)); verify(listSpy).add(s); assertEquals(1, listSpy.size()); } 

Compare this with using a mock object where only the method call can be validated. Because we only mock the behaviour of the list, it does not record that the item has been added and returns the default value of zero when we call the size() method.

@Mock List listMock = new ArrayList(); @Test public void testMockReturnsZero() throws Exception { String s = "dobie"; listMock.add(new String(s)); verify(listMock).add(s); assertEquals(0, listMock.size()); } 

Another useful feature of the testSpy is the ability to stub return calls. When this is done the object will behave as normal until the stubbed method is called.

In this example we stub the get method to always throw a RuntimeException. The rest of the behaviour remains the same.

@Test(expected=RuntimeException.class) public void testSpyReturnsStubbedValues() throws Exception { listSpy.add(new String("dobie")); assertEquals(1, listSpy.size()); when(listSpy.get(anyInt())).thenThrow(new RuntimeException()); listSpy.get(0); } 

In this example we again keep the core behaviour but change the size() method to return 1 initially and 5 for all subsequent calls.

public void testSpyReturnsStubbedValues2() throws Exception { int size = 5; when(listSpy.size()).thenReturn(1, size); int mockedListSize = listSpy.size(); assertEquals(1, mockedListSize); mockedListSize = listSpy.size(); assertEquals(5, mockedListSize); mockedListSize = listSpy.size(); assertEquals(5, mockedListSize); } 

This is pretty Magic!

//xunitpatterns.com/Fake%20Object.html

偽物は通常、手作りまたは軽量のオブジェクトであり、テストにのみ使用され、生産には適していません。良い例は、インメモリデータベースまたは偽のサービスレイヤーです。

これらは、標準のテストダブルよりもはるかに多くの機能を提供する傾向があるため、通常、Mockitoを使用した実装の候補にはなりません。それは、それらをそのように構築できなかったということではなく、おそらくこの方法で実装する価値がないということです。

ダブルパターンをテストする

Endo-Testing:モックオブジェクトを使用したユニットテスト

オブジェクトではなく、モックロール

モックはスタブではありません

//msdn.microsoft.com/en-us/magazine/cc163358.aspx

このストーリー「モックとスタブ-Mockitoでテストダブルを理解する」は、もともとJavaWorldによって公開されました。