리소스로 시도하기위한 8 가지 지점-Jacoco 커버리지 가능?
try with resources를 사용하는 코드가 있으며 jacoco에서는 절반으로 만 표시됩니다. 모든 소스 코드 줄이 녹색이지만 8 개 분기 중 4 개만 포함되어 있음을 알려주는 노란색 기호가 표시됩니다.
나는 모든 분기가 무엇인지, 그리고 그것들을 다루는 코드를 작성하는 방법을 파악하는 데 어려움을 겪고 있습니다. 세 가지 가능한 장소가 던져 PipelineException
집니다. 이들은 createStageList()
, processItem()
그리고는 암시close()
- 예외를 던지지 않고
- 예외 던지기
createStageList()
- 예외 던지기
processItem()
- 예외 던지기
close()
- 에서 예외를 throw
processItem()
및close()
다른 케이스는 생각할 수 없지만 여전히 8 개 중 4 개만 다룹니다.
누군가가 왜 그것이 4/8이고 어쨌든 8 브랜치를 모두 칠 수 있는지 설명해 줄 수 있습니까? 나는 바이트 코드 해독 / 읽기 / 해석에 능숙하지 않지만 아마도 당신은 ... :) 나는 이미 https://github.com/jacoco/jacoco/issues/82 봤지만 그것도 문제도 아닙니다 (컴파일러가 생성 한 블록 때문이라는 점을 제외하고는) 도움말을 매우 많이 참조합니다.
흠,이 글을 다 쓰는 순간 위에서 언급 한 내용으로 테스트되지 않은 케이스가 무엇인지 생각해 보았습니다. 정답을 맞히면 답변을 게시하겠습니다. 나는이 질문과 대답이 어떤 경우에도 도움이 될 것이라고 확신합니다.
편집 : 아니요, 찾지 못했습니다. 발생하는 RuntimeExceptions (catch 블록에 의해 처리되지 않음)가 더 이상 분기를 다루지 않았습니다.
Jacoco의 정확한 문제가 무엇인지 말할 수는 없지만 Try With Resources가 컴파일되는 방법을 보여줄 수 있습니다. 기본적으로 다양한 지점에서 발생하는 예외를 처리하기 위해 많은 컴파일러 생성 스위치가 있습니다.
다음 코드를 가져 와서 컴파일하면
public static void main(String[] args){
String a = "before";
try (CharArrayWriter br = new CharArrayWriter()) {
br.writeTo(null);
} catch (IOException e){
System.out.println(e.getMessage());
}
String a2 = "after";
}
그런 다음 분해하면
.method static public main : ([Ljava/lang/String;)V
.limit stack 2
.limit locals 7
.catch java/lang/Throwable from L26 to L30 using L33
.catch java/lang/Throwable from L13 to L18 using L51
.catch [0] from L13 to L18 using L59
.catch java/lang/Throwable from L69 to L73 using L76
.catch [0] from L51 to L61 using L59
.catch java/io/IOException from L3 to L94 using L97
ldc 'before'
astore_1
L3:
new java/io/CharArrayWriter
dup
invokespecial java/io/CharArrayWriter <init> ()V
astore_2
aconst_null
astore_3
L13:
aload_2
aconst_null
invokevirtual java/io/CharArrayWriter writeTo (Ljava/io/Writer;)V
L18:
aload_2
ifnull L94
aload_3
ifnull L44
L26:
aload_2
invokevirtual java/io/CharArrayWriter close ()V
L30:
goto L94
L33:
.stack full
locals Object [Ljava/lang/String; Object java/lang/String Object java/io/CharArrayWriter Object java/lang/Throwable
stack Object java/lang/Throwable
.end stack
astore 4
aload_3
aload 4
invokevirtual java/lang/Throwable addSuppressed (Ljava/lang/Throwable;)V
goto L94
L44:
.stack same
aload_2
invokevirtual java/io/CharArrayWriter close ()V
goto L94
L51:
.stack same_locals_1_stack_item
stack Object java/lang/Throwable
.end stack
astore 4
aload 4
astore_3
aload 4
athrow
L59:
.stack same_locals_1_stack_item
stack Object java/lang/Throwable
.end stack
astore 5
L61:
aload_2
ifnull L91
aload_3
ifnull L87
L69:
aload_2
invokevirtual java/io/CharArrayWriter close ()V
L73:
goto L91
L76:
.stack full
locals Object [Ljava/lang/String; Object java/lang/String Object java/io/CharArrayWriter Object java/lang/Throwable Top Object java/lang/Throwable
stack Object java/lang/Throwable
.end stack
astore 6
aload_3
aload 6
invokevirtual java/lang/Throwable addSuppressed (Ljava/lang/Throwable;)V
goto L91
L87:
.stack same
aload_2
invokevirtual java/io/CharArrayWriter close ()V
L91:
.stack same
aload 5
athrow
L94:
.stack full
locals Object [Ljava/lang/String; Object java/lang/String
stack
.end stack
goto L108
L97:
.stack same_locals_1_stack_item
stack Object java/io/IOException
.end stack
astore_2
getstatic java/lang/System out Ljava/io/PrintStream;
aload_2
invokevirtual java/io/IOException getMessage ()Ljava/lang/String;
invokevirtual java/io/PrintStream println (Ljava/lang/String;)V
L108:
.stack same
ldc 'after'
astore_2
return
.end method
바이트 코드를 사용하지 않는 사람들을 위해 이것은 대략 다음 의사 Java와 동일합니다. 바이트 코드가 실제로 Java 제어 흐름에 해당하지 않기 때문에 gotos를 사용해야했습니다.
보시다시피, 억제 된 예외의 다양한 가능성을 처리하는 많은 경우가 있습니다. 이러한 모든 경우를 다룰 수있는 것은 합리적이지 않습니다. 사실, goto L59
첫 번째 시도 블록 의 분기는 도달 할 수 없습니다. 첫 번째 Throwable catch는 모든 예외를 포착하기 때문입니다.
try{
CharArrayWriter br = new CharArrayWriter();
Throwable x = null;
try{
br.writeTo(null);
} catch (Throwable t) {goto L51;}
catch (Throwable t) {goto L59;}
if (br != null) {
if (x != null) {
try{
br.close();
} catch (Throwable t) {
x.addSuppressed(t);
}
} else {br.close();}
}
break;
try{
L51:
x = t;
throw t;
L59:
Throwable t2 = t;
} catch (Throwable t) {goto L59;}
if (br != null) {
if (x != null) {
try{
br.close();
} catch (Throwable t){
x.addSuppressed(t);
}
} else {br.close();}
}
throw t2;
} catch (IOException e) {
System.out.println(e)
}
8 개 지점을 모두 커버 할 수 있으므로 내 대답은 YES입니다. 다음 코드를보세요. 이것은 빠른 시도 일 뿐이지 만 작동합니다 (또는 내 github : https://github.com/bachoreczm/basicjava 및 'trywithresources'패키지 참조). 여기서 try-with- 리소스가 작동합니다. 'ExplanationOfTryWithResources'클래스 참조) :
import java.io.ByteArrayInputStream;
import java.io.IOException;
import org.junit.Test;
public class TestAutoClosable {
private boolean isIsNull = false;
private boolean logicThrowsEx = false;
private boolean closeThrowsEx = false;
private boolean getIsThrowsEx = false;
private void autoClose() throws Throwable {
try (AutoCloseable is = getIs()) {
doSomething();
} catch (Throwable t) {
System.err.println(t);
}
}
@Test
public void test() throws Throwable {
try {
getIsThrowsEx = true;
autoClose();
} catch (Throwable ex) {
getIsThrowsEx = false;
}
}
@Test
public void everythingOk() throws Throwable {
autoClose();
}
@Test
public void logicThrowsException() {
try {
logicThrowsEx = true;
everythingOk();
} catch (Throwable ex) {
logicThrowsEx = false;
}
}
@Test
public void isIsNull() throws Throwable {
isIsNull = true;
everythingOk();
isIsNull = false;
}
@Test
public void closeThrow() {
try {
closeThrowsEx = true;
logicThrowsEx = true;
everythingOk();
closeThrowsEx = false;
} catch (Throwable ex) {
}
}
@Test
public void test2() throws Throwable {
try {
isIsNull = true;
logicThrowsEx = true;
everythingOk();
} catch (Throwable ex) {
isIsNull = false;
logicThrowsEx = false;
}
}
private void doSomething() throws IOException {
if (logicThrowsEx) {
throw new IOException();
}
}
private AutoCloseable getIs() throws IOException {
if (getIsThrowsEx) {
throw new IOException();
}
if (closeThrowsEx) {
return new ByteArrayInputStream("".getBytes()) {
@Override
public void close() throws IOException {
throw new IOException();
}
};
}
if (!isIsNull) {
return new ByteArrayInputStream("".getBytes());
}
return null;
}
}
진짜 질문은 아니지만 더 많은 연구를하고 싶었습니다. tl; dr = try-finally에 대해서는 100 % 커버리지를 달성 할 수 있지만 try-with-resource에 대해서는 달성 할 수없는 것 같습니다.
당연히 구식 try-finally와 Java7 try-with-resources 사이에는 차이가 있습니다. 다음은 대체 접근 방식을 사용하여 동일한 것을 보여주는 두 개의 동등한 예입니다.
올드 스쿨 예 (시도 종료 접근 방식) :
final Statement stmt = conn.createStatement();
try {
foo();
if (stmt != null) {
stmt.execute("SELECT 1");
}
} finally {
if (stmt != null)
stmt.close();
}
Java7 예제 (리소스를 사용한 시도 접근 방식) :
try (final Statement stmt = conn.createStatement()) {
foo();
if (stmt != null) {
stmt.execute("SELECT 1");
}
}
분석 : 구식 예 :
Jacoco 0.7.4.201502262128 및 JDK 1.8.0_45를 사용하여 다음 4 가지 테스트를 사용하여 구식 예에서 100 % 라인, 교육 및 분기 범위를 얻을 수있었습니다.
- 기본 그리스 경로 (null이 아니고 execute ()가 정상적으로 실행 됨)
- execute ()에서 예외 발생
- foo ()는 예외 AND 문을 null로 반환합니다.
- null로 반환 된 문
Analysis: java-7 example:
If the same 4 tests run against the Java7 style example, jacoco indicates 6/8 branches are covered (on the try itself) and 2/2 on the null-check within the try. I tried a number of additional tests to increase coverage, but I can find no way to get better than 6/8. As others have indicated, the decompiled code (which I did also look at) for the java-7 example suggests that the java compiler is generating unreachable segments for try-with-resource. Jacoco is reporting (accurately) that such segments exist.
업데이트 : Java7 코딩 스타일을 사용하여, 당신은 100 % 혜택을받을 수있을 경우 Java7 JRE (아래 마 탸스 응답 참조)을 사용. 그러나 Java8 JRE와 함께 Java7 코딩 스타일을 사용하면 6/8 브랜치를 다룰 것이라고 믿습니다. 동일한 코드, 다른 JRE. Java8이 도달 할 수없는 경로를 생성하는 두 JRE간에 바이트 코드가 다르게 생성되는 것 같습니다.
네 살이지만 여전히 ...
- null이 아닌 행복한 경로
AutoCloseable
- null로 행복한 길
AutoCloseable
- 쓰기시 발생
- 닫기에 던짐
- 쓰기 및 닫기시 발생
- 리소스 사양에서 발생 ( with 부분, 예 : 생성자 호출)
try
블록 에서 던지지 만AutoCloseable
null입니다.
위의 7 가지 조건이 모두 나열되어 있습니다. 8 개 분기의 이유는 반복 된 조건 때문입니다.
모든 브랜치에 도달 할 수 있습니다. 이는 try-with-resources
상당히 단순한 컴파일러 설탕입니다 (적어도에 비해 switch-on-string
). 도달 할 수없는 경우 정의상 컴파일러 버그입니다.
단지 6 단위 테스트 실제로 아래 예 코드 (필요한, throwsOnClose
인 @Ingore
D 지점에 따르면이 8/8이다.
또한 Throwable.addSuppressed (Throwable) 는 자신을 억제 할 수 없으므로 생성 된 바이트 코드에는이를 방지하기위한 추가 가드 (IF_ACMPEQ-참조 동등성)가 포함되어 있습니다. 다행히도이 분기는 쓰기시 발생, 닫기시 발생 및 쓰기시 발생 및 닫기 사례로 처리됩니다. 바이트 코드 변수 슬롯은 3 개의 예외 처리기 영역 중 외부 2 개에서 재사용되기 때문입니다.
이것은 Jacoco의 문제 가 아닙니다 . 실제로 연결된 문제 # 82 의 예제 코드는 중복 된 null 검사가없고 닫기를 둘러싼 중첩 된 catch 블록이 없기 때문에 올바르지 않습니다.
8 개 분기 중 8 개가 포함 된 JUnit 테스트
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) {
//...
}
}
이 경우 생성 된 코드는 여전히 리소스 참조를 보호합니다. Syntatic sugar는 Java 9에서 업데이트되어 로컬 변수가 더 이상 필요하지 않습니다.try(arg){ /*...*/ }
보충-분기를 완전히 피하기 위해 라이브러리 사용 제안
Admittedly some of these branches can be written off as unrealistic - i.e. where the try block uses the AutoCloseable
without null checking or where the resource reference (with
) cannot be null.
Often your application doesn't care where it failed - to open the file, write to it or close it - the granularity of failure is irrelevant (unless the app is specifically concerned with files, e.g. file-browser or word processor).
Furthermore, in the OP's code, to test the null closeable path - you'd have to refactor the try block into a protected method, subclass and provide a NOOP implementation - all this just get coverage on branches that will never be taken in the wild.
I wrote a tiny Java 8 library io.earcam.unexceptional (in Maven Central) that deals with most checked exception boilerplate.
Relevant to this question: it provides a bunch of zero-branch, one-liners for AutoCloseable
s, converting checked exceptions to unchecked.
Example: Free Port Finder
int port = Closing.closeAfterApplying(ServerSocket::new, 0, ServerSocket::getLocalPort);
Jacoco has recently fixed this issue, Release 0.8.0 (2018/01/02)
"During creation of reports various compiler generated artifacts are filtered out, which otherwise require unnecessary and sometimes impossible tricks to not have partial or missed coverage:
- Part of bytecode for try-with-resources statements (GitHub #500)."
http://www.jacoco.org/jacoco/trunk/doc/changes.html
i had a similar issue with something like this:
try {
...
} finally {
if (a && b) {
...
}
}
it complained that 2 of 8 branches weren't covered. ended up doing this:
try {
...
} finally {
ab(a,b);
}
void ab(a, b) {
if (a && b) {
...
}
}
다른 변화가 없었고 지금은 100 %에 도달했습니다 ....
'IT TIP' 카테고리의 다른 글
Android에서 확장 가능한 패널을 구현하는 방법은 무엇입니까? (0) | 2020.12.01 |
---|---|
이름으로 스왑 파일을 찾았습니다. (0) | 2020.12.01 |
라 라벨 가드 란 무엇입니까? (0) | 2020.12.01 |
C #의 간단한 스레드 풀에 대한 코드 (0) | 2020.12.01 |
Mercurial에서 저장소의 하위 폴더를 어떻게 복제합니까? (0) | 2020.12.01 |