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

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