Skip to content

Commit

Permalink
Add support for ExpectedException
Browse files Browse the repository at this point in the history
Issues: #433, #169
  • Loading branch information
marcphilipp committed Nov 19, 2016
1 parent b1942ea commit 031463b
Show file tree
Hide file tree
Showing 7 changed files with 259 additions and 10 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,18 @@
import org.junit.Rule;
import org.junit.jupiter.api.extension.AfterEachCallback;
import org.junit.jupiter.api.extension.BeforeEachCallback;
import org.junit.jupiter.api.extension.TestExecutionExceptionHandler;
import org.junit.jupiter.api.extension.TestExtensionContext;
import org.junit.jupiter.migrationsupport.rules.adapter.AbstractTestRuleAdapter;
import org.junit.jupiter.migrationsupport.rules.adapter.GenericBeforeAndAfterAdvice;
import org.junit.jupiter.migrationsupport.rules.member.RuleAnnotatedMember;
import org.junit.platform.commons.meta.API;
import org.junit.platform.commons.util.ExceptionUtils;
import org.junit.rules.TestRule;

@API(Experimental)
public abstract class AbstractTestRuleSupport implements BeforeEachCallback, AfterEachCallback {
public abstract class AbstractTestRuleSupport
implements BeforeEachCallback, TestExecutionExceptionHandler, AfterEachCallback {

private final Class<Rule> annotationType = Rule.class;
private final Class<? extends TestRule> ruleType;
Expand Down Expand Up @@ -57,12 +60,24 @@ public void beforeEach(TestExtensionContext context) throws Exception {
this.invokeAppropriateMethodOnRuleAnnotatedMembers(context, GenericBeforeAndAfterAdvice::before);
}

@Override
public void handleTestExecutionException(TestExtensionContext context, Throwable throwable) throws Throwable {
this.invokeAppropriateMethodOnRuleAnnotatedMembers(context, advice -> {
try {
advice.handleTestExecutionException(throwable);
}
catch (Throwable t) {
throw ExceptionUtils.throwAsUncheckedException(t);
}
});
}

@Override
public void afterEach(TestExtensionContext context) throws Exception {
this.invokeAppropriateMethodOnRuleAnnotatedMembers(context, GenericBeforeAndAfterAdvice::after);
}

protected void invokeAppropriateMethodOnRuleAnnotatedMembers(TestExtensionContext context,
private void invokeAppropriateMethodOnRuleAnnotatedMembers(TestExtensionContext context,
Consumer<GenericBeforeAndAfterAdvice> methodCaller) {
List<Member> members = this.findRuleAnnotatedMembers(context.getTestInstance());

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,20 +23,25 @@
/**
* This class-level annotation turns on native JUnit 4 rule support
* within JUnit Jupiter.
* Currently, rules of type {@code Verifier} and
* {@code ExternalResource} are supported.
* {@code EnableRuleMigrationSupport} is a meta-annotation which
* includes both supported extensions: {@code VerifierSupport} and
* {@code ExternalResourceSupport}.
*
* <p>Currently, rules of type {@code Verifier},
* {@code ExternalResource} as well as {@code ExpectedException} rules are
* supported.
*
* <p>{@code EnableRuleMigrationSupport} is a meta-annotation which
* includes all supported extensions: {@link VerifierSupport},
* {@link ExternalResourceSupport}, and {@link ExpectedExceptionSupport}.
*
* @since 5.0
* @see ExternalResourceSupport
* @see VerifierSupport
* @see ExpectedExceptionSupport
*/
@Target({ ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@API(Experimental)
@ExtendWith(ExternalResourceSupport.class)
@ExtendWith(VerifierSupport.class)
@ExtendWith(ExpectedExceptionSupport.class)
public @interface EnableRuleMigrationSupport {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/*
* Copyright 2015-2016 the original author or authors.
*
* All rights reserved. This program and the accompanying materials are
* made available under the terms of the Eclipse Public License v1.0 which
* accompanies this distribution and is available at
*
* http://www.eclipse.org/legal/epl-v10.html
*/

package org.junit.jupiter.migrationsupport.rules;

import static java.lang.Boolean.FALSE;
import static java.lang.Boolean.TRUE;
import static org.junit.platform.commons.meta.API.Usage.Experimental;

import java.util.function.Function;

import org.junit.jupiter.api.extension.AfterEachCallback;
import org.junit.jupiter.api.extension.ExtensionContext.Namespace;
import org.junit.jupiter.api.extension.ExtensionContext.Store;
import org.junit.jupiter.api.extension.TestExecutionExceptionHandler;
import org.junit.jupiter.api.extension.TestExtensionContext;
import org.junit.jupiter.migrationsupport.rules.adapter.AbstractTestRuleAdapter;
import org.junit.jupiter.migrationsupport.rules.adapter.ExpectedExceptionAdapter;
import org.junit.jupiter.migrationsupport.rules.member.RuleAnnotatedMember;
import org.junit.platform.commons.meta.API;
import org.junit.rules.ExpectedException;

/**
* This {@code Extension} provides native support for the
* {@code ExpectedException} rule from JUnit 4.
*
* <p>By using this class-level extension on a test class,
* {@code ExpectedException} can continued to be used.
*
* <p>However, you should rather switch to
* {@link org.junit.jupiter.api.Assertions#assertThrows} for new code.
*
* @since 5.0
* @see org.junit.jupiter.api.Assertions#assertThrows
* @see ExpectedException
* @see org.junit.rules.TestRule
* @see org.junit.Rule
*/
@API(Experimental)
public class ExpectedExceptionSupport implements AfterEachCallback, TestExecutionExceptionHandler {

private static final String EXCEPTION_WAS_HANDLED = "exceptionWasHandled";

private final Function<RuleAnnotatedMember, AbstractTestRuleAdapter> adapterGenerator = ExpectedExceptionAdapter::new;

private final AbstractTestRuleSupport fieldSupport = new TestRuleFieldSupport(this.adapterGenerator,
ExpectedException.class);
private final AbstractTestRuleSupport methodSupport = new TestRuleMethodSupport(this.adapterGenerator,
ExpectedException.class);

@Override
public void handleTestExecutionException(TestExtensionContext context, Throwable throwable) throws Throwable {
getStore(context).put(EXCEPTION_WAS_HANDLED, TRUE);
this.fieldSupport.handleTestExecutionException(context, throwable);
this.methodSupport.handleTestExecutionException(context, throwable);
}

@Override
public void afterEach(TestExtensionContext context) throws Exception {
boolean exceptionWasHandled = getStore(context).getOrComputeIfAbsent(EXCEPTION_WAS_HANDLED, key -> FALSE,
Boolean.class);
if (!exceptionWasHandled) {
this.fieldSupport.afterEach(context);
this.methodSupport.afterEach(context);
}
}

private Store getStore(TestExtensionContext context) {
return context.getStore(Namespace.create(ExpectedExceptionSupport.class, context.getUniqueId()));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,19 @@ private void failIfAdapteeClassIsNotAssignableFromTargetClass() {
throw new IllegalStateException(this.adapteeClass + " is not assignable from " + this.target.getClass());
}

protected void executeMethod(String name) {
protected Object executeMethod(String name) {
return executeMethod(name, new Class<?>[0]);
}

protected Object executeMethod(String name, Class<?>[] parameterTypes, Object... arguments) {
try {
Method method = target.getClass().getDeclaredMethod(name);
Method method = target.getClass().getDeclaredMethod(name, parameterTypes);
method.setAccessible(true);
ReflectionUtils.invokeMethod(method, target);
return ReflectionUtils.invokeMethod(method, target, arguments);
}
catch (NoSuchMethodException | SecurityException exception) {
LOG.warning(exception.getMessage());
return null;
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* Copyright 2015-2016 the original author or authors.
*
* All rights reserved. This program and the accompanying materials are
* made available under the terms of the Eclipse Public License v1.0 which
* accompanies this distribution and is available at
*
* http://www.eclipse.org/legal/epl-v10.html
*/

package org.junit.jupiter.migrationsupport.rules.adapter;

import static java.lang.Boolean.TRUE;
import static org.junit.platform.commons.meta.API.Usage.Internal;

import org.junit.jupiter.migrationsupport.rules.member.RuleAnnotatedMember;
import org.junit.platform.commons.meta.API;
import org.junit.rules.ExpectedException;

@API(Internal)
public class ExpectedExceptionAdapter extends AbstractTestRuleAdapter {

public ExpectedExceptionAdapter(RuleAnnotatedMember annotatedMember) {
super(annotatedMember, ExpectedException.class);
}

@Override
public void handleTestExecutionException(Throwable cause) throws Throwable {
executeMethod("handleException", new Class<?>[] { Throwable.class }, cause);
}

@Override
public void after() {
if (TRUE.equals(executeMethod("isAnyExceptionExpected"))) {
executeMethod("failDueToMissingException");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ public interface GenericBeforeAndAfterAdvice {
default void before() {
}

default void handleTestExecutionException(Throwable cause) throws Throwable {
}

default void after() {
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
/*
* Copyright 2015-2016 the original author or authors.
*
* All rights reserved. This program and the accompanying materials are
* made available under the terms of the Eclipse Public License v1.0 which
* accompanies this distribution and is available at
*
* http://www.eclipse.org/legal/epl-v10.html
*/

package org.junit.jupiter.migrationsupport.rules;

import static org.assertj.core.api.Assertions.allOf;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass;
import static org.junit.platform.engine.test.event.ExecutionEventConditions.event;
import static org.junit.platform.engine.test.event.ExecutionEventConditions.finishedSuccessfully;
import static org.junit.platform.engine.test.event.ExecutionEventConditions.finishedWithFailure;
import static org.junit.platform.engine.test.event.ExecutionEventConditions.test;
import static org.junit.platform.engine.test.event.TestExecutionResultConditions.isA;
import static org.junit.platform.engine.test.event.TestExecutionResultConditions.message;
import static org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder.request;

import java.io.IOException;

import org.junit.Rule;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.engine.JupiterTestEngine;
import org.junit.platform.engine.ExecutionRequest;
import org.junit.platform.engine.TestDescriptor;
import org.junit.platform.engine.UniqueId;
import org.junit.platform.engine.test.event.ExecutionEventRecorder;
import org.junit.platform.launcher.LauncherDiscoveryRequest;
import org.junit.rules.ExpectedException;

class ExpectedExceptionSupportTests {

@Test
void expectedExceptionIsProcessedCorrectly() {
ExecutionEventRecorder eventRecorder = executeTestsForClass(ExpectedExceptionTestCase.class);

assertEquals(4, eventRecorder.getTestStartedCount(), "# tests started");
assertEquals(1, eventRecorder.getTestSuccessfulCount(), "# tests succeeded");
assertEquals(0, eventRecorder.getTestAbortedCount(), "# tests aborted");
assertEquals(3, eventRecorder.getTestFailedCount(), "# tests failed");

assertThat(eventRecorder.getSuccessfulTestFinishedEvents()).have(
event(test("correctExceptionExpectedThrown"), finishedSuccessfully()));

assertThat(eventRecorder.getFailedTestFinishedEvents())//
.haveExactly(1,
event(test("noExceptionExpectedButThrown"), //
finishedWithFailure(message("no exception expected")))) //
.haveExactly(1,
event(test("exceptionExpectedButNotThrown"), //
finishedWithFailure(allOf(isA(AssertionError.class), //
message("Expected test to throw an instance of java.lang.RuntimeException"))))) //
.haveExactly(1,
event(test("wrongExceptionExpected"), //
finishedWithFailure(allOf(isA(AssertionError.class), //
message(value -> value.contains("Expected: an instance of java.io.IOException"))))));
}

private ExecutionEventRecorder executeTestsForClass(Class<?> testClass) {
LauncherDiscoveryRequest request = request().selectors(selectClass(testClass)).build();
JupiterTestEngine engine = new JupiterTestEngine();
TestDescriptor testDescriptor = engine.discover(request, UniqueId.forEngine(engine.getId()));
ExecutionEventRecorder eventRecorder = new ExecutionEventRecorder();
engine.execute(new ExecutionRequest(testDescriptor, eventRecorder, request.getConfigurationParameters()));
return eventRecorder;
}

@ExtendWith(ExpectedExceptionSupport.class)
private static class ExpectedExceptionTestCase {

@Rule
public ExpectedException thrown = ExpectedException.none();

@Test
void noExceptionExpectedButThrown() {
throw new RuntimeException("no exception expected");
}

@Test
void exceptionExpectedButNotThrown() {
thrown.expect(RuntimeException.class);
}

@Test
void wrongExceptionExpected() {
thrown.expect(IOException.class);
throw new RuntimeException("wrong exception");
}

@Test
void correctExceptionExpectedThrown() {
thrown.expect(RuntimeException.class);
throw new RuntimeException("wrong exception");
}

}

}

0 comments on commit 031463b

Please sign in to comment.