Статьи

Регулярные юнит-тесты и заглушки — Методы испытаний 4

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

… и я высказал идею, что любой класс, который не содержит никакой логики, на самом деле не нуждается в модульном тестировании. В это я включил свой объект доступа к данным, 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;
  }
}

В книге Майкла Фезера «Эффективная работа с устаревшим кодом» говорится, что тест не является модульным тестом, если:

  1. Это говорит с базой данных.
  2. Он общается через сеть.
  3. Это касается файловой системы.
  4. Вы должны сделать специальные вещи в вашей среде (например, редактирование файлов конфигурации), чтобы запустить его.

Чтобы соблюдать эти правила, вам необходимо изолировать тестируемый объект от остальной части вашей системы, и именно здесь вступают объекты-заглушки. Объекты-заглушки — это объекты, которые вводятся в ваш объект и используются для замены реальных объектов в тестовых ситуациях. Мартин Фаулер определяет заглушки в своем эссе
: «Заглушки
не заглушки» :

«Заглушки обеспечивают постоянные ответы на вызовы, сделанные во время теста, обычно вообще не реагируя ни на что, кроме того, что запрограммировано для теста. Заглушки могут также записывать информацию о вызовах, такую ​​как заглушка шлюза электронной почты, которая запоминает сообщения, которые она «отправила», или, возможно, только сколько сообщений она «отправила» ».


Выбрать слово для описания заглушек очень сложно, я мог бы выбрать
пустышку или
подделку , но есть типы заменяющих объектов, которые известны как пустышки или подделки — также описанные Мартином Фаулером:

  • Пустые объекты передаются, но никогда не используются. Обычно они просто используются для заполнения списков параметров.
  • Ложные объекты на самом деле имеют рабочие реализации, но обычно используют некоторые ярлыки, которые делают их непригодными для производства (хороший пример — база данных в памяти).

Тем не менее, я видел другие определения термина
поддельный объект, например, Рой Ошеров в своей книге «Искусство модульного
тестирования» определяет поддельный объект как:

  • Подделка — это общий термин, который можно использовать для описания заглушки или фиктивного объекта … потому что оба выглядят как реальный объект.

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

При тестировании 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.
     */
  }
}

Обратите внимание, что при написании модульного теста мы стремимся к ясности. Часто допускаемая ошибка заключается в том, что тестовый код уступает производственному коду, в результате чего он часто более грязный и неразборчивый. Рой Ошеров в «Искусстве модульного
тестирования» выдвигает идею, что тестовый код должен быть более читабельным, чем производственный код. Четкие тесты должны следовать следующим основным линейным шагам:

  1. Создайте тестируемый объект. В приведенном выше коде это делается в методе setUp (), поскольку я использую один и тот же тестируемый объект для всех (одного) тестов.
  2. Настройте тест. Это делается в тестовом методе testFindAddressWithStub (), поскольку данные, используемые в тесте, специфичны для этого теста.
  3. Запустить тест
  4. Снеси тест. Это гарантирует, что тесты изолированы друг от друга и могут быть запущены в любом порядке.

Использование упрощенной заглушки дает два преимущества изоляции AddressService от внешнего мира и быстрых тестов.

Насколько хрупок этот вид теста? Если ваши требования меняются, то тест и заглушка меняются — не так ли все-таки хрупко?

Для сравнения, мой следующий блог переписывает этот тест с использованием EasyMock.


1 Исходный код доступен на GitHub по адресу:

git: //github.com/roghughe/captaindebug.git

 

С http://www.captaindebug.com/2011/11/regular-unit-tests-and-stubs-testing.html