Статьи

Насмешка в юнит-тестах

Модульные тесты должны быть небольшими (атомными), легкими и быстрыми. Однако тестируемый объект может зависеть от других объектов. Может потребоваться взаимодействие с базой данных, связь с почтовым сервером или связь с веб-службой или очередью сообщений. Все эти сервисы могут быть недоступны во время модульного тестирования. Даже если они доступны, модульное тестирование тестируемого  объекта  вместе с его зависимостями может занять недопустимое количество времени. Что если…

  • Веб-сервис недоступен?
  • База данных недоступна для обслуживания?
  • Очередь сообщений тяжелая и медленная?

Все они побеждают целую цель модульных тестов: атомные, легкие и быстрые. Мы хотим, чтобы модульные тесты выполнялись за несколько миллисекунд. Если юнит-тесты медленные, ваши сборки становятся медленными, что влияет на производительность вашей команды разработчиков. Решение состоит в том, чтобы использовать mocking, способ обеспечить двойные тесты для тестируемых классов.

Если вы следовали  SOLID Принципам объектно-ориентированного программирования и использовали Spring Framework для  внедрения зависимостей , имитация становится естественным решением для модульного тестирования. Вам действительно не нужно подключение к базе данных. Вам просто нужен объект, который возвращает ожидаемый результат. Если вы написали тесно связанный код, вам будет сложно использовать макеты. Я видел много унаследованного кода, который нельзя было тестировать модульно, потому что он был так тесно связан с другими зависимыми объектами. Этот непроверяемый код не соответствовал принципам SOLID объектно-ориентированного программирования и не использовал Dependency Injection.

Макет объектов: Введение

В модульном тесте двойной тест — это замена зависимого компонента (соавтора) тестируемого объекта. Двойной тест обеспечивает тот же интерфейс, что и у соавтора. Это может быть не полный интерфейс, но для функциональности, необходимой для теста. Кроме того, двойной тест не должен вести себя точно так же, как соавтор. Цель состоит в том, чтобы имитировать соавтора, чтобы заставить тестируемый объект думать, что он действительно использует соавтора.

В зависимости от роли, которую играют во время тестирования, могут быть различные типы тестовых двойников, и один из них — фиктивный объект. Некоторые другие типы — фиктивный объект, поддельный объект и заглушка.

Что делает фиктивный объект отличным от других, так это то, что он использует проверку поведения. Это означает, что фиктивный объект проверяет, что  он (фиктивный объект) правильно используется тестируемым объектом . Если проверка прошла успешно, можно считать, что тестируемый объект будет правильно использовать реальный соавтор.

Тестовый сценарий

Для тестового сценария рассмотрим услугу заказа продукта. Клиент взаимодействует с DAO, чтобы выполнить процесс заказа продукта.

Начнем с  Product объекта домена и интерфейса DAO  ProductDao.

Product.java

package guru.springframework.unittest.mockito;

public class Product {

}

ProductDao.java

package guru.springframework.unittest.mockito;

public interface ProductDao {
int getAvailableProducts(Product product);
int orderProduct(Product product, int orderedQuantity);
}

Для примера я оставил  Product класс пустым. Но в реальных приложениях это, как правило, будет объект с состояниями, имеющими соответствующие методы получения и установки, наряду с любым реализованным поведением.

В  ProductDao интерфейсе мы объявили два метода:

  • getAvailableProducts() Метод возвращает количество доступного количества  Productпереданной ей.
  • orderProduct() Размещает заказ на продукт.

ProductServiceКласс , который мы будем писать дальше, что мы заинтересованы в   тестируемом объекте .

ProductService.java

package guru.springframework.unittest.mockito;

public class ProductService {
private ProductDao productDao;
public void setProductDao(ProductDao productDao) {
this.productDao = productDao;
}
public boolean buy(Product product, int orderedQuantity) throws InsufficientProductsException {
boolean transactionStatus=false;
int availableQuantity = productDao.getAvailableProducts(product);
if (orderedQuantity > availableQuantity) {
throw new InsufficientProductsException();
}
productDao.orderProduct(product, orderedQuantity);
transactionStatus=true;
return transactionStatus;
}

}

Составленный  ProductService выше класс состоит из  ProductDaoметода инициализации. В  buy() методе, мы позвонили  getAvailableProducts() из  ProductDao проверить , если достаточное количество указанного продукта доступно. Если нет, выдается исключение типа  InsufficientProductsException . Если достаточное количество доступно, мы называем  orderProduct() метод  ProductDao.

Теперь нам нужно провести модульное тестирование  ProductService . Но, как вы можете видеть,  ProductService  состоит из ProductDao , реализации которого у нас пока нет. Это может быть реализация Spring Data JPA, извлекающая данные из удаленной базы данных, или реализация, которая взаимодействует с веб-службой, на которой размещен облачный репозиторий — мы не знаем. Даже если у нас есть реализация, мы будем использовать ее позже во время интеграционного тестирования, одного из  типов тестирования программного обеспечения, которое  я написал ранее. Но сейчас нас  не интересуют какие-либо внешние реализации  в этом модульном тесте.

В модульных тестах нам не следует беспокоиться о том, что делает реализация. Мы хотим проверить, что наш ProductService  работает должным образом и что он может правильно использовать своих соавторов. Для этого мы будем издеваться над  ProductDao  и  Product,  используя Mockito.

Класс  ProductService  также генерирует пользовательское исключение, достаточное для исключения  продукта . Код класса исключения — это.

InsufficientProductsException.java

package guru.springframework.unittest.mockito;

public class InsufficientProductsException extends Exception {
private static final long serialVersionUID = 1L;
private String message = null;
public InsufficientProductsException() { super(); }
public InsufficientProductsException(String message) {
super(message);
this.message = message;
}
public InsufficientProductsException(Throwable cause)
{
super(cause);
}
@Override
public String toString() {
return message;
}
}

Использование Mockito

Mockito — это макет для модульных тестов, написанных на Java. Это платформа с открытым исходным кодом, доступная на github . Вы можете использовать Mockito с JUnit для создания и использования фиктивных объектов во время модульного тестирования. Чтобы начать использовать Mockito,  загрузите  файл JAR и поместите его в свой класс проекта. Если вы используете Maven, вам нужно добавить его зависимость в файл pom.xml, как показано ниже.

pom.xml

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>guru.springframework.unittest.quickstart</groupId>
  <artifactId>unittest</artifactId>
  <packaging>jar</packaging>
  <version>1.0-SNAPSHOT</version>
  <name>unittest</name>
  <url>http://maven.apache.org</url>
  <dependencies>
    <dependency>
     <groupId>junit</groupId>
     <artifactId>junit</artifactId>
     <version>4.12</version>
     <scope>test</scope>
    </dependency>
      <dependency>
          <groupId>org.hamcrest</groupId>
          <artifactId>hamcrest-library</artifactId>
          <version>1.3</version>
          <scope>test</scope>
      </dependency>
      <dependency>
          <groupId>org.mockito</groupId>
          <artifactId>mockito-all</artifactId>
          <version>1.9.5</version>
      </dependency>
  </dependencies>
</project>

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

Создание фиктивного объекта

Для нашего примера очевидно, что нам нужно издеваться над  ProductDao  и  Product . Самый простой способ — через вызовы  mock() метода  Mockito класса. Приятной особенностью Mockito является то, что он позволяет создавать фиктивные объекты как интерфейсов, так и классов без каких-либо явных объявлений.

MockCreationTest.java

package guru.springframework.unittest.mockito;

import org.junit.Before;
import org.junit.Test;
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
public class MockCreationTest {
    private ProductDao productDao;
    private Product product;
    @Before
    public void setupMock() {
        product = mock(Product.class);
        productDao = mock(ProductDao.class);
    }
    @Test
    public void testMockCreation(){
        assertNotNull(product);
        assertNotNull(productDao);
    }
}

Альтернативный способ — использовать  @Mock аннотацию. Когда вы используете его, вам нужно будет инициализировать макеты с помощью вызова  MockitoAnnotations.initMocks(this) или указать  MockitoJUnitRunner в  качестве тестера JUnit as  @RunWith(MockitoJUnitRunner.class).

MockCreationAnnotationTest.java

package guru.springframework.unittest.mockito;

import org.junit.Before;
import org.junit.Test;
import static org.junit.Assert.*;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

public class MockCreationAnnotationTest {
    @Mock
    private ProductDao productDao;
    @Mock
    private Product product;
    @Before
    public void setupMock() {
       MockitoAnnotations.initMocks(this);
    }
    @Test
    public void testMockCreation(){
        assertNotNull(product);
        assertNotNull(productDao);
    }
}

Stubbing

Заглушка означает моделирование поведения метода фиктивного объекта. Мы можем заглушить метод на фиктивном объекте, установив ожидание при вызове метода. Например, мы можем заглушить  getAvailableProducts() метод  ProductDao mock для возврата определенного значения при вызове метода.

. . .
@Test
public void testBuy() throws InsufficientProductsException {
    when(productDao.getAvailableProducts(product)).thenReturn(30);
    assertEquals(30,productDao.getAvailableProducts(product));
}
. . .

В  строке 4  указанного выше кода, мы гася  getAvailableProducts(product) из  ProductDao возвращению  30when() Метод представляет собой триггер для запуска Stubbing и  thenReturn() представляет собой действие триггера — который в примере кода , чтобы вернуть значение  30. В  строке 5  с  утверждением мы подтвердили, что заглушка выполнена должным образом.

Проверка

Наша цель — протестировать  ProductService , и теперь мы только высмеивали  Product  и  ProductDao  и вставляли getAvailableProducts ()  в  ProductDao .

Теперь мы хотим проверить поведение  buy() метода  ProductService. Во- первых, мы хотим , чтобы проверить , является ли это вызвав  orderProduct() из  ProductDao с требуемым набором параметров.

. . .
@Test
public void testBuy() throws InsufficientProductsException {
    when(productDao.getAvailableProducts(product)).thenReturn(30);
    assertEquals(30,productDao.getAvailableProducts(product));
    productService.buy(product, 5);
    verify(productDao).orderProduct(product, 5);
}
. . .

В  строке 6  мы назвали  buy() метод  ProductService проверки. В  строке 7 мы убедились, что orderProduct() метод  ProductDao фиктивного get вызывается с ожидаемым набором параметров (которые мы передали  buy()).

Наш тест пройден. Но еще не завершено. Мы также хотим проверить:

  • Число вызовов, выполненных для методаметод buy ()  вызывает  getAvailableProduct ()  хотя бы один раз.
  • Последовательность вызова метод buy () сначала вызывает  getAvailableProduct () , а затем  orderProduct () .
  • Проверка исключения метод buy () завершается неудачно с  InsufficientProductsException,  если переданное ему количество заказа превышает доступное количество, возвращаемое  getAvailableProduct () .
  • Поведение во время исключения метод buy () не вызывает  orderProduct (),  когда создается исключение InsufficientProductsException  .

Вот полный тестовый код.

ProductServiceTest.java

package guru.springframework.unittest.mockito;


import org.junit.Before;
import org.junit.Test;
import org.mockito.InOrder;
import static org.mockito.Mockito.*;
import org.mockito.Mock;

public class ProductServiceTest {
    private ProductService productService;
    private ProductDao productDao;
    private Product product;
    private int purchaseQuantity = 15;

    @Before
    public void setupMock() {
        productService = new ProductService();
        product = mock(Product.class);
        productDao = mock(ProductDao.class);
        productService.setProductDao(productDao);
    }

    @Test
    public void testBuy() throws InsufficientProductsException {
        int availableQuantity = 30;
        System.out.println("Stubbing getAvailableProducts(product) to return " + availableQuantity);
        when(productDao.getAvailableProducts(product)).thenReturn(availableQuantity);
        System.out.println("Calling ProductService.buy(product," + purchaseQuantity + ")");
        productService.buy(product, purchaseQuantity);
        System.out.println("Verifying ProductDao(product, " + purchaseQuantity + ") is called");
        verify(productDao).orderProduct(product, purchaseQuantity);
        System.out.println("Verifying getAvailableProducts(product) is called at least once");
        verify(productDao, atLeastOnce()).getAvailableProducts(product);
        System.out.println("Verifying order of method calls on ProductDao: First call getAvailableProducts() followed by orderProduct()");
        InOrder order = inOrder(productDao);
        order.verify(productDao).getAvailableProducts(product);
        order.verify(productDao).orderProduct(product, purchaseQuantity);



    }

    @Test(expected = InsufficientProductsException.class)
    public void purchaseWithInsufficientAvailableQuantity() throws InsufficientProductsException {
        int availableQuantity = 3;
        System.out.println("Stubbing getAvailableProducts(product) to return " + availableQuantity);
        when(productDao.getAvailableProducts(product)).thenReturn(availableQuantity);
        try {
            System.out.println("productService.buy(product" + purchaseQuantity + ") should throw InsufficientProductsException");
            productService.buy(product, purchaseQuantity);
        } catch (InsufficientProductsException e) {
            System.out.println("InsufficientProductsException has been thrown");
            verify(productDao, times(0)).orderProduct(product, purchaseQuantity);
            System.out.println("Verified orderProduct(product, " + purchaseQuantity + ") is not called");
            throw e;
        }
    }

}

Я уже объяснил исходный код тестового класса выше. Итак, мы начнем со  строки 36 — строки 38,  где мы использовали  inOrder() метод для проверки порядка вызова метода, который выполняется  buy() методом  ProductDao.

Затем мы написали  purchaseWithInsufficientAvailableQuantity() тестовый метод, чтобы проверить, будет ли  генерироваться  исключение InsufficientProductsException , как и ожидалось, когда сделан заказ с количеством, превышающим доступное количество. Мы также проверили в  строке 54,  что, если   метод будет сгенерирован InsufficientProductsException ,  orderProduct()метод не вызывается.

Результат теста таков.

-------------------------------------------------------
 T E S T S
-------------------------------------------------------

Running guru.springframework.unittest.mockito.ProductServiceTest
Stubbing getAvailableProducts(product) to return 30
Calling ProductService.buy(product,15)
Verifying ProductDao(product, 15) is called
Verifying getAvailableProducts(product) is called at least once
Verifying order of method calls on ProductDao: First call getAvailableProducts() followed by orderProduct()
Stubbing getAvailableProducts(product) to return 3
productService.buy(product15) should throw InsufficientProductsException 
InsufficientProductsException has been thrown
Verified orderProduct(product, 15) is not called
Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.077 sec

Резюме

Пересмешивание в модульном тестировании широко используется при разработке  корпоративных приложений с помощью Spring . Используя Mockito, вы можете заменить   компоненты @Autowired в классе, который вы хотите протестировать, на фиктивные объекты. Вы будете тестировать юнит-контроллеры, вводя фиктивные сервисы. Вы также будете настраивать службы для использования фиктивных DAO для модульного тестирования уровня обслуживания. Чтобы выполнить модульное тестирование уровня DAO, вы будете имитировать API базы данных. Список бесконечен — он зависит от типа приложения, над которым вы работаете, и от тестируемого объекта. Если вы следуете Принципу инверсии зависимости  и используете  Dependency Injection , насмешка становится легкой.

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