Правила (Rules) JUnit позволяют влиять на поведение и исполнение всех тестов в классе. Правило представляет собой код, исполняющийся «вокруг» тестового метода, для каждого такого метода, позволяя расширять и переделывать поведение JUnit каким угодно образом.
Собственное правило
Возьмём код из примера с предположениями и перепишем его с использованием правил. С помощью правил мы будем выводить на консоль текущую системную локаль.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
public class LocaleWatcherRule implements TestRule {
@Override
public final Statement apply(
final Statement statement,
final Description description) {
return new Statement() {
@Override
public void evaluate() throws Throwable {
System.out.println("Starting: " + description.getDisplayName() + " with language " + System.getProperty("user.language"));
statement.evaluate();
}
};
}
}
|
Все классы правил реализуют интерфейс TestRule и его метод apply, который принимает завёрнутый в Statement тест и его описание в Description и возвращает Statement. Причём это может быть любой Statement, например с другим тестом внутри или вообще без теста. В примере я возвращаю Statement, который выводит значение языка для теста и исполняет сам тест.
Имзенение кода теста, для использования нового правила, тоже незначительное:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
public class LocaleLocalizedDateServiceTest {
private LocalizedDateService testedObject = new LocalizedDateService();
@Rule
public LocaleRule localeRule = new LocaleRule();
@Test
public void testFormatDate() throws Exception {
assumeThat(System.getProperty("user.language"), is("ru"));
Calendar date = Calendar.getInstance();
date.set(1961, 4, 21, 9, 7, 0);
assertThat(testedObject.formatDate(date.getTime()), is("Вс, май 21, '61"));
}
@Test
public void testFormatDateTW() throws Exception {
assumeThat(System.getProperty("user.language"), is("tw"));
Calendar date = Calendar.getInstance();
date.set(1961, 4, 21, 9, 7, 0);
assertThat(testedObject.formatDate(date.getTime()), is("星期日, 五月 21, '61"));
}
}
|
Запуск этого теста порадует нас строчками в логе:
1
2
|
Starting: testFormatDateTW(ru.easyjava.junit.LocaleLocalizedDateServiceTest) with language ru
Starting: testFormatDate(ru.easyjava.junit.LocaleLocalizedDateServiceTest) with language ru
|
Заготовки
JUnit предоставляет два базовых класса облегчающих написание собствнных правил: ExternalResource и TestWatcher.
ExternalResource
ExternalResource реализует @Before/@After парадигму — позволяет проводить какие-либо действия до вызова класса и после его вызова. Например, перепишем тест для StringUtils из статьи по основам JUnit с использованием ExternalResource:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
@Rule
public ExternalResource fixtures = new ExternalResource() {
@Override
protected void before() throws Throwable {
testString = "T:E:S:T";
testArray = new String[] {"T", "E", "S", "T"};
}
@Override
protected void after() {
testString=""; //Not very useful, but good enough for the example.
}
};
@ClassRule
public static ExternalResource classFixtures = new ExternalResource() {
@Override
protected void before() throws Throwable {
veryLargeString = new BigInteger(16384, new Random()).toString();
}
@Override
protected void after() {
veryLargeString = "";
}
};
|
Пример, конечно, надуманный, поэтому явного выигрыша мы здесь не получили. Но, если бы речь шла о какой-либо сложной инициализации, требующейся более чем в одном тесте, заключение её в (повторно используемое) правило облегчило бы написание тестов.
Само по себе использование класса ExternalResource вполне очевидно — метод before() вызывает перед вызовом каждого теста, метод after() — после. Однако, если правило имеет аннотацию @ClassRule, то методы before()/after() будут вызваны однократно, для всего класса. Действие @ClassRule, разумеется, распространяется на все правила JUnit.
TestWatcher
Базовый класс для правил, которые участвуют в исполнении тестов, но не вмешиваются в сам процесс тестирования. Например правило, записывающее текущую локаль теста, может быть переписано с использованием TestWatcher:
1
2
3
4
5
6
|
public class LocaleWatcherRule extends TestWatcher {
@Override
protected void starting(Description description) {
System.out.println("TestWatcher: Starting: " + description.getDisplayName() + " with language " + System.getProperty("user.language"));
}
}
|
Применим оба правила вместе:
1
2
3
4
|
@Rule
public TestRule localesRule = RuleChain
.outerRule(new LocaleRule())
.around(new LocaleWatcherRule());
|
RuleChain позволяет объединять несколько правил в цепочкe правил с заданным порядком выполнения. Если бы я написал:
1
2
3
4
5
|
@Rule
public LocaleRule localeRule = new LocaleRule();
@Rule
public LocaleWatcherRule watcherRule = new LocaleWatcherRule();
|
оба правила тоже выполнились бы, но в случайном порядке.
При запуске теста с обоими правилами несложно убедиться, что оба правила работают:
1
2
3
4
|
Starting: testFormatDateTW(ru.easyjava.junit.LocaleLocalizedDateServiceTest) with language ru
TestWatcher: Starting: testFormatDateTW(ru.easyjava.junit.LocaleLocalizedDateServiceTest) with language ru
Starting: testFormatDate(ru.easyjava.junit.LocaleLocalizedDateServiceTest) with language ru
TestWatcher: Starting: testFormatDate(ru.easyjava.junit.LocaleLocalizedDateServiceTest) with language ru
|
Правила, поставляемые с JUnit
JUnit содержит несколько заранее написанных правил:
- TemporaryFolder
- TestName
- ErrorCollector
- Verifier
- ExpectedException
- Timeout
TemporaryFolder
Предоставляет временную директорию для файлов, которая будет очищена после завершения теста (пример из официальной документации JUnit):
1
2
3
4
5
6
7
8
9
10
11
|
public static class HasTempFolder {
@Rule
public TemporaryFolder folder = new TemporaryFolder();
@Test
public void testUsingTempFolder() throws IOException {
File createdFile = folder.newFile("myfile.txt");
File createdFolder = folder.newFolder("subfolder");
// ...
}
}
|
TestName
Позволяет тестам узнавать собственное имя:
1
2
3
4
5
6
7
8
9
10
11
|
@Rule
public TestName nameRule = new TestName();
@Test
public void testIsEmpty() {
assertFalse("Non empty string claimed to be empty", StringUtils.isEmpty("TEST"));
assertTrue("Empty string not recognized", StringUtils.isEmpty(""));
assertTrue("Whitespaces not recognized", StringUtils.isEmpty(" "));
assertFalse("Test name could not be empty", StringUtils.isEmpty(nameRule.getMethodName()));
}
|
ErrorCollector
Собирает ошибки, возникающие в ходе выпонения теста и проваливает весь тест целиком. Полезен, когда проверяется поведение при каких-то независимых условиях и хочется проверить всё сразу:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
@Rule
public ErrorCollector collectorRule = new ErrorCollector();
@Test
public void testToDouble() {
collectorRule.checkThat(
StringUtils.toDouble("3.1415"),
is(closeTo(3.1415, 0.0001)));
collectorRule.checkThat(
StringUtils.toDouble(null),
is(Double.NaN));
}
|
Если при выполнении теста возникнут ошибки, они будут записаны, а тест продолжит своё выполнение. Список ошибок будет выведен после завершения теста.
Verifier
Базовый класс для ErrorCollectior , позволяет провалить успешный тест, если он не проходит проверку после выполнения (пример из официальной документации JUnit):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
private static String sequence;
public static class UsesVerifier {
@Rule
public Verifier collector = new Verifier() {
@Override
protected void verify() {
sequence += "verify ";
}
};
@Test
public void example() {
sequence += "test ";
}
}
@Test
public void verifierRunsAfterTest() {
sequence = "";
assertThat(testResult(UsesVerifier.class), isSuccessful());
assertEquals("test verify ", sequence);
}
|
ExpectedException
Улучшает поддержку исключений в JUnit. Это правило подробно рассмотрено в статье о тестировании исключений.
Помимо описанного там обычно применения, можно использовать это правило совместое с теориями, для перехвата исключений в них (хотя отсуствие expectedException у аннотации @Theory как бы говорит нам, что так лучше не делать):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
@RunWith(Theories.class)
public class StringConversionTest {
@DataPoint public static String A = "q";
@DataPoint
public static String B = "w";
//Next line will fail
//@DataPoint public static String C = "17";
@Rule
public ExpectedException thrown = ExpectedException.none();
@Theory
public void StringNotANumber(String value) {
thrown.expect(NumberFormatException.class);
Integer.valueOf(value);
}
}
|
Timeout
Этому правилу посвящена отдельная статья.
Код примеров на github, отдельно для предположений и теорий и отдельно для основных возможностей.