creating mocks spies mockito with code examples
Mockitoスパイとモックのチュートリアル:
これで Mockitoチュートリアルシリーズ 、前のチュートリアルで Mockitoフレームワークの紹介 。このチュートリアルでは、Mockitoのモックとスパイの概念を学習します。
モックとスパイとは何ですか?
モックとスパイはどちらもテストダブルのタイプであり、単体テストの作成に役立ちます。
モックは依存関係の完全な代替品であり、モックのメソッドが呼び出されるたびに指定された出力を返すようにプログラムできます。 Mockitoは、モックのすべてのメソッドのデフォルトの実装を提供します。
学習内容:
スパイとは何ですか?
スパイは本質的に、モックされた依存関係の実際のインスタンスのラッパーです。これが意味するのは、オブジェクトまたは依存関係の新しいインスタンスが必要であり、その上にモックオブジェクトのラッパーが追加されるということです。デフォルトでは、スパイはスタブされない限り、オブジェクトの実際のメソッドを呼び出します。
スパイは、メソッド呼び出しに提供された引数、実際に呼び出されたメソッドなど、特定の追加機能を提供します。
一言で言えば、スパイの場合:
- オブジェクトの実際のインスタンスが必要です。
- スパイは、スパイされたオブジェクトの一部(またはすべて)のメソッドをスタブ化する柔軟性を提供します。その時点で、スパイは本質的に、部分的にモックまたはスタブされたオブジェクトと呼ばれるか、参照されます。
- スパイされたオブジェクトで呼び出されたインタラクションは、検証のために追跡できます。
一般に、スパイはあまり頻繁に使用されませんが、依存関係を完全にモックできないレガシーアプリケーションの単体テストに役立ちます。
すべてのモックとスパイの説明では、モック/スパイしたい「DiscountCalculator」と呼ばれる架空のクラス/オブジェクトを参照しています。
以下に示すように、いくつかの方法があります。
計算割引 –特定の製品の割引価格を計算します。
getDiscountLimit –製品の割引の上限を取得します。
モックの作成
#1)コードを使用した模擬作成
Mockitoは、Mockitoのいくつかのオーバーロードバージョンを提供します。モックメソッドであり、依存関係のモックを作成できます。
構文:
Mockito.mock(Class classToMock)
例:
コードでモックを作成するために、クラス名がDiscountCalculatorであるとします。
DiscountCalculator mockedDiscountCalculator = Mockito.mock(DiscountCalculator.class)
Mockは、インターフェースまたは具象クラスの両方に対して作成できることに注意することが重要です。
オブジェクトがモックされた場合、スタブされていない限り、すべてのメソッドはデフォルトでnullを返します 。
DiscountCalculator mockDiscountCalculator = Mockito.mock(DiscountCalculator.class);
#2)アノテーションを使用した模擬作成
Mockitoライブラリの静的な「モック」メソッドを使用してモックする代わりに、「@ Mock」アノテーションを使用してモックを作成する簡単な方法も提供します。
このアプローチの最大の利点は、シンプルであり、宣言と基本的に初期化を組み合わせることができることです。また、テストが読みやすくなり、同じモックが複数の場所で使用されているときにモックが繰り返し初期化されるのを防ぎます。
このアプローチでモックの初期化を確実に行うには、テスト対象のクラスに対して「MockitoAnnotations.initMocks(this)」を呼び出す必要があります。これは、Junitの「beforeEach」メソッドの一部として理想的な候補であり、そのクラスからテストが実行されるたびにモックが初期化されるようにします。
構文:
@Mock private transient DiscountCalculator mockedDiscountCalculator;
スパイの作成
モックと同様に、スパイも2つの方法で作成できます。
#1)コードによるスパイの作成
Mockito.spyは、実際のオブジェクトインスタンスの周りに「スパイ」オブジェクト/ラッパーを作成するために使用される静的メソッドです。
構文:
j2eeインタビューの質問と回答pdf
private transient ItemService itemService = new ItemServiceImpl() private transient ItemService spiedItemService = Mockito.spy(itemService);
#2)アノテーションを使用したスパイの作成
モックと同様に、スパイは@Spyアノテーションを使用して作成できます。
スパイの初期化についても、スパイを初期化するために、実際のテストでスパイを使用する前に、MockitoAnnotations.initMocks(this)が呼び出されていることを確認する必要があります。
構文:
@Spy private transient ItemService spiedItemService = new ItemServiceImpl();
テスト対象のクラス/オブジェクトにモックされた依存関係を注入する方法は?
テスト対象のクラスのモックオブジェクトを他のモックされた依存関係とともに作成する場合は、@ InjectMocksアノテーションを使用できます。
これが本質的に行うことは、@ Mock(または@Spy)アノテーションでマークされたすべてのオブジェクトが、ContractorまたはプロパティインジェクションとしてクラスObjectに注入され、最終的なモックオブジェクトで相互作用を検証できることです。
繰り返しになりますが、言うまでもなく、@ InjectMocksは、クラスの新しいオブジェクトを作成することに対する省略形であり、依存関係のモックオブジェクトを提供します。
例でこれを理解しましょう:
コンストラクターフィールドまたはプロパティフィールドを介して注入される依存関係としてDiscountCalculatorとUserServiceを持つクラスPriceCalculatorがあるとします。
したがって、価格計算クラスのモック実装を作成するには、次の2つのアプローチを使用できます。
#1)作成 PriceCalculatorの新しいインスタンスであり、モックされた依存関係を挿入します
@Mock private transient DiscountCalculator mockedDiscountCalculator; @Mock private transient UserService userService; @Mock private transient ItemService mockedItemService; private transient PriceCalculator priceCalculator; @BeforeEach public void beforeEach() { MockitoAnnotations.initMocks(this); priceCalculator = new PriceCalculator(mockedDiscountCalculator, userService, mockedItemService); }
#2)作成 PriceCalculatorのモックインスタンスであり、@ InjectMocksアノテーションを介して依存性を注入します
@Mock private transient DiscountCalculator mockedDiscountCalculator; @Mock private transient UserService userService; @Mock private transient ItemService mockedItemService; @InjectMocks private transient PriceCalculator priceCalculator; @BeforeEach public void beforeEach() { MockitoAnnotations.initMocks(this);
InjectMocksアノテーションは、実際には、以下のいずれかのアプローチを使用して、モックされた依存関係を注入しようとします。
- コンストラクターベースのインジェクション –テスト対象のクラスにコンストラクターを使用します。
- セッターメソッドベース –コンストラクターが存在しない場合、Mockitoはプロパティセッターを使用して注入を試みます。
- フィールドベース –上記の2つが使用できない場合は、フィールドを介して直接注入を試みます。
ヒントとコツ
#1)同じメソッドの異なる呼び出しに対して異なるスタブを設定する:
スタブメソッドがテスト対象のメソッド内で複数回呼び出された場合(またはスタブメソッドがループ内にあり、毎回異なる出力を返したい場合)、毎回異なるスタブ応答を返すようにMockを設定できます。
例えば: あなたが欲しいとしましょう ItemService 3回の連続呼び出しで別のアイテムを返し、テスト対象のメソッドでItem1、Item2、Item3として宣言されているアイテムがある場合、次のコードを使用して、3回の連続呼び出しでこれらを返すことができます。
@Test public void calculatePrice_withCorrectInput_returnsValidResult() { // Arrange ItemSku item1 = new ItemSku(); ItemSku item2 = new ItemSku(); ItemSku item3 = new ItemSku(); // Setup Mocks when(mockedItemService.getItemDetails(anyInt())).thenReturn(item1, item2, item3); // Assert //TODO - add assert statements }
#二) モックを介して例外をスローする: これは、例外をスローするダウンストリーム/依存関係をテスト/検証し、テスト対象のシステムの動作を確認する場合に非常に一般的なシナリオです。ただし、モックで例外をスローするには、thenThrowを使用してスタブを設定する必要があります。
@Test public void calculatePrice_withInCorrectInput_throwsException() { // Arrange ItemSku item1 = new ItemSku(); // Setup Mocks when(mockedItemService.getItemDetails(anyInt())).thenThrow(new ItemServiceException(anyString())); // Assert //TODO - add assert statements }
anyInt()やanyString()のような一致については、今後の記事で取り上げられるため、恐れることはありません。しかし、本質的には、特定の関数の引数なしで、それぞれ整数値と文字列値を提供する柔軟性を提供します。
コード例–スパイとモック
前に説明したように、スパイとモックはどちらもテストダブルのタイプであり、独自の使用法があります。
スパイはレガシーアプリケーションのテストに役立ちますが(モックが不可能な場合)、他のすべての適切に記述されたテスト可能なメソッド/クラスの場合、ユニットテストのニーズのほとんどはモックで十分です。
同じ例の場合: 価格計算ツールのモックを使用してテストを作成しましょう-> calculatePriceメソッド(このメソッドは、適用可能な割引を差し引いたitemPriceを計算します)
PriceCalculatorクラスとテスト対象のメソッドcalculatePriceは、次のようになります。
public class PriceCalculator { public DiscountCalculator discountCalculator; public UserService userService; public ItemService itemService; public PriceCalculator(DiscountCalculator discountCalculator, UserService userService, ItemService itemService) { this.discountCalculator = discountCalculator; this.userService = userService; this.itemService = itemService; } public double calculatePrice(int itemSkuCode, int customerAccountId) { double price = 0; // get Item details ItemSku sku = itemService.getItemDetails(itemSkuCode); // get User and calculate price CustomerProfile customerProfile = userService.getUser(customerAccountId); double basePrice = sku.getPrice(); price = basePrice - (basePrice* (sku.getApplicableDiscount() + customerProfile.getExtraLoyaltyDiscountPercentage())/100); return price; } }
それでは、このメソッドのポジティブテストを書いてみましょう。
以下に説明するように、userServiceとitemserviceをスタブ化します。
- UserServiceは、常にloyaltyDiscountPercentageを2に設定してCustomerProfileを返します。
- ItemServiceは、basePriceが100でapplicableDiscountが5のItemを常に返します。
- 上記の値を使用すると、テスト対象のメソッドによって返されるexpectedPriceは93 $になります。
テストのコードは次のとおりです。
@Test public void calculatePrice_withCorrectInput_returnsExpectedPrice() { // Arrange ItemSku item1 = new ItemSku(); item1.setApplicableDiscount(5.00); item1.setPrice(100.00); CustomerProfile customerProfile = new CustomerProfile(); customerProfile.setExtraLoyaltyDiscountPercentage(2.00); double expectedPrice = 93.00; // Setting up stubbed responses using mocks when(mockedItemService.getItemDetails(anyInt())).thenReturn(item1); when(mockedUserService.getUser(anyInt())).thenReturn(customerProfile); // Act double actualPrice = priceCalculator.calculatePrice(123,5432); // Assert assertEquals(expectedPrice, actualPrice); }
ご覧のとおり、上記のテストでは、メソッドによって返されるactualPriceがexpectedPrice、つまり93.00に等しいと主張しています。
経験豊富なシェルスクリプト面接の質問と回答
それでは、Spyを使用してテストを作成しましょう。
ItemServiceをスパイし、2367のskuCodeで呼び出されるたびに、basePrice 200とapplicableDiscountが10.00%(残りのモックセットアップは同じまま)のアイテムを常に返すようにItemService実装をコーディングします。
@InjectMocks private PriceCalculator priceCalculator; @Mock private DiscountCalculator mockedDiscountCalculator; @Mock private UserService mockedUserService; @Spy private ItemService mockedItemService = new ItemServiceImpl(); @BeforeEach public void beforeEach() { MockitoAnnotations.initMocks(this); } @Test public void calculatePrice_withCorrectInputRealMethodCall_returnsExpectedPrice() { // Arrange CustomerProfile customerProfile = new CustomerProfile(); customerProfile.setExtraLoyaltyDiscountPercentage(2.00); double expectedPrice = 176.00; // Setting up stubbed responses using mocks when(mockedUserService.getUser(anyInt())).thenReturn(customerProfile); // Act double actualPrice = priceCalculator.calculatePrice(2367,5432); // Assert assertEquals(expectedPrice, actualPrice);
さて、見てみましょう 例 使用可能なアイテムの数量が0であったため、ItemServiceによって例外がスローされました。例外をスローするようにモックを設定します。
@InjectMocks private PriceCalculator priceCalculator; @Mock private DiscountCalculator mockedDiscountCalculator; @Mock private UserService mockedUserService; @Mock private ItemService mockedItemService = new ItemServiceImpl(); @BeforeEach public void beforeEach() { MockitoAnnotations.initMocks(this); } @Test public void calculatePrice_whenItemNotAvailable_throwsException() { // Arrange CustomerProfile customerProfile = new CustomerProfile(); customerProfile.setExtraLoyaltyDiscountPercentage(2.00); double expectedPrice = 176.00; // Setting up stubbed responses using mocks when(mockedUserService.getUser(anyInt())).thenReturn(customerProfile); when(mockedItemService.getItemDetails(anyInt())).thenThrow(new ItemServiceException(anyString())); // Act & Assert assertThrows(ItemServiceException.class, () -> priceCalculator.calculatePrice(123, 234)); }
上記の例を使用して、Mocks&Spiesの概念と、それらを組み合わせて効果的で有用な単体テストを作成する方法を説明しようとしました。
これらの手法を複数組み合わせて、テスト対象のメソッドのカバレッジを強化する一連のテストを取得できます。これにより、コードの信頼性が高まり、コードの回帰バグに対する耐性が高まります。
ソースコード
インターフェイス
DiscountCalculator
public interface DiscountCalculator { double calculateDiscount(ItemSku itemSku, double markedPrice); void calculateProfitability(ItemSku itemSku, CustomerProfile customerProfile); }
ItemService
public interface ItemService { ItemSku getItemDetails(int skuCode) throws ItemServiceException; }
UserService
public interface UserService { void addUser(CustomerProfile customerProfile); void deleteUser(CustomerProfile customerProfile); CustomerProfile getUser(int customerAccountId); }
インターフェイスの実装
DiscountCalculatorImpl
public class DiscountCalculatorImpl implements DiscountCalculator { @Override public double calculateDiscount(ItemSku itemSku, double markedPrice) { return 0; } @Override public void calculateProfitability(ItemSku itemSku, CustomerProfile customerProfile) { } }
ItemServiceImpl
public class DiscountCalculatorImpl implements DiscountCalculator { @Override public double calculateDiscount(ItemSku itemSku, double markedPrice) { return 0; } @Override public void calculateProfitability(ItemSku itemSku, CustomerProfile customerProfile) { } }
モデル
顧客情報
public class CustomerProfile { private String customerName; private String loyaltyTier; private String customerAddress; private String accountId; private double extraLoyaltyDiscountPercentage; public double getExtraLoyaltyDiscountPercentage() { return extraLoyaltyDiscountPercentage; } public void setExtraLoyaltyDiscountPercentage(double extraLoyaltyDiscountPercentage) { this.extraLoyaltyDiscountPercentage = extraLoyaltyDiscountPercentage; } public String getAccountId() { return accountId; } public void setAccountId(String accountId) { this.accountId = accountId; } public String getCustomerName() { return customerName; } public void setCustomerName(String customerName) { this.customerName = customerName; } public String getLoyaltyTier() { return loyaltyTier; } public void setLoyaltyTier(String loyaltyTier) { this.loyaltyTier = loyaltyTier; } public String getCustomerAddress() { return customerAddress; } public void setCustomerAddress(String customerAddress) { this.customerAddress = customerAddress; } }
ItemSku
public class ItemSku { private int skuCode; private double price; private double maxDiscount; private double margin; private int totalQuantity; private double applicableDiscount; public double getApplicableDiscount() { return applicableDiscount; } public void setApplicableDiscount(double applicableDiscount) { this.applicableDiscount = applicableDiscount; } public int getTotalQuantity() { return totalQuantity; } public void setTotalQuantity(int totalQuantity) { this.totalQuantity = totalQuantity; } public int getSkuCode() { return skuCode; } public void setSkuCode(int skuCode) { this.skuCode = skuCode; } public double getPrice() { return price; } public void setPrice(double price) { this.price = price; } public double getMaxDiscount() { return maxDiscount; } public void setMaxDiscount(double maxDiscount) { this.maxDiscount = maxDiscount; } public double getMargin() { return margin; } public void setMargin(double margin) { this.margin = margin; } }
テスト中のクラス– PriceCalculator
public class PriceCalculator { public DiscountCalculator discountCalculator; public UserService userService; public ItemService itemService; public PriceCalculator(DiscountCalculator discountCalculator, UserService userService, ItemService itemService){ this.discountCalculator = discountCalculator; this.userService = userService; this.itemService = itemService; } public double calculatePrice(int itemSkuCode, int customerAccountId) { double price = 0; // get Item details ItemSku sku = itemService.getItemDetails(itemSkuCode); // get User and calculate price CustomerProfile customerProfile = userService.getUser(customerAccountId); double basePrice = sku.getPrice(); price = basePrice - (basePrice* (sku.getApplicableDiscount() + customerProfile.getExtraLoyaltyDiscountPercentage())/100); return price; } }
ユニットテスト– PriceCalculatorUnitTests
public class PriceCalculatorUnitTests { @InjectMocks private PriceCalculator priceCalculator; @Mock private DiscountCalculator mockedDiscountCalculator; @Mock private UserService mockedUserService; @Mock private ItemService mockedItemService; @BeforeEach public void beforeEach() { MockitoAnnotations.initMocks(this); } @Test public void calculatePrice_withCorrectInput_returnsExpectedPrice() { // Arrange ItemSku item1 = new ItemSku(); item1.setApplicableDiscount(5.00); item1.setPrice(100.00); CustomerProfile customerProfile = new CustomerProfile(); customerProfile.setExtraLoyaltyDiscountPercentage(2.00); double expectedPrice = 93.00; // Setting up stubbed responses using mocks when(mockedItemService.getItemDetails(anyInt())).thenReturn(item1); when(mockedUserService.getUser(anyInt())).thenReturn(customerProfile); // Act double actualPrice = priceCalculator.calculatePrice(123,5432); // Assert assertEquals(expectedPrice, actualPrice); } @Test @Disabled // to enable this change the ItemService MOCK to SPY public void calculatePrice_withCorrectInputRealMethodCall_returnsExpectedPrice() { // Arrange CustomerProfile customerProfile = new CustomerProfile(); customerProfile.setExtraLoyaltyDiscountPercentage(2.00); double expectedPrice = 176.00; // Setting up stubbed responses using mocks when(mockedUserService.getUser(anyInt())).thenReturn(customerProfile); // Act double actualPrice = priceCalculator.calculatePrice(2367,5432); // Assert assertEquals(expectedPrice, actualPrice); } @Test public void calculatePrice_whenItemNotAvailable_throwsException() { // Arrange CustomerProfile customerProfile = new CustomerProfile(); customerProfile.setExtraLoyaltyDiscountPercentage(2.00); double expectedPrice = 176.00; // Setting up stubbed responses using mocks when(mockedUserService.getUser(anyInt())).thenReturn(customerProfile); when(mockedItemService.getItemDetails(anyInt())).thenThrow(new ItemServiceException(anyString())); // Act & Assert assertThrows(ItemServiceException.class, () -> priceCalculator.calculatePrice(123, 234)); } }
Mockitoが提供するさまざまなタイプのマッチャーについては、次のチュートリアルで説明します。