Статьи

Несколько мыслей о модульном тестировании лесов

Леон: Профессионал Люка Бессона

Леон: Профессионал Люка Бессона

Когда я начинаю повторять себя в методах модульного тестирования, создавая одни и те же объекты и подготавливая данные для запуска теста, я чувствую разочарование в своем дизайне. Длинные методы тестирования с большим количеством дублирования кода просто не выглядят правильно. Чтобы упростить и сократить их, в основном есть два варианта, по крайней мере в Java: 1) закрытые свойства, инициализированные с помощью @Before и @BeforeClass , и 2) приватные статические методы. Они оба выглядят анти-ООП для меня, и я думаю, что есть альтернатива. Позволь мне объяснить.

JUnit официально предлагает испытательный прибор :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public final class MetricsTest {
  private File temp;
  private Folder folder;
  @Before
  public void prepare() {
    this.temp = Files.createTempDirectory("test");
    this.folder = new DiscFolder(this.temp);
    this.folder.save("first.txt", "Hello, world!");
    this.folder.save("second.txt", "Goodbye!");
  }
  @After
  public void clean() {
    FileUtils.deleteDirectory(this.temp);
  }
  @Test
  public void calculatesTotalSize() {
    assertEquals(22, new Metrics(this.folder).size());
  }
  @Test
  public void countsWordsInFiles() {
    assertEquals(4, new Metrics(this.folder).wc());
  }
}

Я думаю, это очевидно, что делает этот тест. Во-первых, в prepare() он создает «тестовое устройство» типа Folder . Это используется во всех трех тестах в качестве аргумента для конструктора Metrics . Реальный класс, который здесь тестируется, это Metrics тогда как this.folder нам нужен для его тестирования.

Что не так с этим тестом? Есть одна серьезная проблема: связь между методами испытаний. Методы испытаний (и все тесты в целом) должны быть полностью изолированы друг от друга. Это означает, что изменение одного теста не должно влиять на другие. В этом примере это не так. Когда я хочу изменить countsWords() , я должен изменить внутреннюю структуру before() , что повлияет на другой метод в тестовом «классе».

При всем уважении к JUnit идея создания тестовых приборов в @Before и @After ошибочна, главным образом потому, что она побуждает разработчиков @After методы тестирования.

Вот как мы можем улучшить наши тесты и изолировать методы тестирования:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public final class MetricsTest {
  @Test
  public void calculatesTotalSize() {
    final File dir = Files.createTempDirectory("test-1");
    final Folder folder = MetricsTest.folder(
      dir,
      "first.txt:Hello, world!",
      "second.txt:Goodbye!"
    );
    try {
      assertEquals(22, new Metrics(folder).size());
    } finally {
      FileUtils.deleteDirectory(dir);
    }
  }
  @Test
  public void countsWordsInFiles() {
    final File dir = Files.createTempDirectory("test-2");
    final Folder folder = MetricsTest.folder(
      dir,
      "alpha.txt:Three words here",
      "beta.txt:two words"
      "gamma.txt:one!"
    );
    try {
      assertEquals(6, new Metrics(folder).wc());
    } finally {
      FileUtils.deleteDirectory(dir);
    }
  }
  private static Folder folder(File dir, String... parts) {
    Folder folder = new DiscFolder(dir);
    for (final String part : parts) {
      final String[] pair = part.split(":", 2);
      this.folder.save(pair[0], pair[1]);
    }
    return folder;
  }
}

Теперь это выглядит лучше? Мы еще не там, но теперь наши методы испытаний совершенно изолированы. Если я захочу изменить один из них, я не собираюсь влиять на другие, потому что я передаю все параметры конфигурации в folder() приватной статической утилиты (!) folder() .

Полезный метод, а? Да, это пахнет .

Главная проблема этого дизайна, хотя он и намного лучше предыдущего, заключается в том, что он не предотвращает дублирование кода между тестовыми «классами». Если мне понадобится аналогичное тестовое устройство типа Folder в другом тестовом примере, мне придется перенести этот статический метод туда. Или, что еще хуже, мне придется создать служебный класс. Да, в объектно-ориентированном программировании нет ничего хуже, чем служебные классы.

Намного лучше было бы использовать «поддельные» объекты вместо частных статических утилит. Вот как. Сначала мы создаем поддельный класс и помещаем его в src/main/java . Этот класс может быть использован в тестах, а также в производственном коде, если необходимо ( Fk для «подделки»):

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public final class FkFolder implements Folder, Closeable {
  private final File dir;
  private final String[] parts;
  public FkFolder(String... prts) {
    this(Files.createTempDirectory("test-1"), parts);
  }
  public FkFolder(File file, String... prts) {
    this.dir = file;
    this.parts = parts;
  }
  @Override
  public Iterable<File> files() {
    final Folder folder = new DiscFolder(this.dir);
    for (final String part : this.parts) {
      final String[] pair = part.split(":", 2);
      folder.save(pair[0], pair[1]);
    }
    return folder.files();
  }
  @Override
  public void close() {
    FileUtils.deleteDirectory(this.dir);
  }
}

Вот как теперь будет выглядеть наш тест:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public final class MetricsTest {
  @Test
  public void calculatesTotalSize() {
    final String[] parts = {
      "first.txt:Hello, world!",
      "second.txt:Goodbye!"
    };
    try (final Folder folder = new FkFolder(parts)) {
      assertEquals(22, new Metrics(folder).size());
    }
  }
  @Test
  public void countsWordsInFiles() {
    final String[] parts = {
      "alpha.txt:Three words here",
      "beta.txt:two words"
      "gamma.txt:one!"
    };
    try (final Folder folder = new FkFolder(parts)) {
      assertEquals(6, new Metrics(folder).wc());
    }
  }
}

Что вы думаете? Разве это не лучше, чем то, что предлагает JUnit? Разве это не более многократно и расширяемо, чем служебные методы?

Подводя итог, я считаю, что леса в модульном тестировании должны быть сделаны через поддельные объекты , которые поставляются вместе с рабочим кодом.

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