Статьи

Встроенные поддельные объекты

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

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

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

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

В качестве примера возьмем интерфейс Region из jcabi-динамо (для краткости этот фрагмент и все остальные в этой статье упрощены):

1
2
3
public interface Region {
  Table table(String name);
}

Его метод table() возвращает экземпляр интерфейса Table , который имеет свои собственные методы:

1
2
3
4
5
public interface Table {
  Frame frame();
  Item put(Attributes attrs);
  Region region();
}

Интерфейс Frame , возвращаемый методом frame() , также имеет свои собственные методы. И так далее. Чтобы создать правильно смоделированный экземпляр интерфейса Region , обычно нужно создать дюжину других фиктивных объектов. С Mockito это будет выглядеть так:

1
2
3
4
5
6
7
8
9
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-динамо для управления данными в DynamoDB. Ваш класс может выглядеть примерно так:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
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-динамо . Просто посмотрите на его JavaDoc . Существует пакет с именем com.jcabi.dynamo.mock который содержит только поддельные классы, подходящие только для модульного тестирования.

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

Вот как будет выглядеть тест, когда используется поддельный класс MkRegion :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
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 в памяти). Хранилище будет готово для одной таблицы employees с name хеш-ключа и одним атрибутом salary .

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

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

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

Похожие сообщения

Вы также можете найти эти сообщения интересными:

Ссылка: Встроенные поддельные объекты от нашего партнера по JCG Егора Бугаенко в блоге About Programming .