四岁了,还是...
- 非空的快乐路径
AutoCloseable
- null 的快乐路径
AutoCloseable
- 写入时抛出
- 近距离投掷
- 写入并关闭
- 抛出资源规范(with部分,例如构造函数调用)
- 抛出
try
块但AutoCloseable
为空
上面列出了所有 7 个条件 - 8 个分支的原因是由于重复条件。
可以到达所有分支,这try-with-resources
是相当简单的编译器糖(至少与 相比switch-on-string
)-如果无法到达,则根据定义它是编译器错误。
实际上只需要 6 个单元测试(在下面的示例代码中,throwsOnClose
是@Ingore
d,分支覆盖率是 8/8.
还要注意Throwable.addSuppressed(Throwable)不能抑制自身,因此生成的字节码包含一个额外的保护(IF_ACMPEQ - 引用相等)来防止这种情况)。幸运的是,这个分支被 throw-on-write、throw-on-close 和 throw-on-write-and-close 情况所覆盖,因为字节码变量槽被 3 个异常处理程序区域中的外部 2 个重用。
这不是Jacoco 的问题 - 事实上,链接问题 #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");
/**
* Covers one branch
*/
@Test
public void happyPath()
{
Subject subject = new Subject(new DummyOutputStream(null, null));
subject.scrutinize("text");
}
/**
* Covers one branch
*/
@Test
public void happyPathWithNullCloseable()
{
Subject subject = new Subject((OutputStream) null);
subject.scrutinize("text");
}
/**
* Covers one branch
*/
@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)));
}
}
/**
* Covers three branches
*/
@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)));
}
}
/**
* Covers one branch - Not needed for coverage if you have the other tests
*/
@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)));
}
}
/**
* Covers two branches
*/
@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))));
}
}
/**
* Covers three branches
*/
@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 的示例代码中,但有一种情况无法测试 AFAIK。
如果您将资源引用作为参数传递,那么在 Java 7/8 中,您必须有一个局部变量要分配给:
void someMethod(AutoCloseable arg)
{
try(AutoCloseable pfft = arg) {
//...
}
}
在这种情况下,生成的代码仍将保护资源引用。语法糖在 Java 9 中更新,不再需要局部变量:try(arg){ /*...*/ }
补充 - 建议使用库来完全避免分支
诚然,这些分支中的一些可以被认为是不切实际的 - 即 try 块使用AutoCloseable
不带空检查的地方或资源引用 ( with
) 不能为空的地方。
通常你的应用程序并不关心它失败的地方——打开文件、写入文件或关闭文件——失败的粒度是无关紧要的(除非应用程序特别关注文件,例如文件浏览器或文字处理器)。
此外,在 OP 的代码中,要测试 null 可关闭路径 - 您必须将 try 块重构为受保护的方法、子类并提供 NOOP 实现 - 所有这些都只是覆盖了永远不会在野外使用的分支.
我编写了一个小型 Java 8 库io.earcam.unexceptional(在Maven Central 中),它处理大多数检查异常样板。
与这个问题相关:它为 s 提供了一堆零分支、单行代码AutoCloseable
,将已检查的异常转换为未检查的异常。
示例:自由端口查找器
int port = Closing.closeAfterApplying(ServerSocket::new, 0, ServerSocket::getLocalPort);