creating mocks spies mockito with code examples
Mockito Spy 및 Mocks 튜토리얼 :
이것에 Mockito 튜토리얼 시리즈 , 이전 튜토리얼에서 Mockito 프레임 워크 소개 . 이 튜토리얼에서는 Mockito에서 Mocks와 Spies의 개념을 배웁니다.
목과 스파이는 무엇입니까?
Mocks와 Spies는 모두 테스트 복식의 유형으로 단위 테스트 작성에 도움이됩니다.
모의는 종속성을 완전히 대체하며 모의 메서드가 호출 될 때마다 지정된 출력을 반환하도록 프로그래밍 할 수 있습니다. Mockito는 모의 모든 메소드에 대한 기본 구현을 제공합니다.
학습 내용 :
- 스파이는 무엇입니까?
- 모의 만들기
- 스파이 만들기
- 테스트중인 클래스 / 객체에 대해 모의 종속성을 삽입하는 방법은 무엇입니까?
- 팁 & 트릭
- 코드 예 – 스파이 및 모의
- 소스 코드
- 추천 도서
스파이는 무엇입니까?
스파이는 기본적으로 모의 종속성의 실제 인스턴스에 대한 래퍼입니다. 이것이 의미하는 바는 Object 또는 종속성의 새 인스턴스가 필요하고 그 위에 모의 객체의 래퍼를 추가한다는 것입니다. 기본적으로 스파이는 stubb되지 않는 한 개체의 실제 메서드를 호출합니다.
스파이는 메서드 호출에 제공된 인수, 전혀 호출 된 실제 메서드 등과 같은 특정 추가 권한을 제공합니다.
요컨대, 스파이에게 :
- 개체의 실제 인스턴스가 필요합니다.
- 스파이는 스파이 대상의 일부 (또는 모든) 메서드를 스텁 할 수있는 유연성을 제공합니다. 그 당시 스파이는 본질적으로 부분적으로 조롱되거나 스터브 된 개체라고 불리거나 언급됩니다.
- 감시 대상 개체에 대해 호출 된 상호 작용은 확인을 위해 추적 할 수 있습니다.
일반적으로 스파이는 자주 사용되지는 않지만 종속성을 완전히 모의 할 수없는 레거시 애플리케이션 단위 테스트에 유용 할 수 있습니다.
모든 모의 및 스파이 설명에서 우리는 모의 / 스파이를 원하는 'DiscountCalculator'라는 가상의 클래스 / 객체를 참조합니다.
다음과 같은 몇 가지 방법이 있습니다.
계산 할인 – 주어진 제품의 할인 된 가격을 계산합니다.
getDiscountLimit – 상품의 할인 상한선을 가져옵니다.
모의 만들기
# 1) 코드로 모의 생성
Mockito는 Mockito의 여러 오버로드 된 버전을 제공합니다. Mocks 메서드를 사용하고 종속성에 대한 모의를 만들 수 있습니다.
통사론:
Mockito.mock(Class classToMock)
예:
클래스 이름이 DiscountCalculator라고 가정하여 코드에서 모의 객체를 만듭니다.
DiscountCalculator mockedDiscountCalculator = Mockito.mock(DiscountCalculator.class)
Mock은 인터페이스 또는 구체적인 클래스 모두에 대해 생성 될 수 있습니다.
객체가 모의 될 때 스텁되지 않는 한 모든 메서드는 기본적으로 null을 반환합니다. .
DiscountCalculator mockDiscountCalculator = Mockito.mock(DiscountCalculator.class);
# 2) 주석으로 모의 생성
Mockito 라이브러리의 정적 'mock'방법을 사용하여 조롱하는 대신 '@Mock'주석을 사용하여 조롱을 만드는 간단한 방법도 제공합니다.
이 접근 방식의 가장 큰 장점은 간단하고 선언과 기본적으로 초기화를 결합 할 수 있다는 것입니다. 또한 테스트를 더 읽기 쉽게 만들고 동일한 모의가 여러 위치에서 사용될 때 반복되는 모의 초기화를 방지합니다.
이 접근 방식을 통해 Mock 초기화를 보장하려면 테스트중인 클래스에 대해 'MockitoAnnotations.initMocks (this)'를 호출해야합니다. 이는 해당 클래스에서 테스트가 실행될 때마다 모의가 초기화되도록하는 Junit의 'beforeEach'메소드의 일부가되기에 이상적인 후보입니다.
통사론:
@Mock private transient DiscountCalculator mockedDiscountCalculator;
스파이 만들기
Mocks와 마찬가지로 Spies는 두 가지 방법으로 생성 할 수 있습니다.
# 1) 코드로 스파이 생성
Mockito.spy는 실제 개체 인스턴스 주위에 '스파이'개체 / 래퍼를 만드는 데 사용되는 정적 메서드입니다.
통사론:
최고의 받아쓰기 소프트웨어는 무엇입니까
private transient ItemService itemService = new ItemServiceImpl() private transient ItemService spiedItemService = Mockito.spy(itemService);
# 2) 주석으로 스파이 생성
Mock과 마찬가지로 스파이는 @Spy 주석을 사용하여 생성 할 수 있습니다.
스파이 초기화의 경우에도 스파이를 초기화하기 위해 실제 테스트에서 스파이가 사용되기 전에 MockitoAnnotations.initMocks (this)가 호출되었는지 확인해야합니다.
통사론:
@Spy private transient ItemService spiedItemService = new ItemServiceImpl();
테스트중인 클래스 / 객체에 대해 모의 종속성을 삽입하는 방법은 무엇입니까?
다른 모의 종속성을 사용하여 테스트중인 클래스의 모의 객체를 생성하려는 경우 @InjectMocks 주석을 사용할 수 있습니다.
이것이 본질적으로하는 일은 @Mock (또는 @Spy) 주석으로 표시된 모든 개체가 계약자 또는 속성 주입으로 Object 클래스에 주입 된 다음 최종 Mocked 개체에서 상호 작용을 확인할 수 있다는 것입니다.
다시 언급 할 필요없이 @InjectMocks는 클래스의 새 개체를 만드는 것에 대한 속기이며 종속성의 모의 개체를 제공합니다.
예를 들어 이것을 이해합시다.
생성자 또는 속성 필드를 통해 삽입되는 종속성으로 DiscountCalculator 및 UserService가있는 PriceCalculator 클래스가 있다고 가정합니다.
따라서 Price 계산기 클래스에 대한 Mocked 구현을 만들기 위해 두 가지 접근 방식을 사용할 수 있습니다.
# 1) 만들기 PriceCalculator의 새 인스턴스 및 Mocked 종속성 주입
@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 주석은 실제로 아래 접근 방식 중 하나를 사용하여 모의 종속성을 주입하려고합니다.
- 생성자 기반 주입 – 테스트중인 클래스에 생성자를 활용합니다.
- Setter 메서드 기반 – 생성자가 없을 때 Mockito는 속성 setter를 사용하여 주입을 시도합니다.
- 필드 기반 – 위의 2 개를 사용할 수없는 경우 필드를 통해 직접 주입을 시도합니다.
팁 & 트릭
# 1) 동일한 메서드의 다른 호출에 대해 다른 스텁 설정 :
스텁 메서드가 테스트중인 메서드 내에서 여러 번 호출되는 경우 (또는 stubbed 메서드가 루프에 있고 매번 다른 출력을 반환하려는 경우) 매번 다른 스텁 응답을 반환하도록 Mock을 설정할 수 있습니다.
예를 들면 : 당신이 원한다고 가정하자 ItemService 3 회 연속 호출에 대해 다른 항목을 반환하고 테스트중인 메서드에서 Item1, Item2 및 Item3으로 선언 된 Items가있는 경우 아래 코드를 사용하여 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 }
#두) Mock을 통해 예외 발생 : 이는 예외를 발생시키는 다운 스트림 / 종속성을 테스트 / 확인하고 테스트중인 시스템의 동작을 확인하려는 경우 매우 일반적인 시나리오입니다. 그러나 Mock에서 예외를 발생 시키려면 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 ()과 같은 경기의 경우 다음 기사에서 다룰 것이므로 겁 먹지 마십시오. 그러나 본질적으로 특정 함수 인수없이 Integer 및 String 값을 각각 제공 할 수있는 유연성을 제공합니다.
웹 사이트에 코드를 삽입하는 방법
코드 예 – 스파이 및 모의
앞서 논의한 바와 같이 스파이와 목은 둘 다 테스트 복식의 유형이며 자체 용도가 있습니다.
스파이는 레거시 응용 프로그램을 테스트하는 데 유용하지만 (그리고 모의가 불가능한 경우), 잘 작성된 다른 모든 테스트 가능한 메서드 / 클래스의 경우 Mocks는 대부분의 단위 테스트 요구 사항을 충족합니다.
동일한 예의 경우 : Mocks for PriceCalculator-> 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 및 항목 서비스를 스텁 할 것입니다.
- UserService는 항상 loyaltyDiscountPercentage가 2로 설정된 CustomerProfile을 반환합니다.
- ItemService는 항상 basePrice가 100이고 해당 할인이 5 인 항목을 반환합니다.
- 위의 값을 사용하면 테스트중인 메서드에서 반환 된 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과 같다고 주장합니다.
이제 스파이를 사용하여 테스트를 작성해 보겠습니다.
ItemService를 감시하고 skuCode 2367로 호출 될 때마다 항상 basePrice 200과 해당 할인이 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);
이제 예 사용 가능한 Item 수량이 0 이었기 때문에 ItemService에 의해 throw되는 예외입니다. 예외를 throw하도록 mock을 설정합니다.
@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의 개념과이를 결합하여 효과적이고 유용한 단위 테스트를 만드는 방법을 설명하려고했습니다.
이러한 기술의 여러 조합을 사용하여 테스트중인 메서드의 적용 범위를 향상시키는 일련의 테스트를 얻을 수 있으므로 코드에 대한 높은 수준의 신뢰를 보장하고 코드가 회귀 버그에 대해 더 내성을 갖도록 할 수 있습니다.
소스 코드
인터페이스
할인 계산기
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) { } }
모델
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에서 제공하는 다양한 유형의 Matcher는 다음 튜토리얼에서 설명합니다.
추천 도서
- Mockito에서 제공하는 다양한 유형의 매처
- Mockito 자습서 : 단위 테스트에서 모의를위한 Mockito 프레임 워크
- Eclipse 용 Epoch Studio를 사용하여 Epochs 테스트 만들기
- 예제가 포함 된 Python DateTime 자습서
- 예제와 함께 Unix의 Cut 명령
- Unix Cat 명령 구문, 예제가있는 옵션
- 예제와 함께 MongoDB에서 커서 사용
- 예제가있는 Unix의 Ls 명령