Четыре года, но все же ...
- Счастливый путь с ненулевым
AutoCloseable
- Счастливый путь с нулем
AutoCloseable
- Бросает на запись
- Бросает близко
- Кидает на запись и закрывает
- Выбрасывает спецификацию ресурса (часть with, например, вызов конструктора)
- Выбрасывает блок
try
, но AutoCloseable
имеет значение null
Выше перечислены все 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, поскольку слоты переменных байт-кода повторно используются внешними 2 из 3 областей обработчика исключений.
Это не проблема с 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, чтобы проверить нулевой закрываемый путь - вам нужно будет реорганизовать блок try в защищенный метод, подкласс и предоставить реализацию NOOP - все это просто охватит ветки, которые никогда не будут приняты в дикой природе .
Я написал крошечную библиотеку Java 8 io.earcam.unexceptional (в Maven Central), который работает с большинством проверенных шаблонов исключений.
Имеет отношение к этому вопросу: он предоставляет кучу однострочников с нулевым переходом для AutoCloseable
s, конвертирующих отмеченные исключения в непроверенные.
Пример: Free Port Finder
int port = Closing.closeAfterApplying(ServerSocket::new, 0, ServerSocket::getLocalPort);
person
earcam
schedule
22.10.2017