Статьи

Mockito Mock vs. Spy в весенних загрузочных тестах

Я встречал многих разработчиков, которые называют тесты «модульными тестами», когда они на самом деле являются интеграционными тестами. В сервисных слоях я видел тесты, называемые модульными тестами, но написанные с зависимостями от реального сервиса, такого как база данных, веб-сервис или некоторый сервер сообщений. Это часть интеграционного тестирования. Даже если вы просто используете Spring Context для автоматической привязки зависимостей, ваш тест является интеграционным тестом. Вместо использования реальных сервисов вы можете использовать макеты и шпионы Mockito, чтобы поддерживать свои тестовые юнит-тесты и избежать накладных расходов на проведение интеграционных тестов.

Это не значит, что интеграционные тесты плохие. Конечно, есть роль для интеграционных тестов. Они необходимы.

Но по сравнению с модульными тестами интеграционные тесты являются sloooowwwww. Очень медленно. Ваш типовой юнит-тест будет выполнен за доли секунды. Даже сложные модульные тесты на устаревшем оборудовании будут завершаться за доли секунды.

Интеграционные тесты, с другой стороны. занять несколько секунд, чтобы выполнить. Для запуска Spring Context требуется время. Требуется время, чтобы запустить базу данных H2 в памяти. Требуется время, чтобы установить соединение с базой данных.

Хотя это может показаться незначительным, оно становится экспоненциальным в большом проекте. Когда вы добавляете все больше и больше тестов, длина вашей сборки становится все длиннее и длиннее.

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

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

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

Они по-прежнему тестировали те же цели, но Mockito использовался для заполнения зависимости, обусловливая необходимость проведения интеграционного теста.

Например, Spring Boot упрощает тестирование с использованием базы данных H2 в памяти, используя JPA и репозитории, поставляемые Spring Data JPA.

Но почему бы не использовать Mockito для создания макета для репозитория Spring Data JPA?

Модульные тесты должны быть атомными, легкими, быстрыми и проводиться в виде отдельных блоков. Кроме того, модульные тесты в Spring не должны вызывать Spring Context. Я писал о различных типах тестов в моем предыдущем посте « Программное обеспечение для тестирования» .

Я уже написал серию постов о JUnit и пост о тестировании Spring MVC с Spring Boot 1.4: часть 1 . В последнем я рассмотрел модульное тестирование контроллеров в приложении Spring MVC.

Я считаю, что большинство ваших тестов должны быть модульными, а не интеграционными. Если вы пишете свой код в соответствии с принципами SOLID ООП , ваш код уже хорошо структурирован, чтобы принимать макеты Mockito.

В этой статье я объясню, как использовать Mockito для тестирования сервисного уровня приложения Spring Boot MVC. Если Mockito является новым для вас, я советую сначала прочитать мою публикацию Mocking in Unit Tests With Mockito .

Использование Mockito издевается и шпионовМокито издевается против шпионов

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

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

Есть несколько других типов тестовых двойников, таких как фиктивные объекты, поддельные объекты и заглушки. Если вы используете Спока, одним из моих любимых приемов было составить карту замыканий в качестве тестового двойника. (Одна из многих забавных вещей, которые вы можете сделать с Groovy!)

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

Шпионы, с другой стороны, предоставляют способ шпионить за реальным объектом. С помощью шпиона вы можете вызывать все реальные базовые методы объекта, все еще отслеживая каждое взаимодействие, так же, как если бы вы имитировали.

Все немного по-другому для насмешек Мокито и шпионов. Макет Mockito позволяет нам заглушить вызов метода. Это означает, что мы можем заглушить метод для возврата определенного объекта. Например, мы можем смоделировать репозиторий Spring Data JPA в классе обслуживания, чтобы заглушить метод getProduct () из репозитория для возврата объекта Product. Чтобы запустить тест, нам не нужно, чтобы база данных была запущена и работала — чистый модульный тест.

Шпион Mockito — частичная насмешка. Мы можем смоделировать часть объекта, заглушая несколько методов, в то время как реальные вызовы методов будут использоваться для других. Сказав это, мы можем заключить, что вызов метода у шпиона вызовет реальный метод, если только мы явно не заглушим метод и, следовательно, не будем использовать термин частичное макетирование.

Давайте рассмотрим макеты и шпионов в действии с приложением Spring Boot MVC.

Тестируемое приложение

Наше приложение содержит один объект JPA продукта. Операции CRUD выполняются на объекте с помощью ProductRepository с использованием CrudRepository, предоставленного Spring Data JPA. Если вы посмотрите на код, вы увидите, что все, что мы сделали, это расширили Spring Data JPA CrudRepository для создания нашего ProductRepository. В Spring Spring JPA предусмотрены реализации для управления объектами для самых распространенных операций, таких как сохранение объекта, его обновление, удаление или поиск по идентификатору.

Сервисный уровень разработан в соответствии с принципами SOLID . Мы использовали технику « Код в интерфейс », одновременно используя преимущества внедрения зависимостей. У нас есть интерфейс ProductService и реализация ProductServiceImpl. Именно этот класс ProductServiceImpl мы будем тестировать.

Вот код ProductServiceImpl.

ProductServiceImpl.java :

package guru.springframework.services;

import guru.springframework.domain.Product;
import guru.springframework.repositories.ProductRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class ProductServiceImpl implements ProductService {
    private final Logger logger = LoggerFactory.getLogger(this.getClass());
    private ProductRepository productRepository;

    @Autowired
    public void setProductRepository(ProductRepository productRepository) {
        this.productRepository = productRepository;
    }

    @Override
    public Iterable<Product> listAllProducts() {
        logger.debug("listAllProducts called");
        return productRepository.findAll();
    }

    @Override
    public Product getProductById(Integer id) {
        logger.debug("getProductById called");
        return productRepository.findOne(id);
    }

    @Override
    public Product saveProduct(Product product) {
        logger.debug("saveProduct called");
        return productRepository.save(product);
    }

    @Override
    public void deleteProduct(Integer id) {
        logger.debug("deleteProduct called");
        productRepository.delete(id);
    }
}

В классе ProductServiceImpl вы можете видеть, что ProductRepository является @Autowired in. Хранилище используется для выполнения операций CRUD. — фиктивный кандидат для тестирования ProductServiceImpl.

Тестирование с Mockito Mocks

Переходя к части тестирования, давайте рассмотрим метод getProductById () класса ProductServiceImpl. Для модульного тестирования функциональности этого метода нам нужно смоделировать внешние объекты Product и ProductRepository. Мы можем сделать это, используя метод Mockito mock () или аннотацию @Mockito. Мы будем использовать последний вариант, так как он удобен, когда вам нужно вводить много макетов.

Как только мы объявим макет с аннотацией @Mockito, нам также нужно его инициализировать. Инициализация инициализации происходит перед каждым методом тестирования. У нас есть два варианта — использовать средство запуска тестов JUnit, MockitoJUnitRunner или MockitoAnnotations.initMocks (). Оба являются эквивалентными решениями.

Наконец, вам нужно предоставить макеты для тестируемого объекта. Это можно сделать, вызвав метод setProductRepository () класса ProductServiceImpl или используя аннотацию @InjectMocks.

Следующий код создает макеты Mockito и устанавливает их для тестируемого объекта.

...
private ProductServiceImpl productServiceImpl;
@Mock
private ProductRepository productRepository;
@Mock
private Product product;

@Before
public void setupMock() {
    MockitoAnnotations.initMocks(this);
    productServiceImpl = new ProductServiceImpl();
    productServiceImpl.setProductRepository(productRepository);
}
...

Примечание . Поскольку мы используем стартовую зависимость Spring Boot Test, ядро ​​Mockito автоматически добавляется в наш проект. Поэтому в нашем Maven POM не требуется никакого дополнительного объявления зависимостей.

Как только наши макеты будут готовы, мы сможем начать использовать методы макета. Заглушка означает моделирование поведения метода фиктивного объекта. Мы можем заглушить метод в фиктивном объекте ProductRepository, настроив ожидание при вызове метода.

Например, мы можем заглушить метод findOne () макета ProductRepository, чтобы он возвращал Product при вызове. Затем мы вызываем метод, функциональность которого мы хотим проверить, с последующим утверждением, например так.

@Test
public void shouldReturnProduct_whenGetProductByIdIsCalled() throws Exception {
    // Arrange  
    when(productRepository.findOne(5)).thenReturn(product);
    // Act   
    Product retrievedProduct = productServiceImpl.getProductById(5);
    // Assert     
    assertThat(retrievedProduct, is(equalTo(product)));
}

Этот подход можно использовать для тестирования других методов ProductServiceImpl, оставляя в стороне deleteProduct (), для которого в качестве типа возврата используется void.

Чтобы протестировать deleteProduct (), мы отложим его, чтобы ничего не делать, затем вызовем deleteProduct () и, наконец, подтвердим, что метод delete () действительно был вызван.

Вот полный тестовый код для использования Mockito mocks:

ProductServiceImplMockTest.java:

package guru.springframework.services;

import guru.springframework.domain.Product;
import guru.springframework.repositories.ProductRepository;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

public class ProductServiceImplMockTest {
    private ProductServiceImpl productServiceImpl;
    @Mock
    private ProductRepository productRepository;
    @Mock
    private Product product;

    @Before
    public void setupMock() {
        MockitoAnnotations.initMocks(this);
        productServiceImpl = new ProductServiceImpl();
        productServiceImpl.setProductRepository(productRepository);
    }

    @Test
    public void shouldReturnProduct_whenGetProductByIdIsCalled() throws Exception {
        // Arrange     
        when(productRepository.findOne(5)).thenReturn(product);
        // Act    
        Product retrievedProduct = productServiceImpl.getProductById(5);
        // Assert    
        assertThat(retrievedProduct, is(equalTo(product)));
    }

    @Test
    public void shouldReturnProduct_whenSaveProductIsCalled() throws Exception {
        // Arrange       
        when(productRepository.save(product)).thenReturn(product);
        // Act         
        Product savedProduct = productServiceImpl.saveProduct(product);
        // Assert         
        assertThat(savedProduct, is(equalTo(product)));
    }

    @Test
    public void shouldCallDeleteMethodOfProductRepository_whenDeleteProductIsCalled() throws Exception {
        // Arrange         
        doNothing().when(productRepository).delete(5);
        ProductRepository my = Mockito.mock(ProductRepository.class);
        // Act         
        productServiceImpl.deleteProduct(5);
        // Assert         
        verify(productRepository, times(1)).delete(5);
    }
}

Примечание . Альтернативой doNothing () для создания метода void является использование doReturn (null).

Тестирование со шпионами Мокито

Мы протестировали наш ProductServiceImpl с макетами. Так зачем вообще нужны шпионы? На самом деле, нам не нужен один в этом случае использования.

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

До Mockito 1.8 шпионы Mockito не производили настоящих частичных издевательств. Однако после многих дискуссий и обсуждений, а также после нахождения допустимого варианта использования для частичной имитации в Mockito 1.8 была добавлена ​​поддержка частичной имитации.

Вы можете частично макетировать объекты, используя spies и метод callRealMethod (). Это означает, что без указания метода теперь вы можете вызвать базовый реальный метод макета, например так.

when(mock.someMethod()).thenCallRealMethod();

Будьте осторожны, чтобы реальная реализация была «безопасной» при использовании thenCallRealMethod (). Реальная реализация должна быть в состоянии работать в контексте вашего теста.

Другой подход к частичной насмешке — использовать шпиона. Как я упоминал ранее, все вызовы методов для шпиона являются реальными вызовами базового метода, если они не являются заглушками. Таким образом, вы также можете использовать шпион Mockito, чтобы частично высмеивать несколько тупых методов.

Вот код, предоставляющий шпион Mockito для нашего ProductServiceImpl.

ProductServiceImplSpyTest.java:

package guru.springframework.services;

import guru.springframework.domain.Product;
import guru.springframework.repositories.ProductRepository;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.Spy;
import org.mockito.runners.MockitoJUnitRunner;

@RunWith(MockitoJUnitRunner.class)
public class ProductServiceImplSpyTest {
    @Spy
    private ProductServiceImpl prodServiceSpy;
    @Mock
    private ProductRepository productRepository;
    @Mock
    private Product product;

    @Test(expected = NullPointerException.class)
    public void shouldThrowNullPointerException_whenGetProductByIdIsCalledWithoutContext() throws Exception {
        //Act
        Product retrievedProduct = prodServiceSpy.getProductById(5);
        //Assert
        assertThat(retrievedProduct, is(equalTo(product)));
    }

    public void shouldThrowNullPointerException_whenSaveProductIsCalledWithoutContext() throws Exception {
        //Arrange
        Mockito.doReturn(product).when(productRepository).save(product);
        //Act
        Product savedProduct = prodServiceSpy.saveProduct(product);
        //Assert
        assertThat(savedProduct, is(equalTo(product)));
    }

    @Test
    public void shouldVerifyThatGetProductByIdIsCalled() throws Exception {
        //Arrange
        Mockito.doReturn(product).when(prodServiceSpy).getProductById(5);
        //Act
        Product retrievedProduct = prodServiceSpy.getProductById(5);
        //Assert
        Mockito.verify(prodServiceSpy).getProductById(5);
    }

    @Test
    public void shouldVerifyThatSaveProductIsNotCalled() throws Exception {
        //Arrange
        Mockito.doReturn(product).when(prodServiceSpy).getProductById(5);
        //Act
        Product retrievedProduct = prodServiceSpy.getProductById(5);
        //Assert
        Mockito.verify(prodServiceSpy, never()).saveProduct(product);
    }
}

Обратите внимание, что в этом тестовом классе мы использовали MockitoJUnitRunner вместо MockitoAnnotations.initMocks () для наших аннотаций.

Для первого теста мы ожидали NullPointerException, потому что вызов getProductById () для шпиона вызовет фактический метод getProductById () ProductServiceImpl, а наши реализации репозитория еще не созданы.

Во втором тесте мы не ожидаем каких-либо исключений, так как используем метод save () в ProductRepository.

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

Вывод

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

Для частичной насмешки используйте его для тестирования сторонних API и устаревшего кода. Вам не потребуются частичные макеты для нового, проверенного и хорошо разработанного кода, который следует принципу единой ответственности . Другая проблема заключается в том, что стиль «(») нельзя использовать для шпионов. Кроме того, учитывая выбор между thenCallRealMethod для mock и spy, используйте первый, так как он легкий. Использование thenCallRealMethod для mock не создает фактический экземпляр объекта, а пустой экземпляр оболочки класса для отслеживания взаимодействий. Однако, если вы используете шпионов, вы создаете экземпляр объекта. И если говорить о шпионах, используйте их, если хотите, только если вы хотите изменить поведение небольшой части API, а затем полагаться в основном на реальные вызовы методов.

Код для этого поста доступен для скачивания здесь .