四年过去了,但仍然……
- 使用非空的
AutoCloseable
的正常情况
- 使用空的
AutoCloseable
的正常情况
- 写入时抛出异常
- 关闭时抛出异常
- 写入和关闭时都抛出异常
- 在资源规范中(例如构造函数调用)抛出异常
try
块中抛出异常,但AutoCloseable
为空
以上列出了所有7个条件 - 8个分支的原因是由于重复条件。
所有分支均可到达,
try-with-resources
相对简单的编译器语法糖(至少与
switch-on-string
相比如此)- 如果无法到达,则根据定义是编译器错误。
实际上只需要6个单元测试(在下面的示例代码中,
throwsOnClose
被
@Ingore
,分支覆盖率为8/8)。
请注意,
Throwable.addSuppressed(Throwable) 不能压制自己,因此生成的字节码包含一个额外的保护(IF_ACMPEQ - 引用相等),以防止这种情况发生。幸运的是,这个分支被 throw-on-write、throw-on-close 和 throw-on-write-and-close 情况覆盖,因为字节码变量插槽被外部的三个异常处理程序区域重复使用。
这不是Jacoco的问题 - 实际上,链接的
issue #82 中的示例代码是不正确的,因为没有重复的空检查,并且没有包围关闭操作的嵌套 catch 块。
JUnit 测试演示了 8 个分支中的 8 个已被覆盖。
import static org.hamcrest.Matchers.arrayContaining;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.sameInstance;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.fail;
import java.io.IOException;
import java.io.OutputStream;
import java.io.UncheckedIOException;
import org.junit.Ignore;
import org.junit.Test;
public class FullBranchCoverageOnTryWithResourcesTest {
private static class DummyOutputStream extends OutputStream {
private final IOException thrownOnWrite;
private final IOException thrownOnClose;
public DummyOutputStream(IOException thrownOnWrite, IOException thrownOnClose)
{
this.thrownOnWrite = thrownOnWrite;
this.thrownOnClose = thrownOnClose;
}
@Override
public void write(int b) throws IOException
{
if(thrownOnWrite != null) {
throw thrownOnWrite;
}
}
@Override
public void close() throws IOException
{
if(thrownOnClose != null) {
throw thrownOnClose;
}
}
}
private static class Subject {
private OutputStream closeable;
private IOException exception;
public Subject(OutputStream closeable)
{
this.closeable = closeable;
}
public Subject(IOException exception)
{
this.exception = exception;
}
public void scrutinize(String text)
{
try(OutputStream closeable = create()) {
process(closeable);
} catch(IOException e) {
throw new UncheckedIOException(e);
}
}
protected void process(OutputStream closeable) throws IOException
{
if(closeable != null) {
closeable.write(1);
}
}
protected OutputStream create() throws IOException
{
if(exception != null) {
throw exception;
}
return closeable;
}
}
private final IOException onWrite = new IOException("Two writes don't make a left");
private final IOException onClose = new IOException("Sorry Dave, we're open 24/7");
@Test
public void happyPath()
{
Subject subject = new Subject(new DummyOutputStream(null, null));
subject.scrutinize("text");
}
@Test
public void happyPathWithNullCloseable()
{
Subject subject = new Subject((OutputStream) null);
subject.scrutinize("text");
}
@Test
public void throwsOnCreateResource()
{
IOException chuck = new IOException("oom?");
Subject subject = new Subject(chuck);
try {
subject.scrutinize("text");
fail();
} catch(UncheckedIOException e) {
assertThat(e.getCause(), is(sameInstance(chuck)));
}
}
@Test
public void throwsOnWrite()
{
Subject subject = new Subject(new DummyOutputStream(onWrite, null));
try {
subject.scrutinize("text");
fail();
} catch(UncheckedIOException e) {
assertThat(e.getCause(), is(sameInstance(onWrite)));
}
}
@Ignore
@Test
public void throwsOnClose()
{
Subject subject = new Subject(new DummyOutputStream(null, onClose));
try {
subject.scrutinize("text");
fail();
} catch(UncheckedIOException e) {
assertThat(e.getCause(), is(sameInstance(onClose)));
}
}
@SuppressWarnings("unchecked")
@Test
public void throwsOnWriteAndClose()
{
Subject subject = new Subject(new DummyOutputStream(onWrite, onClose));
try {
subject.scrutinize("text");
fail();
} catch(UncheckedIOException e) {
assertThat(e.getCause(), is(sameInstance(onWrite)));
assertThat(e.getCause().getSuppressed(), is(arrayContaining(sameInstance(onClose))));
}
}
@Test
public void throwsInTryBlockButCloseableIsNull() throws Exception
{
IOException chucked = new IOException("ta-da");
Subject subject = new Subject((OutputStream) null) {
@Override
protected void process(OutputStream closeable) throws IOException
{
throw chucked;
}
};
try {
subject.scrutinize("text");
fail();
} catch(UncheckedIOException e) {
assertThat(e.getCause(), is(sameInstance(chucked)));
}
}
}
注意事项
虽然不在OP的示例代码中,但我认为有一种情况无法测试。
如果将资源引用作为参数传递,则在Java 7/8中必须有一个本地变量来分配:
void someMethod(AutoCloseable arg)
{
try(AutoCloseable pfft = arg) {
}
}
在这种情况下,生成的代码仍然将保护资源引用。语法糖是
在Java 9中更新,其中不再需要局部变量:
try(arg){ /*...*/ }
补充 - 建议使用库完全避免分支
诚然,其中一些分支可以被认为是不现实的 - 即尝试块使用
AutoCloseable
而没有进行空值检查或者资源引用(
with
)不能为null的情况。
通常,你的应用程序并不关心它失败的位置 - 是打开文件、写入文件还是关闭文件 - 失败的粒度是无关紧要的(除非应用程序专门处理文件,例如文件浏览器或文字处理器)。
此外,在OP的代码中,为了测试空闭合路径 - 你必须将try块重构为受保护的方法,子类化并提供NOOP实现 - 所有这些只是为了覆盖永远不会在野外执行的分支。
我写了一个小型的Java 8库
io.earcam.unexceptional(在
Maven Central中),用于处理大部分检查异常样板。与这个问题相关的是:它提供了一些零分支、单行代码,用于AutoCloseable,将受检异常转换为未受检异常。
例如:自由端口查找器
int port = Closing.closeAfterApplying(ServerSocket::new, 0, ServerSocket::getLocalPort)