Статьи

Mocks And Stubs — Понимание двойников с Мокито

Введение

Я часто сталкиваюсь с тем, что команды, использующие фальшивые рамки, предполагают, что они насмехаются.
Они не знают, что Mocks — это всего лишь один из нескольких «тестовых двойников», которые Жерар Месарос классифицировал на xunitpatterns.com.

Важно понимать, что каждый тип двойного теста играет различную роль в тестировании. Точно так же, как вам нужно изучать различные шаблоны или рефакторинг, вы должны понимать примитивные роли каждого типа теста double. Затем они могут быть объединены для достижения ваших потребностей тестирования.
Я расскажу очень краткую историю о том, как появилась эта классификация, и как каждый из типов отличается.
Я сделаю это, используя несколько простых примеров из Mockito.

Очень краткая история

В течение многих лет люди писали облегченные версии компонентов системы, чтобы помочь с тестированием. В общем это называлось окурком. В 2000 году в статье «Эндотестирование: модульное тестирование с использованием фиктивных объектов» была представлена ​​концепция фиктивного объекта. С тех пор Месарос классифицировал Stubs, Mocks и ряд других типов тестовых объектов как Test Doubles.
На эту терминологию ссылался Мартин Фаулер в статье «Мок не тупики», и в сообществе Майкрософт она используется, как показано в разделе «Изучение континуума тестовых пар».
Ссылка на каждый из этих важных документов приведена в справочном разделе.

Категории тестовых пар

Диаграмма выше показывает наиболее часто используемые типы двойного теста. Следующий URL дает хорошую перекрестную ссылку на каждый из шаблонов и их функции, а также альтернативную терминологию.
http://xunitpatterns.com/Test%20Double.html

Mockito

Mockito — это тестовый шпионский фреймворк, который очень прост в освоении. С Mockito следует отметить, что ожидания любых фиктивных объектов не определяются перед тестом, как это иногда бывает в других фреймворках. Это приводит к более естественному стилю (ИМХО), когда начинается издевательство.
Следующие примеры приведены исключительно для демонстрации использования Mockito для реализации различных типов двойных тестов.

Существует гораздо больше конкретных примеров использования Mockito на сайте.
http://docs.mockito.googlecode.com/hg/latest/org/mockito/Mockito.html

Пробные пары с Мокито

Ниже приведены некоторые основные примеры использования Mockito для демонстрации роли каждого двойного теста, как это определено Месаросом.
Я включил ссылку на основное определение для каждого, чтобы вы могли получить больше примеров и полное определение.

Пустышка

http://xunitpatterns.com/Dummy%20Object.html
Это самый простой из всех двойных тестов. Это объект, который не имеет реализации, которая используется исключительно для заполнения аргументов вызовов методов, которые не имеют отношения к вашему тесту.

Например, приведенный ниже код использует много кода для создания клиента, что не важно для теста.
При проверке не имеет значения, какой клиент добавлен, если количество клиентов возвращается к единице.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
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());
}

Нам на самом деле наплевать на содержимое объекта customer — но это обязательно. Мы можем попробовать нулевое значение, но если код верен, вы ожидаете, что будет выдано какое-то исключение.

1
2
3
4
5
6
@Test(expected=Exception.class)
public void addNullCustomerTest() {
 Customer dummy = null;
 AddressBook addressBook = new AddressBook();
 addressBook.addCustomer(dummy);

Чтобы избежать этого, мы можем использовать простой манекен Mockito, чтобы получить желаемое поведение.

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

Именно этот простой код создает фиктивный объект для передачи в вызов.

1
Customer dummy = mock(Customer.class);

Не обманывайтесь ложным синтаксисом — здесь играют роль манекена, а не насмешки.
Его разделяет роль двойного теста, а не синтаксис, использованный для его создания.

Этот класс работает как простая замена классу клиента и делает тест очень легко читаемым.

Тестовая заглушка

http://xunitpatterns.com/Test%20Stub.html
Роль тестовой заглушки — возвращать контролируемые значения тестируемому объекту. Они описаны как косвенные входные данные для теста. Надеюсь, пример прояснит, что это значит.

Возьми следующий код

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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;
 }

TheSimplePricingService имеет один взаимодействующий объект — торговый репозиторий. Торговый репозиторий предоставляет торговые цены службе ценообразования с помощью метода getPriceForTrade.
Чтобы проверить логику бизнеса в SimplePricingService, нам нужно контролировать эти косвенные входы
т.е. входные данные, которые мы никогда не проходили в тесте.
Это показано ниже.

В следующем примере мы заглушаем PricingRepository для возврата известных значений, которые можно использовать для проверки бизнес-логики SimpleTradeService.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
@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());
}

Пример диверсанта

Существует два распространенных варианта тестовых заглушек: ответчик и диверсант.
Ответчики используются для проверки правильного пути, как в предыдущем примере.
Диверсант используется для проверки исключительного поведения, как показано ниже.

01
02
03
04
05
06
07
08
09
10
11
12
@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());
}

Макет объекта

http://xunitpatterns.com/Mock%20Object.html
Поддельные объекты используются для проверки поведения объекта во время теста. Под поведением объекта я подразумеваю, что мы проверяем правильность методов и путей к объекту при выполнении теста.
Это очень отличается от вспомогательной роли заглушки, которая используется для предоставления результатов всему, что вы тестируете.
В заглушке мы используем шаблон определения возвращаемого значения для метода.

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

В макете мы проверяем поведение объекта, используя следующую форму.

1
verify(listMock).add(s);

Вот простой пример, где мы хотим проверить, правильно ли проверяется новая сделка.
Вот основной код.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
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;
}

Тест ниже создает заглушку для торгового репозитория и макет для AuditService
Затем мы вызываем проверку на поддельном AuditService, чтобы убедиться, что TradeService вызывает его
метод logNewTrade правильно

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
@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);
}

Следующая строка выполняет проверку по ложному AuditService.

1
verify(auditService).logNewTrade(trade);

Этот тест позволяет нам показать, что служба аудита ведет себя корректно при создании сделки.

Тестовый шпион

http://xunitpatterns.com/Test%20Spy.html
Стоит взглянуть на приведенную выше ссылку для строгого определения тестового шпиона.
Однако в Mockito мне нравится использовать его, чтобы позволить вам обернуть реальный объект, а затем проверить или изменить его поведение для поддержки вашего тестирования.
Вот пример, где мы проверяем стандартное поведение List. Обратите внимание, что мы можем как проверить, что вызывается метод add, так и утверждать, что элемент был добавлен в список.

01
02
03
04
05
06
07
08
09
10
11
@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());
}

Сравните это с использованием фиктивного объекта, где может быть проверен только вызов метода. Поскольку мы только высмеиваем поведение списка, он не записывает, что элемент был добавлен, и возвращает значение по умолчанию, равное нулю, когда мы вызываем метод size ().

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
@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());
}
 
                     

Еще одна полезная функция testSpy — возможность заглушить ответные звонки. Когда это будет сделано, объект будет вести себя как обычно, пока не будет вызван метод с заглушкой.
В этом примере мы используем метод get, чтобы всегда генерировать исключение RuntimeException. Остальное поведение остается прежним.

1
2
3
4
5
6
7
8
@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);
}

В этом примере мы снова сохраняем базовое поведение, но изменяем метод size (), чтобы он возвращал 1 изначально и 5 для всех последующих вызовов.

01
02
03
04
05
06
07
08
09
10
11
12
13
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); 
}

Это довольно волшебство!

Поддельный Объект

http://xunitpatterns.com/Fake%20Object.html
Поддельные предметы обычно представляют собой предметы ручной работы или легкие предметы, используемые только для испытаний и не пригодные для производства. Хорошим примером будет база данных в памяти или фальшивый сервисный уровень.

Они, как правило, предоставляют гораздо больше функциональных возможностей, чем стандартные тестовые дубликаты, и, как таковые, обычно не являются кандидатами для реализации с использованием Mockito. Это не означает, что они не могут быть построены как таковые, просто, вероятно, не стоит реализовывать этот способ.

Ссылка: Мок и заглушки — Понимание двойных испытаний с Mockito от нашего партнера JCG Джона Доби в блоге Agile Engineering Techniques .