Skip to content

Commit 70f65aa

Browse files
committed
Ensure exceptions from methodBlock() don't result in unrooted tests
The introduction of the runLeaf() method in BlockJUnit4ClassRunner in JUnit 4.9 introduced a regression with regard to exception handling. Specifically, the invocation of methodBlock() is no longer executed within a try-catch block as was the case in previous versions of JUnit. Custom modifications to methodBlock() or the methods it invokes may in fact throw exceptions. In such cases, exceptions thrown from methodBlock() cause the current test execution to abort immediately. As a result, the failing test method is unrooted in test reports, and subsequent test methods are never invoked. Furthermore, RunListeners registered with JUnit are not notified. This commit addresses this issue by wrapping the invocation of methodBlock() within a try-catch block. If an exception is not thrown, the resulting Statement is passed to runLeaf(). If an exception is thrown, it is wrapped in a Fail statement which is passed to runLeaf(). Issue: #1066
1 parent 0f0152a commit 70f65aa

3 files changed

Lines changed: 117 additions & 4 deletions

File tree

src/main/java/org/junit/runners/BlockJUnit4ClassRunner.java

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,9 @@
5555
* @since 4.5
5656
*/
5757
public class BlockJUnit4ClassRunner extends ParentRunner<FrameworkMethod> {
58+
5859
private final ConcurrentHashMap<FrameworkMethod, Description> methodDescriptions = new ConcurrentHashMap<FrameworkMethod, Description>();
60+
5961
/**
6062
* Creates a BlockJUnit4ClassRunner to run {@code testClass}
6163
*
@@ -75,10 +77,17 @@ protected void runChild(final FrameworkMethod method, RunNotifier notifier) {
7577
if (isIgnored(method)) {
7678
notifier.fireTestIgnored(description);
7779
} else {
78-
runLeaf(methodBlock(method), description, notifier);
80+
Statement statement;
81+
try {
82+
statement = methodBlock(method);
83+
}
84+
catch (Throwable ex) {
85+
statement = new Fail(ex);
86+
}
87+
runLeaf(statement, description, notifier);
7988
}
8089
}
81-
90+
8291
/**
8392
* Evaluates whether {@link FrameworkMethod}s are ignored based on the
8493
* {@link Ignore} annotation.
@@ -390,10 +399,10 @@ private List<org.junit.rules.MethodRule> getMethodRules(Object target) {
390399
protected List<MethodRule> rules(Object target) {
391400
List<MethodRule> rules = getTestClass().getAnnotatedMethodValues(target,
392401
Rule.class, MethodRule.class);
393-
402+
394403
rules.addAll(getTestClass().getAnnotatedFieldValues(target,
395404
Rule.class, MethodRule.class));
396-
405+
397406
return rules;
398407
}
399408

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
/*
2+
* Copyright 2015 the original author or authors.
3+
*
4+
* All rights reserved. This program and the accompanying materials are
5+
* made available under the terms of the Eclipse Public License v1.0 which
6+
* accompanies this distribution and is available at
7+
*
8+
* http://www.eclipse.org/legal/epl-v10.html
9+
*/
10+
11+
package org.junit.runners;
12+
13+
import static org.junit.Assert.assertEquals;
14+
15+
import java.util.concurrent.atomic.AtomicInteger;
16+
17+
import org.junit.Ignore;
18+
import org.junit.Test;
19+
import org.junit.runner.Description;
20+
import org.junit.runner.notification.Failure;
21+
import org.junit.runner.notification.RunListener;
22+
import org.junit.runner.notification.RunNotifier;
23+
import org.junit.runners.model.FrameworkMethod;
24+
import org.junit.runners.model.InitializationError;
25+
import org.junit.runners.model.Statement;
26+
27+
/**
28+
* Tests that verify proper behavior for custom runners that extend
29+
* {@link BlockJUnit4ClassRunner}.
30+
*
31+
* @author Sam Brannen
32+
* @since 4.13
33+
*/
34+
public class CustomBlockJUnit4ClassRunnerTest {
35+
36+
@Test
37+
public void exceptionsFromMethodBlockMustNotResultInUnrootedTests() throws Exception {
38+
TrackingRunListener listener = new TrackingRunListener();
39+
RunNotifier notifier = new RunNotifier();
40+
notifier.addListener(listener);
41+
42+
new CustomBlockJUnit4ClassRunner(CustomBlockJUnit4ClassRunnerTestCase.class).run(notifier);
43+
assertEquals("tests started.", 2, listener.testStartedCount.get());
44+
assertEquals("tests failed.", 1, listener.testFailureCount.get());
45+
assertEquals("tests finished.", 2, listener.testFinishedCount.get());
46+
}
47+
48+
49+
@Ignore("This test case is run manually by the enclosing test class")
50+
public static class CustomBlockJUnit4ClassRunnerTestCase {
51+
@Test public void shouldPass() { /* no-op */ }
52+
@Test public void throwException() { /* no-op */ }
53+
}
54+
55+
/**
56+
* Custom extension of {@link BlockJUnit4ClassRunner} that always throws
57+
* an exception from the {@code methodBlock()} if a test method is named
58+
* exactly {@code "throwException"}.
59+
*/
60+
private static class CustomBlockJUnit4ClassRunner extends BlockJUnit4ClassRunner {
61+
62+
CustomBlockJUnit4ClassRunner(Class<?> testClass) throws InitializationError {
63+
super(testClass);
64+
}
65+
66+
@Override
67+
protected Statement methodBlock(FrameworkMethod method) {
68+
if ("throwException".equals(method.getName())) {
69+
throw new RuntimeException("throwException() test method invoked");
70+
}
71+
return super.methodBlock(method);
72+
}
73+
}
74+
75+
/**
76+
* Simple {@link RunListener} that tracks the number of times that
77+
* certain callbacks are invoked.
78+
*/
79+
private static class TrackingRunListener extends RunListener {
80+
81+
final AtomicInteger testStartedCount = new AtomicInteger();
82+
final AtomicInteger testFailureCount = new AtomicInteger();
83+
final AtomicInteger testFinishedCount = new AtomicInteger();
84+
85+
86+
@Override
87+
public void testStarted(Description description) throws Exception {
88+
testStartedCount.incrementAndGet();
89+
}
90+
91+
@Override
92+
public void testFailure(Failure failure) throws Exception {
93+
testFailureCount.incrementAndGet();
94+
}
95+
96+
@Override
97+
public void testFinished(Description description) throws Exception {
98+
testFinishedCount.incrementAndGet();
99+
}
100+
}
101+
102+
}

src/test/java/org/junit/tests/AllTests.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import org.junit.runner.notification.ConcurrentRunNotifierTest;
1919
import org.junit.runner.notification.RunNotifierTest;
2020
import org.junit.runner.notification.SynchronizedRunListenerTest;
21+
import org.junit.runners.CustomBlockJUnit4ClassRunnerTest;
2122
import org.junit.runners.Suite;
2223
import org.junit.runners.Suite.SuiteClasses;
2324
import org.junit.runners.model.FrameworkFieldTest;
@@ -203,6 +204,7 @@
203204
RuleMemberValidatorTest.class,
204205
RuleChainTest.class,
205206
BlockJUnit4ClassRunnerTest.class,
207+
CustomBlockJUnit4ClassRunnerTest.class,
206208
MethodSorterTest.class,
207209
TestedOnSupplierTest.class,
208210
StacktracePrintingMatcherTest.class,

0 commit comments

Comments
 (0)