среда, 24 мая 2017 г.

Создание фиктивных объектов

Хотя фиктивные объекты являются совершенными инструментами для модульного тестирования, насмешки над макетными фреймворками могут превратить ваши юнит-тесты в бесполезный беспорядок. 

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

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

Иерархия объектов

В качестве примера возьмем интерфейс Region из jcabi-dynamo (этот фрагмент и все остальные в этой статье упрощены для краткости):
public interface Region {
  Table table(String name);
}
Его метод table () возвращает экземпляр интерфейса Table, который имеет свои собственные методы:
public interface Table {
  Frame frame();
  Item put(Attributes attrs);
  Region region();
}
Интерфейсный кадр, возвращаемый методом frame (), также имеет свои собственные методы. И так далее. Чтобы создать правильно высмеянный экземпляр интерфейса Region, обычно создавали бы десяток других фиктивных объектов. С Mockito это будет выглядеть так:
public void testMe() {
  // many more lines here...
  Frame frame = Mockito.mock(Frame.class);
  Mockito.doReturn(...).when(frame).iterator();
  Table table = Mockito.mock(Table.class);
  Mockito.doReturn(frame).when(table).frame();
  Region region = Mockito.mock(Region.class);
  Mockito.doReturn(table).when(region).table(Mockito.anyString());
}
И все это - просто строительные леса перед фактическим тестированием.

Пример использования

Предположим, вы разрабатываете проект, который использует jcabi-dynamo для управления данными в DynamoDB. Ваш класс может выглядеть примерно так:
public class Employee {
  private final String name;
  private final Region region;
  public Employee(String empl, Region dynamo) {
    this.name = empl;
    this.region = dynamo;
  }
  public Integer salary() {
    return Integer.parseInt(
      this.region
        .table("employees")
        .frame()
        .where("name", this.name)
        .iterator()
        .next()
        .get("salary")
        .getN()
    );
  }
}
Вы можете себе представить, насколько сложно будет тестировать этот класс с помощью, например, Mockito. Во-первых, мы должны издеваться над интерфейсом Region. Затем мы должны издеваться над интерфейсом Table и убедиться, что он возвращается методом table (). Затем мы должны издеваться над интерфейсом Frame и т.д.

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

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

Поддельные классы


Решение заключается в создании поддельных классов и отправке их вместе с реальными классами. Это то, что делает jcabi-dynamo. Просто взгляните на его JavaDoc. Есть пакет под названием com.jcabi.dynamo.mock, который содержит только поддельные классы, подходящие только для модульного тестирования.

Хотя их единственной целью является оптимизация тестирования модулей, мы отправляем их вместе с кодом производства в том же пакете JAR.

Вот как будет выглядеть тест, когда используется поддельный класс MkRegion:
public class EmployeeTest {
  public void canFetchSalaryFromDynamoDb() {
    Region region = new MkRegion(
      new H2Data().with(
        "employees", new String[] {"name"},
        new String[] {"salary"}
      )
    );
    region.table("employees").put(
      new Attributes()
        .with("name", "Jeff")
        .with("salary", new AttributeValue().withN(50000))
    );
    Employee emp = new Employee("Jeff", region);
    assertThat(emp.salary(), equalTo(50000));
  }
}
Этот тест выглядит для меня очевидным. Во-первых, мы создаем поддельный регион DynamoDB, который работает поверх хранилища H2Data (в базе данных H2). Хранилище будет готово для отдельной таблицы сотрудников с именем ключа хэша и одним атрибутом оклада.

Затем мы помещаем запись в таблицу с хешем Jeff и зарплатой 50000.

Наконец, мы создаем экземпляр класса Employee и проверяем, как он получает зарплату от DynamoDB.

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

Кстати, отличная статья на ту же тему: tl; dw: Прекратите издеваться, начните тестирование Нед Батчелдер.

PS. Посмотрите на это, на очень похожую тему: «Издевательство над HTTP-сервером в Java».

Комментариев нет:

Отправить комментарий