JUnitで'expected'例外を定義できることを知っています。
@Test(expect=MyException.class)
public void someMethod() { ... }
しかし、常に同じ例外がスローされるが、「ネストされた」異なる原因の場合はどうなるでしょうか。
助言がありますか?
テストコードをtry/catchブロックでラップし、スローされた例外をキャッチし、内部原因を確認し、ログ/アサート/何でもして、(必要に応じて)例外を再スローできます。
JUnit 4.11以降では、ExpectedException
ルールの expectCause()
メソッドを使用できます。
import static org.hamcrest.CoreMatchers.*;
// ...
@Rule
public ExpectedException expectedException = ExpectedException.none();
@Test
public void throwsNestedException() throws Exception {
expectedException.expectCause(isA(SomeNestedException.class));
throw new ParentException("foo", new SomeNestedException("bar"));
}
JUnitの最新バージョンを使用している場合は、デフォルトのテストランナーを拡張してこれを処理できます(try/catchブロックで各メソッドをラップする必要はありません)。
ExtendedTestRunner.Java-新しいテストランナー:
public class ExtendedTestRunner extends BlockJUnit4ClassRunner
{
public ExtendedTestRunner( Class<?> clazz )
throws InitializationError
{
super( clazz );
}
@Override
protected Statement possiblyExpectingExceptions( FrameworkMethod method,
Object test,
Statement next )
{
ExtendedTest annotation = method.getAnnotation( ExtendedTest.class );
return expectsCauseException( annotation ) ?
new ExpectCauseException( next, getExpectedCauseException( annotation ) ) :
super.possiblyExpectingExceptions( method, test, next );
}
@Override
protected List<FrameworkMethod> computeTestMethods()
{
Set<FrameworkMethod> testMethods = new HashSet<FrameworkMethod>( super.computeTestMethods() );
testMethods.addAll( getTestClass().getAnnotatedMethods( ExtendedTest.class ) );
return testMethods;
}
@Override
protected void validateTestMethods( List<Throwable> errors )
{
super.validateTestMethods( errors );
validatePublicVoidNoArgMethods( ExtendedTest.class, false, errors );
}
private Class<? extends Throwable> getExpectedCauseException( ExtendedTest annotation )
{
if (annotation == null || annotation.expectedCause() == ExtendedTest.None.class)
return null;
else
return annotation.expectedCause();
}
private boolean expectsCauseException( ExtendedTest annotation) {
return getExpectedCauseException(annotation) != null;
}
}
ExtendedTest.Java-テストメソッドをマークする注釈:
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface ExtendedTest
{
/**
* Default empty exception
*/
static class None extends Throwable {
private static final long serialVersionUID= 1L;
private None() {
}
}
Class<? extends Throwable> expectedCause() default None.class;
}
ExpectCauseException.Java-新しいJUnitステートメント:
public class ExpectCauseException extends Statement
{
private Statement fNext;
private final Class<? extends Throwable> fExpected;
public ExpectCauseException( Statement next, Class<? extends Throwable> expected )
{
fNext= next;
fExpected= expected;
}
@Override
public void evaluate() throws Exception
{
boolean complete = false;
try {
fNext.evaluate();
complete = true;
} catch (Throwable e) {
if ( e.getCause() == null || !fExpected.isAssignableFrom( e.getCause().getClass() ) )
{
String message = "Unexpected exception cause, expected<"
+ fExpected.getName() + "> but was<"
+ ( e.getCause() == null ? "none" : e.getCause().getClass().getName() ) + ">";
throw new Exception(message, e);
}
}
if (complete)
throw new AssertionError( "Expected exception cause: "
+ fExpected.getName());
}
}
使用法:
@RunWith( ExtendedTestRunner.class )
public class MyTests
{
@ExtendedTest( expectedCause = MyException.class )
public void someMethod()
{
throw new RuntimeException( new MyException() );
}
}
あなたはいつでもそれを手動で行うことができます:
@Test
public void someMethod() {
try{
... all your code
} catch (Exception e){
// check your nested clauses
if(e.getCause() instanceof FooException){
// pass
} else {
Assert.fail("unexpected exception");
}
}
例外のために Matcher を作成できます。これは Arquillian の@RunWith(Arquillian.class)
などの別のテストランナーを使用している場合でも機能するため、上記の@RunWith(ExtendedTestRunner.class)
アプローチを使用できません。
以下に簡単な例を示します。
public class ExceptionMatcher extends BaseMatcher<Object> {
private Class<? extends Throwable>[] classes;
// @SafeVarargs // <-- Suppress warning in Java 7. This usage is safe.
public ExceptionMatcher(Class<? extends Throwable>... classes) {
this.classes = classes;
}
@Override
public boolean matches(Object item) {
for (Class<? extends Throwable> klass : classes) {
if (! klass.isInstance(item)) {
return false;
}
item = ((Throwable) item).getCause();
}
return true;
}
@Override
public void describeTo(Description descr) {
descr.appendText("unexpected exception");
}
}
次に、これを @ Rule および ExpectedException と次のように使用します。
@Rule
public ExpectedException thrown = ExpectedException.none();
@Test
public void testSomething() {
thrown.expect(new ExceptionMatcher(IllegalArgumentException.class, IllegalStateException.class));
throw new IllegalArgumentException("foo", new IllegalStateException("bar"));
}
2012年にCraig Ringerによって追加されましたedit:拡張されたより信頼性の高いバージョン:
boolean rethrow
を渡して、一致しない例外をスローできます。これにより、ネストされた例外のスタックトレースが保持され、デバッグが容易になります。@SaveVarargs
を削除してください。完全なコード:
import org.Apache.commons.lang3.exception.ExceptionUtils;
import org.hamcrest.BaseMatcher;
import org.hamcrest.Description;
public class ExceptionMatcher extends BaseMatcher<Object> {
private Class<? extends Throwable>[] acceptedClasses;
private Throwable[] nestedExceptions;
private final boolean rethrow;
@SafeVarargs
public ExceptionMatcher(Class<? extends Throwable>... classes) {
this(false, classes);
}
@SafeVarargs
public ExceptionMatcher(boolean rethrow, Class<? extends Throwable>... classes) {
this.rethrow = rethrow;
this.acceptedClasses = classes;
}
@Override
public boolean matches(Object item) {
nestedExceptions = ExceptionUtils.getThrowables((Throwable)item);
for (Class<? extends Throwable> acceptedClass : acceptedClasses) {
for (Throwable nestedException : nestedExceptions) {
if (acceptedClass.isInstance(nestedException)) {
return true;
}
}
}
if (rethrow) {
throw new AssertionError(buildDescription(), (Throwable)item);
}
return false;
}
private String buildDescription() {
StringBuilder sb = new StringBuilder();
sb.append("Unexpected exception. Acceptable (possibly nested) exceptions are:");
for (Class<? extends Throwable> klass : acceptedClasses) {
sb.append("\n ");
sb.append(klass.toString());
}
if (nestedExceptions != null) {
sb.append("\nNested exceptions found were:");
for (Throwable nestedException : nestedExceptions) {
sb.append("\n ");
sb.append(nestedException.getClass().toString());
}
}
return sb.toString();
}
@Override
public void describeTo(Description description) {
description.appendText(buildDescription());
}
}
典型的な出力:
Java.lang.AssertionError: Expected: Unexpected exception. Acceptable (possibly nested) exceptions are:
class some.application.Exception
Nested exceptions found were:
class javax.ejb.EJBTransactionRolledbackException
class javax.persistence.NoResultException
got: <javax.ejb.EJBTransactionRolledbackException: getSingleResult() did not retrieve any entities.>
そのために、JUnit拡張機能を少し書きました。静的ヘルパー関数は、関数本体と予期される例外の配列を取ります。
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import Java.util.Arrays;
public class AssertExt {
public static interface Runnable {
void run() throws Exception;
}
public static void assertExpectedExceptionCause( Runnable runnable, @SuppressWarnings("unchecked") Class[] expectedExceptions ) {
boolean thrown = false;
try {
runnable.run();
} catch( Throwable throwable ) {
final Throwable cause = throwable.getCause();
if( null != cause ) {
assertTrue( Arrays.asList( expectedExceptions ).contains( cause.getClass() ) );
thrown = true;
}
}
if( !thrown ) {
fail( "Expected exception not thrown or thrown exception had no cause!" );
}
}
}
次のように、予想されるネストされた例外を確認できます。
import static AssertExt.assertExpectedExceptionCause;
import org.junit.Test;
public class TestExample {
@Test
public void testExpectedExceptionCauses() {
assertExpectedExceptionCause( new AssertExt.Runnable(){
public void run() throws Exception {
throw new Exception( new NullPointerException() );
}
}, new Class[]{ NullPointerException.class } );
}
}
これにより、同じボイラープレートコードを何度も書く手間が省けます。
最も簡潔な構文は catch-exception によって提供されます:
import static com.googlecode.catchexception.CatchException.*;
catchException(myObj).doSomethingNasty();
assertTrue(caughtException().getCause() instanceof MyException);