Правила (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, отдельно для предположений и теорий и отдельно для основных возможностей.