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

unit test scaffolding

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



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 ошибочна, в основном потому, что она побуждает разработчиков к соединению методов тестирования.

Вот как мы можем улучшить наш тест и изолировать методы тестирования:
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;
  }
}
Это выглядит лучше сейчас? Мы еще не пришли, но теперь наши методы тестирования полностью изолированы. Если я хочу изменить один из них, я не буду затрагивать других, потому что передаю все параметры конфигурации в приватную статическую утилиту (!) Method folder ().

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

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

Гораздо лучше было бы использовать «поддельные» объекты вместо частных статических утилит. Вот как. Сначала мы создаем поддельный класс и помещаем его в src / main / java. Этот класс может использоваться в тестах, а также в производственном коде, если это необходимо (Fk для «поддельного»):
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);
  }
}
Вот как теперь будет выглядеть наш тест:
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? Разве это не более многоразово и расширяемо, чем утилитарные методы?

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

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

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