Убедитесь, что расширение JUnit создает определенное исключение


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

public class DisallowUppercaseLetterAtBeginning implements BeforeEachCallback {

    @Override
    public void beforeEach(ExtensionContext context) {
        char c = context.getRequiredTestMethod().getName().charAt(0);
        if (Character.isUpperCase(c)) {
            throw new RuntimeException("test method names should start with lowercase.");
        }
    }
}

Теперь я хочу проверить, что мое расширение работает так, как ожидалось.

@ExtendWith(DisallowUppercaseLetterAtBeginning.class)
class MyTest {

    @Test
    void validTest() {
    }

    @Test
    void TestShouldNotBeCalled() {
        fail("test should have failed before");
    }
}

Как я могу написать тест, чтобы проверить, что попытка выполнить второй метод вызывает исключение RuntimeException с определенным сообщением?

3 4

3 ответа:

Другой подход может заключаться в использовании возможностей, предоставляемых новой структурой JUnit 5 - Jupiter.

Я поставил ниже код, который я тестировал с Java 1.8 на Eclipse Oxygen. Код страдает от недостатка элегантности и лаконичности, но, надеюсь, может послужить основой для построения надежного решения для вашего варианта использования мета-тестирования.

Обратите внимание, что это на самом деле как в JUnit 5 проверяется, я отсылаю вас к модульных тестов двигателя Юпитер на GitHub.

public final class DisallowUppercaseLetterAtBeginningTest { 
    @Test
    void testIt() {
        // Warning here: I checked the test container created below will
        // execute on the same thread as used for this test. We should remain
        // careful though, as the map used here is not thread-safe.
        final Map<String, TestExecutionResult> events = new HashMap<>();

        EngineExecutionListener listener = new EngineExecutionListener() {
            @Override
            public void executionFinished(TestDescriptor descriptor, TestExecutionResult result) {
                if (descriptor.isTest()) {
                    events.put(descriptor.getDisplayName(), result);
                }
                // skip class and container reports
            }

            @Override
            public void reportingEntryPublished(TestDescriptor testDescriptor, ReportEntry entry) {}
            @Override
            public void executionStarted(TestDescriptor testDescriptor) {}
            @Override
            public void executionSkipped(TestDescriptor testDescriptor, String reason) {}
            @Override
            public void dynamicTestRegistered(TestDescriptor testDescriptor) {}
        };

        // Build our test container and use Jupiter fluent API to launch our test. The following static imports are assumed:
        //
        // import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass
        // import static org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder.request

        JupiterTestEngine engine = new JupiterTestEngine();
        LauncherDiscoveryRequest request = request().selectors(selectClass(MyTest.class)).build();
        TestDescriptor td = engine.discover(request, UniqueId.forEngine(engine.getId())); 

        engine.execute(new ExecutionRequest(td, listener, request.getConfigurationParameters()));

        // Bunch of verbose assertions, should be refactored and simplified in real code.
        assertEquals(new HashSet<>(asList("validTest()", "TestShouldNotBeCalled()")), events.keySet());
        assertEquals(Status.SUCCESSFUL, events.get("validTest()").getStatus());
        assertEquals(Status.FAILED, events.get("TestShouldNotBeCalled()").getStatus());

        Throwable t = events.get("TestShouldNotBeCalled()").getThrowable().get();
        assertEquals(RuntimeException.class, t.getClass());
        assertEquals("test method names should start with lowercase.", t.getMessage());
}

Хотя немного многословный, одним из преимуществ этого подхода является то, что он не требует насмешек и выполнения тестов в том же контейнере JUnit, который будет использоваться позже для реальных модульных тестов.

С небольшим количеством очистки, гораздо более читаемый код достижим. Опять же, источники юнит-Юпитер могут быть отличным источником вдохновения.

Если расширение создает исключение, то метод @Test мало что может сделать, так как тестовый бегун никогда не достигнет метода @Test. В этом случае, я думаю, вы должны проверить расширение вне его использования в нормальном тестовом потоке, т. е. пусть расширение будет SUT. Для расширения, указанного в вашем вопросе, тест может быть примерно таким:

@Test
public void willRejectATestMethodHavingANameStartingWithAnUpperCaseLetter() throws NoSuchMethodException {
    ExtensionContext extensionContext = Mockito.mock(ExtensionContext.class);
    Method method = Testable.class.getMethod("MethodNameStartingWithUpperCase");

    Mockito.when(extensionContext.getRequiredTestMethod()).thenReturn(method);

    DisallowUppercaseLetterAtBeginning sut = new DisallowUppercaseLetterAtBeginning();

    RuntimeException actual =
            assertThrows(RuntimeException.class, () -> sut.beforeEach(extensionContext));
    assertThat(actual.getMessage(), is("test method names should start with lowercase."));
}

@Test
public void willAllowTestMethodHavingANameStartingWithAnLowerCaseLetter() throws NoSuchMethodException {
    ExtensionContext extensionContext = Mockito.mock(ExtensionContext.class);
    Method method = Testable.class.getMethod("methodNameStartingWithLowerCase");

    Mockito.when(extensionContext.getRequiredTestMethod()).thenReturn(method);

    DisallowUppercaseLetterAtBeginning sut = new DisallowUppercaseLetterAtBeginning();

    sut.beforeEach(extensionContext);

    // no exception - good enough
}

public class Testable {
    public void MethodNameStartingWithUpperCase() {

    }
    public void methodNameStartingWithLowerCase() {

    }
}

Однако ваш вопрос предполагает, что приведенное выше расширение является только примером, так что, в более общем плане; если ваше расширение имеет побочный эффект (например, устанавливает что-то в адресуемом контексте, заполняет системное свойство и т. д.), то ваш метод @Test может утверждать, что этот побочный эффект присутствует. Например:

public class SystemPropertyExtension implements BeforeEachCallback {

    @Override
    public void beforeEach(ExtensionContext context) {
        System.setProperty("foo", "bar");
    }
}

@ExtendWith(SystemPropertyExtension.class)
public class SystemPropertyExtensionTest {

    @Test
    public void willSetTheSystemProperty() {
        assertThat(System.getProperty("foo"), is("bar"));
    }
}

Этот подход имеет преимущество бокового шага потенциально неудобных шагов настройки: создание ExtensionContext и заполнение его состоянием, требуемым вашим тестом, но это может стоить ограничения покрытия теста, так как вы действительно можете протестировать только один результат. И, конечно, это возможно только в том случае, если расширение имеет побочный эффект, который может быть устранен в тестовом случае, использующем расширение.

Таким образом, на практике, я подозреваю, что вам может понадобиться комбинация этих подходов; для некоторых расширений расширение может быть SUT, а для других расширение может быть проверено путем утверждения против его побочного эффекта(ов).

После того, как я попробовал решения в ответах и вопрос, связанный в комментариях, я получил решение с помощью запуска платформы JUnit.

class DisallowUppercaseLetterAtBeginningTest {

    @Test
    void should_succeed_if_method_name_starts_with_lower_case() {
        TestExecutionSummary summary = runTestMethod(MyTest.class, "validTest");

        assertThat(summary.getTestsSucceededCount()).isEqualTo(1);
    }

    @Test
    void should_fail_if_method_name_starts_with_upper_case() {
        TestExecutionSummary summary = runTestMethod(MyTest.class, "InvalidTest");

        assertThat(summary.getTestsFailedCount()).isEqualTo(1);
        assertThat(summary.getFailures().get(0).getException())
                .isInstanceOf(RuntimeException.class)
                .hasMessage("test method names should start with lowercase.");
    }

    private TestExecutionSummary runTestMethod(Class<?> testClass, String methodName) {
        SummaryGeneratingListener listener = new SummaryGeneratingListener();

        LauncherDiscoveryRequest request = request().selectors(selectMethod(testClass, methodName)).build();
        LauncherFactory.create().execute(request, listener);

        return listener.getSummary();
    }

    @ExtendWith(DisallowUppercaseLetterAtBeginning.class)
    static class MyTest {

        @Test
        void validTest() {
        }

        @Test
        void InvalidTest() {
            fail("test should have failed before");
        }
    }
}

Сам JUnit не будет работать MyTest, потому что это внутренний класс без @Nested. Таким образом, в процессе сборки нет неудачных тестов.

Обновление

Сам JUnit не будет работать MyTest, потому что это внутренний класс без @Nested. Таким образом, во время сборки нет неудачных тестов процесс.

Это не совсем правильно. Сам JUnit также будет работать MyTest, например, если" выполнить все тесты " запускается в IDE или в сборке Gradle.

Причина, по которой MyTest не был выполнен, заключается в том, что я использовал Maven и протестировал его с mvn test. Maven использует плагин maven Surefire для выполнения тестов. Этот плагин имеет конфигурацию по умолчанию , которая исключает все вложенные классы, такие как MyTest.

Смотрите также этот ответ о "выполнении тестов из внутренних классов через Maven" и связанные вопросы в комментариях.