Статьи

Используйте Mockito для макетирования полей с автопроводкой

РЕДАКТИРОВАТЬ: полевые инъекции широко рассматриваются (включая меня сейчас), как плохая практика. Читайте здесь для получения дополнительной информации . Я бы предложил использовать инжектор конструктора вместо этого.

Внедрение зависимостей — очень мощная особенность контейнеров Inversion of Control, таких как Spring и EJB. Всегда полезно инкапсулировать введенные значения в частные поля. Но инкапсуляция автопроводных полей снижает тестируемость.

Мне нравится, как Mockito решил эту проблему, чтобы смоделировать поля с автопроводкой. Поясню это на примере. (В этом сообщении ожидается, что вы немного знакомы с синтаксисом Mockito, но он достаточно информативен.)

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

@Repository
public class OrderDao {
    public Order getOrder(int irderId){
        throw new UnsupportedOperationException("Fail is not mocked!");
    }
}

Вот вторая зависимость класса тестирования. Это также компонент Spring. Этот класс будет шпионить (частично издеваться) в тесте. Его метод calculatePriceForOrderбудет вызываться без изменений. Второй метод будет заглушен.

@Service
public class PriceService {
    public int getActualPrice(Item item){
        throw new UnsupportedOperationException("Fail is not mocked!");
    }

    public int calculatePriceForOrder(Order order){
        int orderPrice = 0;
        for (Item item : order.getItems()){
            orderPrice += getActualPrice(item);
        }
        return orderPrice;
    }
}

И вот класс под тестом. Это автоматически связывает зависимости выше.

@Service
public class OrderService {

    @Autowired
    private PriceService priceService;

    @Autowired
    private OrderDao orderDao;

    public int getOrderPrice(int orderId){
        Order order = orderDao.getOrder(orderId);
        return priceService.calculatePriceForOrder(order);
    }
}

Наконец, вот тестовый пример. Используются аннотации на уровне поля:

  • @InjectMocks— Создает экземпляр объекта тестирования и пытается внедрить поля, помеченные @Mockили @Spyзакрытые полями объекта тестирования.
  • @Mock — Создает фиктивный экземпляр поля, которое он аннотирует
  • @Spy — Создает шпион для аннотированного поля
public class OrderServiceTest {
    private static final int TEST_ORDER_ID = 15;
    private static final int TEST_SHOES_PRICE = 2;  
    private static final int TEST_SHIRT_PRICE = 1;

    @InjectMocks
    private OrderService testingObject;

    @Spy
    private PriceService priceService;

    @Mock
    private OrderDao orderDao;

    @BeforeMethod
    public void initMocks(){
        MockitoAnnotations.initMocks(this);
    }

    @Test
    public void testGetOrderService(){
        Order order = new Order(Arrays.asList(Item.SHOES, Item.SHIRT));
        Mockito.when(orderDao.getOrder(TEST_ORDER_ID)).thenReturn(order);

        //notice different Mockito syntax for spy
        Mockito.doReturn(TEST_SHIRT_PRICE).when(priceService).getActualPrice(Item.SHIRT);
        Mockito.doReturn(TEST_SHOES_PRICE).when(priceService).getActualPrice(Item.SHOES);

        //call testing method
        int actualOrderPrice = testingObject.getOrderPrice(TEST_ORDER_ID);

        Assert.assertEquals(TEST_SHIRT_PRICE + TEST_SHOES_PRICE, actualOrderPrice);
    }
}

Итак, что произойдет, когда вы запустите этот тест:

  1. Прежде всего, среда TestNG подбирает @BeforeMethodаннотацию и вызывает initMocksметод
  2. Этот метод вызывает специальный вызов Mockito ( MockitoAnnotations.initMocks(this)) для инициализации аннотированных полей. Без этого вызова эти объекты были бы null. Распространенная ошибка при таком подходе — забыть этот вызов.
  3. Когда все поля теста заполнены желаемыми значениями, вызывается test.

Этот пример не включает создание контекста Spring, и аннотации Spring приведены здесь только в качестве примеров для использования против производственного кода. Сам тест не включает никакой зависимости от Spring и игнорирует все его аннотации. Фактически, вместо этого можно использовать аннотации EJB, или они могут работать с простыми (не управляемыми IoC) частными полями.

Разработчики склонны считать MockitoAnnotations.initMocks(this)вызов ненужными накладными расходами. Но на самом деле это очень удобно, потому что он сбрасывает объект тестирования и повторно инициализирует макеты. Вы можете использовать это например

  • Если у вас есть различные методы тестирования, использующие одни и те же аннотированные экземпляры, чтобы гарантировать, что различные тестовые прогоны не используют одинаковое записанное поведение
  • Когда используются повторяющиеся / параметризованные тесты. Например, вы можете включить этот вызов в сам метод теста и получить шпионский объект в качестве параметра теста (как часть теста). Эта способность очень сексуальна в сочетании с @DataProviderфункцией TestNG (это будет объяснено в другом сообщении в блоге).

@Spy Аннотированный объект может быть создан двумя способами

  • Автоматически с помощью инфраструктуры Mockito, если есть конструктор по умолчанию (не параметризованный)
  • Или явно инициализирован (например, когда есть только конструктор не по умолчанию)

Аннотируемый объект тестирования также @InjectMocksможет быть явно инициализирован.

Пример исходного кода можно скачать с GitHub.