Мой последний блог был третьим в серии блогов, посвященных подходам к тестированию кода и обсуждению того, что вы делаете и не должны тестировать. Он основан на моем простом сценарии получения адреса из базы данных с использованием очень распространенного шаблона:
… и я высказал идею, что любой класс, который не содержит никакой логики, на самом деле не нуждается в модульном тестировании. В это я включил свой объект доступа к данным, DAO, и вместо этого предпочел интеграционное тестирование этого класса, чтобы убедиться, что он работает в сотрудничестве с базой данных.
Сегодняшний блог посвящен написанию обычного или классического модульного теста, который обеспечивает изоляцию объекта тестирования с использованием объектов-заглушек. Код, который мы будем тестировать, опять же, AddressService:
@Component public class AddressService { private static final Logger logger = LoggerFactory.getLogger(AddressService.class); private AddressDao addressDao; /** * Given an id, retrieve an address. Apply phony business rules. * * @param id * The id of the address object. */ public Address findAddress(int id) { logger.info("In Address Service with id: " + id); Address address = addressDao.findAddress(id); address = businessMethod(address); logger.info("Leaving Address Service with id: " + id); return address; } private Address businessMethod(Address address) { logger.info("in business method"); // Apply the Special Case Pattern (See MartinFowler.com) if (isNull(address)) { address = Address.INVALID_ADDRESS; } // Do some jiggery-pokery here.... return address; } private boolean isNull(Object obj) { return obj == null; } @Autowired @Qualifier("addressDao") void setAddressDao(AddressDao addressDao) { this.addressDao = addressDao; } }
В книге Майкла Фезера «Эффективная работа с устаревшим кодом» говорится, что тест не является модульным тестом, если:
- Это говорит с базой данных.
- Он общается через сеть.
- Это касается файловой системы.
- Вы должны сделать специальные вещи в вашей среде (например, редактирование файлов конфигурации), чтобы запустить его.
Чтобы соблюдать эти правила, вам необходимо изолировать тестируемый объект от остальной части вашей системы, и именно здесь вступают объекты-заглушки. Объекты-заглушки — это объекты, которые вводятся в ваш объект и используются для замены реальных объектов в тестовых ситуациях. Мартин Фаулер определяет заглушки в своем эссе
: «Заглушки не заглушки» :
«Заглушки обеспечивают постоянные ответы на вызовы, сделанные во время теста, обычно вообще не реагируя ни на что, кроме того, что запрограммировано для теста. Заглушки могут также записывать информацию о вызовах, такую как заглушка шлюза электронной почты, которая запоминает сообщения, которые она «отправила», или, возможно, только сколько сообщений она «отправила» ».
Выбрать слово для описания заглушек очень сложно, я мог бы выбрать
пустышку или
подделку , но есть типы заменяющих объектов, которые известны как пустышки или подделки — также описанные Мартином Фаулером:
- Пустые объекты передаются, но никогда не используются. Обычно они просто используются для заполнения списков параметров.
- Ложные объекты на самом деле имеют рабочие реализации, но обычно используют некоторые ярлыки, которые делают их непригодными для производства (хороший пример — база данных в памяти).
Тем не менее, я видел другие определения термина
поддельный объект, например, Рой Ошеров в своей книге «Искусство модульного
тестирования» определяет поддельный объект как:
- Подделка — это общий термин, который можно использовать для описания заглушки или фиктивного объекта … потому что оба выглядят как реальный объект.
… поэтому я, как и многие другие, склонен называть все заменяющие объекты либо
фиктивными, либо
заглушками, поскольку между ними есть разница, но об этом позже.
При тестировании AddressService нам нужно заменить реальный объект доступа к данным на объект-заглушку, и в этом случае он выглядит примерно так:
public class StubAddressDao implements AddressDao { private final Address address; public StubAddressDao(Address address) { this.address = address; } /** * @see com.captaindebug.address.AddressDao#findAddress(int) */ @Override public Address findAddress(int id) { return address; } }
Обратите внимание на простоту кода заглушки. Он должен быть легко читаемым, обслуживаемым, НЕ содержать никакой логики и нуждаться в самостоятельном модульном тестировании. После того, как код заглушки был написан, затем следует модульный тест:
public class ClassicAddressServiceWithStubTest { private AddressService instance; @Before public void setUp() throws Exception { /* Create the object to test */ /* Setup data that's used by ALL tests in this class */ instance = new AddressService(); } /** * Test method for * {@link com.captaindebug.address.AddressService#findAddress(int)}. */ @Test public void testFindAddressWithStub() { /* Setup the test data - stuff that's specific to this test */ Address expectedAddress = new Address(1, "15 My Street", "My Town", "POSTCODE", "My Country"); instance.setAddressDao(new StubAddressDao(expectedAddress)); /* Run the test */ Address result = instance.findAddress(1); /* Assert the results */ assertEquals(expectedAddress.getId(), result.getId()); assertEquals(expectedAddress.getStreet(), result.getStreet()); assertEquals(expectedAddress.getTown(), result.getTown()); assertEquals(expectedAddress.getPostCode(), result.getPostCode()); assertEquals(expectedAddress.getCountry(), result.getCountry()); } @After public void tearDown() { /* * Clear up to ensure all tests in the class are isolated from each * other. */ } }
Обратите внимание, что при написании модульного теста мы стремимся к ясности. Часто допускаемая ошибка заключается в том, что тестовый код уступает производственному коду, в результате чего он часто более грязный и неразборчивый. Рой Ошеров в «Искусстве модульного
тестирования» выдвигает идею, что тестовый код должен быть более читабельным, чем производственный код. Четкие тесты должны следовать следующим основным линейным шагам:
- Создайте тестируемый объект. В приведенном выше коде это делается в методе setUp (), поскольку я использую один и тот же тестируемый объект для всех (одного) тестов.
- Настройте тест. Это делается в тестовом методе testFindAddressWithStub (), поскольку данные, используемые в тесте, специфичны для этого теста.
- Запустить тест
- Снеси тест. Это гарантирует, что тесты изолированы друг от друга и могут быть запущены в любом порядке.
Использование упрощенной заглушки дает два преимущества изоляции AddressService от внешнего мира и быстрых тестов.
Насколько хрупок этот вид теста? Если ваши требования меняются, то тест и заглушка меняются — не так ли все-таки хрупко?
Для сравнения, мой следующий блог переписывает этот тест с использованием EasyMock.
1 Исходный код доступен на GitHub по адресу:
git: //github.com/roghughe/captaindebug.git
С http://www.captaindebug.com/2011/11/regular-unit-tests-and-stubs-testing.html