2

我在一个包含许多嵌入错误代码的“BusinessException”的项目中工作。

在每个异常单元测试中,我必须测试这些重复这种模式的错误代码:

@Test
public void zipFileReaderCtorShouldThrowAnExceptionWithInexistingArchive() {
    try {
        zfr = new ZipFileReader("unexpected/path/to/file");
        fail("'BusinessZipException' not throwed");
    } catch (BusinessZipException e) {
        assertThat("Unexpected error code", e.getErrorCode(), is(ErrorCode.FILE_NOT_FOUND));
    } catch (Exception e) {
        fail("Unexpected Exception: '" + e + "', expected: 'BusinessZipException'");
    }
}

(由于错误代码测试,无法使用 JUnit 注解)

我很无聊这样做,特别是因为我不得不在 fail() 的错误消息中复制/粘贴异常名称。

所以,我写了一个 Util 类。我使用一个抽象类来处理异常断言测试。

公共抽象类 TestExceptionUtil {
    公共无效runAndExpectException(类expectedException,字符串expectedErrorCode){
        String failUnexpectedExceptionMessage = "意外异常。预期为:'%s',但得到:'%s'";
        尝试 {
            代码执行();
            fail("'" + expectedException.getName() + "' 没有抛出");
        } 捕捉(业务异常 e){
            if (e.getClass().equals(expectedException)) {
                assertThat("不应出现异常错误代码", e.getErrorCode(), is(expectedErrorCode));
            } 别的 {
                失败(String.format(failUnexpectedExceptionMessage,expectedException.getName(),e));
            }
        } 捕捉(异常 e){
            失败(String.format(failUnexpectedExceptionMessage,expectedException.getName(),e));
        }
    }

    抽象公共无效codeToExecute();
}

然后,客户以这种方式使用它:

@Test
public void zipFileReaderCtorShouldThrowAnExceptionWithInexistingArchive() {
    new TestExceptionUtil() {
        @Override
        public void codeToExecute() {
            zfr = new ZipFileReader("unexpected/path/to/file");
        }
    }.runAndExpectException(BusinessTechnicalException.class, ErrorCode.FILE_NOT_FOUND);
}

你觉得它“干净”吗?你觉得可以改善吗?你认为它太重和/或无用吗?我的主要目标是在我们的开发团队中统一测试异常。(当然还有分解代码)

谢谢阅读!

4

2 回答 2

10

How about the JUnit ExpectedException Rule?

First you declare the Rule at the top of the test class:

@Rule
public final ExpectedException ee = ExpectedException.none();

Then in your test method you can declare that you can expect an Exception:

@Test
public void testStuff() {
    ee.expect(IllegalArgumentException.class);
    ee.expectMessage("My Exception text");
}

I think this is significatly cleaner than your approach.

You can then use hamcrest Matchers to match the Exception message:

@Test
public void testStuff() {
    ee.expect(IllegalArgumentException.class);
    ee.expectMessage(containsString("error"));
    ee.expect(hasProperty("errorCode", is(7)));
}

The hasProperty Matcher will look for a getter for the named property and check that it matches the second argument - which is another Matcher.

You can even implement your own Matcher, in which case you will not require a dependency on hamcrest:

public class ErrorCodeMatcher extends BaseMatcher<Throwable> {

    private final int expectedErrorCode;

    public ErrorCodeMatcher(int expectedErrorCode) {
        this.expectedErrorCode = expectedErrorCode;
    }

    @Override
    public boolean matches(Object o) {
        return ((BusinessZipException) o).getErrorCode() == expectedErrorCode;
    }

    @Override
    public void describeTo(Description d) {
        d.appendText("Expected error code was" + expectedErrorCode);
    }
}

This would be used as follows:

ee.expect(new ErrorCodeMatcher(7));

With a static factory method and a static import this can become quite clean:

ee.expect(exceptionWithErrorCode(7));

If you have a common interface that defines your business Exception with a getErrorCode() method, say called ErrorAwareException then you can extend the TypeSafeMatcher<T> class to create slightly cleaner code:

public class ErrorCodeMatcher<T extends Exception & ErrorAwareException> extends TypeSafeMatcher<T> {

    public static <E extends Exception & ErrorAwareException> ErrorCodeMatcher<E> exceptionWithErrorCode(final int expectedErrorCode) {
        return new ErrorCodeMatcher<E>(expectedErrorCode);
    }
    private final int expectedErrorCode;

    public ErrorCodeMatcher(int expectedErrorCode) {
        this.expectedErrorCode = expectedErrorCode;
    }

    @Override
    protected boolean matchesSafely(final T t) {
        return t.getErrorCode() == expectedErrorCode;
    }

    @Override
    public void describeTo(Description d) {
        d.appendText("Expected error code was" + expectedErrorCode);
    }
}

Note that if you do choose to use hamcrest then make sure that you include junit-dep rather than pure junit in your project otherwise the hamcrest classes will clash with the hamcrest classes included with junit. In maven, this will look something like this:

<dependency>
    <groupId>org.hamcrest</groupId>
    <artifactId>hamcrest-all</artifactId>
    <version>1.3</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>junit</groupId>
    <artifactId>junit-dep</artifactId>
    <version>4.11</version>
    <scope>test</scope> 
</dependency>
于 2013-10-02T21:03:18.590 回答
3

我认为你实际上是在这里重新发明轮子。您可以使用注释的任expected一参数,@Test这会导致测试方法成功抛出给定的异常。或者使用该ExpectedException规则的功能基本相同但功能更多。所以试试

@Test(expected = Exception.class)
public void myTest() {
    throw new Exception();
}

或者

@Rule
private ExpectedException rule = ExpectedException.none();

@Test
public void myTest() {
    rule.expect(Exception.class);
    throw new Exception();
}
于 2013-10-02T21:08:56.670 回答