Когда я начинаю повторять себя в методах модульного тестирования, создавая одни и те же объекты и подготавливая данные для запуска теста, я чувствую разочарование в своем дизайне. Длинные методы тестирования с большим количеством дублирования кода просто не выглядят правильно. Чтобы упростить и сократить их, в основном есть два варианта, по крайней мере в 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 . |