Проблема синхронизации моков всплывает всякий раз, когда обсуждаются тестовые стратегии. В основном — из-за дополнительной нагрузки, которую моки создают для девелоперов и а также рисков расхождения моков с реальными зависимостями.
Итак, каким способом нам дешевле всего обеспечить синхронизацию моков с реальными имплементациями?
Для синхронизации мы можем написать тест, который выполняет одни и те же проверки против мока и реальной имплементации.
Выглядит это как-то так (я пишу без DI, но с DI проще и правильнее):
OrderDaoTest работает против реального объекта с нижележащей мок- или реальной зависимостью, а ValidOrderDaoTest — против мока.
Если ValidOrderDataSource — это реальная база, то OrderDaoTest будет находиться в отдельном пакете и выполняться как часть интеграционных тестов, которые могут падать время от времени, при обновлении базы, например. Это не должно мешать CI\CD.
Если ValidOrderDataSource — это мок-база, то OrderDaoTest будет запускаться вместе с остальными юнит-тестами.
Поскольку синхронизация мока подразумевает тестирование реального класса, то для
реального класса придется мокать его нижележащие зависимости. Причем нижележащие мок-зависимости должны вести себя сообразно сценарию вышележащего мока. В нашем случае это
ValidOrderDataSource.
Если подумать, это имеет смысл — всякое утверждение о поведении вышестоящих классов неявно подразумевает некоторый сценарий в нижележащих. Если контроллер возвращает что-то из сервиса, то хорошо бы, чтобы база могла это обеспечить.
И наоборот, вышестоящие классы часто живут несбыточными представлениями о нижестоящих, поэтому не худо убрать лишние сценарии.
Рекурсия подсказывает, что для того, чтобы сделать мок верхнего уровня синхронизованным, нужно запустить синхронизацию всех нижележащих моков вплоть до внешних зависимостей.
Это делает спецификацию системы еще более прозрачной, поскольку более общие и абстрактные сценарии опираются на более частные.
Так же отметим, что есть моки, которые синхронизировать не надо. Т.е. у нас нет такой реальной имплементации, которую было бы необходимо кросс-тестировать. Это касается основных ошибочных сценариев. EmptyResultException_Datasource, например. Это сильно сокращает количество необходимых кросс-тестов.
Синхронизация безусловно нужна реальным внешним зависимостям, вроде очередей, внешних сервисов, баз данных — особенно в отношении данных, которые они берут и возвращают.
Если внешний сервис внезапно меняется, что нередко в девелопмент стадии, у нас нет никакого способа проверить его поведение, если не написать синхронизирующий тест.
С точки зрения трудоемкости. Сам по себе тест реального класса с какими-то произвольными мок-зависимостями у нас есть уже. По сравнению с несинхронизованными тестами нам нужно сделать несколько вещей.
Итак, каким способом нам дешевле всего обеспечить синхронизацию моков с реальными имплементациями?
Для синхронизации мы можем написать тест, который выполняет одни и те же проверки против мока и реальной имплементации.
Выглядит это как-то так (я пишу без DI, но с DI проще и правильнее):
public abstract class AbstractValidOrderDaoTest(){
Dao dao;
public abstract arrange();
@Test
public void whenValidOrderInDb_thenReturnValidOrder(){
arrange();
Order order = dao.retrieve();
assertNotNull(order);
assertNotNull(order.getCustomerName());
//и все остальные ассерты
}
}
public class ValidOrderDaoTest extends AbstractOrderDaoTest(){
@Override
public void arrange(){
dao = new FakeValidOrderDao();
}
}
public class OrderDaoTest extends AbstractOrderDaoTest(){
@Override
public void arrange(){
dao = new RealOrderDao(new ValidOrderDataSource(url, user, pwd));
}
}
OrderDaoTest работает против реального объекта с нижележащей мок- или реальной зависимостью, а ValidOrderDaoTest — против мока.
Если ValidOrderDataSource — это реальная база, то OrderDaoTest будет находиться в отдельном пакете и выполняться как часть интеграционных тестов, которые могут падать время от времени, при обновлении базы, например. Это не должно мешать CI\CD.
Если ValidOrderDataSource — это мок-база, то OrderDaoTest будет запускаться вместе с остальными юнит-тестами.
Поскольку синхронизация мока подразумевает тестирование реального класса, то для
реального класса придется мокать его нижележащие зависимости. Причем нижележащие мок-зависимости должны вести себя сообразно сценарию вышележащего мока. В нашем случае это
ValidOrderDataSource.
Если подумать, это имеет смысл — всякое утверждение о поведении вышестоящих классов неявно подразумевает некоторый сценарий в нижележащих. Если контроллер возвращает что-то из сервиса, то хорошо бы, чтобы база могла это обеспечить.
И наоборот, вышестоящие классы часто живут несбыточными представлениями о нижестоящих, поэтому не худо убрать лишние сценарии.
Рекурсия подсказывает, что для того, чтобы сделать мок верхнего уровня синхронизованным, нужно запустить синхронизацию всех нижележащих моков вплоть до внешних зависимостей.
Это делает спецификацию системы еще более прозрачной, поскольку более общие и абстрактные сценарии опираются на более частные.
Так же отметим, что есть моки, которые синхронизировать не надо. Т.е. у нас нет такой реальной имплементации, которую было бы необходимо кросс-тестировать. Это касается основных ошибочных сценариев. EmptyResultException_Datasource, например. Это сильно сокращает количество необходимых кросс-тестов.
Синхронизация безусловно нужна реальным внешним зависимостям, вроде очередей, внешних сервисов, баз данных — особенно в отношении данных, которые они берут и возвращают.
Если внешний сервис внезапно меняется, что нередко в девелопмент стадии, у нас нет никакого способа проверить его поведение, если не написать синхронизирующий тест.
С точки зрения трудоемкости. Сам по себе тест реального класса с какими-то произвольными мок-зависимостями у нас есть уже. По сравнению с несинхронизованными тестами нам нужно сделать несколько вещей.
- выделить act и assert в абстрактный тест
- сделать конкретный тест для мока
- поправить мок-зависимости в тесте реального класса
- при желании полноты рекурсивно повторить до упора во внешние зависимости.