リソースでtryを使用するコードがいくつかありますが、jacocoでは半分しかカバーされていません。ソースコードの行はすべて緑色ですが、8つのブランチのうち4つしかカバーされていないことを示す小さな黄色のシンボルが表示されます。
私はすべてのブランチが何であるか、そしてそれらをカバーするコードをどのように書くかを理解するのに苦労しています。 3つの可能な場所がPipelineException
をスローします。これらは、createStageList()
、processItem()
、および暗黙のclose()
です。
createStageList()
から例外をスローするprocessItem()
から例外をスローするclose()
から例外をスローするprocessItem()
およびclose()
から例外をスローする他のケースは考えられませんが、まだ8分の4しかカバーされていません。
誰かがそれがなぜ8分の4であり、とにかく8つすべてのブランチにヒットする理由があるかを説明できますか?私はバイトコードの解読/読み取り/解釈に精通していませんが、おそらくあなたは... :)私はすでに見ました https://github.com/jacoco/jacoco/issues/82 、しかし、それもそれが参照する問題も非常に助けになりません(これはコンパイラが生成したブロックによるものであることに注意する以外)
うーん、これを書き終えた直後に、上記の説明ではどのケースがテストされないのかを考えました。私はこの質問と答えがどんな場合でも誰かを助けると確信しています。
EDIT:いいえ、見つかりませんでした。 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制御フローに対応していないため、gotoを使用する必要がありました。
ご覧のとおり、抑制された例外のさまざまな可能性を処理する多くのケースがあります。これらのすべてのケースをカバーできるのは妥当ではありません。実際、goto L59
最初のcatch Throwableがすべての例外をキャッチするため、最初のtryブロックのブランチに到達できません。
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-resourcesの仕組みを見つけるには、「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には違いがあります。代替アプローチを使用して同じことを示す2つの同等の例を次に示します。
Old Schoolの例(最後の試み):
final Statement stmt = conn.createStatement();
try {
foo();
if (stmt != null) {
stmt.execute("SELECT 1");
}
} finally {
if (stmt != null)
stmt.close();
}
Java7の例(try-with-resourceアプローチ):
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つのテストを使用して、Old Schoolの例で100%の回線、命令、およびブランチのカバレッジを取得できました。
分析:Java-7の例:
Java4スタイルの例に対して同じ4つのテストを実行すると、jacocoは6/8ブランチが(try自体で)カバーされ、2/2がtry内のnullチェックでカバーされることを示します。カバレッジを増やすためにいくつかの追加テストを試しましたが、6/8を超える方法はありません。他の人が示したように、Java-7の例の逆コンパイルされたコード(これも見てきました)は、Javaコンパイラーがtry-with-resourceの到達不能セグメントを生成していることを示しています。 (正確に)そのようなセグメントが存在すること。
更新: Java7コーディングスタイルを使用すると、100%のカバレッジを取得できる場合があります[〜#〜] if [〜#〜]Java7 JREを使用(以下のMatyasの応答を参照)。ただし、Java8 JREでJava7コーディングスタイルを使用すると、対象の6/8ブランチにヒットすると思います。同じコード、異なるJRE。バイトコードが2つのJRE間で異なる方法で作成され、Java8が到達不能な経路を作成しているようです。
4歳ですが、それでも...
AutoCloseable
を使用したハッピーパスAutoCloseable
を含むハッピーパスtry
ブロックにスローされますが、AutoCloseable
はnullです上記は7つの条件すべてを示しています-8つの分岐の理由は、繰り返される条件によるものです。
すべてのブランチに到達できます。_try-with-resources
_はかなり単純なコンパイラシュガーです(少なくとも_switch-on-string
_と比較して)-到達できない場合は、定義上コンパイラのバグです。
実際に必要なユニットテストは6つだけです(以下のサンプルコードでは、throwsOnClose
は_@Ingore
_ dで、ブランチカバレッジは8/8です。
また、 Throwable.addSuppressed(Throwable) はそれ自体を抑制することができないため、生成されたバイトコードにはこれを防ぐための追加のガード(IF_ACMPEQ-参照の等価性)が含まれます。幸い、この分岐は、バイトコード変数スロットが外側の2/3例外ハンドラー領域によって再利用されるため、スローオンライト、スローオンクローズ、スローオンライトアンドクローズのケースでカバーされています。
これは、notJacocoの問題です。実際、リンクされている issue#82 のコード例はないため、正しくありません。 nullチェックが重複しており、クローズを囲むネストされたcatchブロックはありません。
_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のサンプルコードには含まれていませんが、知る限りではテストできないケースが1つあります。
リソース参照を引数として渡す場合、Java 7/8では、割り当てるローカル変数が必要です。
_ void someMethod(AutoCloseable arg)
{
try(AutoCloseable pfft = arg) {
//...
}
}
_
この場合、生成されたコードは引き続きリソース参照を保護します。シンタックスシュガーは Java 9 で更新され、ローカル変数は不要になりました:try(arg){ /*...*/ }
確かに、これらのブランチの一部は非現実的なものとして書き落とすことができます-つまり、tryブロックがnullチェックなしでAutoCloseable
を使用する場合、またはリソース参照(with
)をnullにできない場合。
頻繁にアプリケーションは、どこで失敗したかを気にしません-ファイルを開く、書き込む、または閉じる-失敗の粒度は無関係です(アプリがファイルに特に関係していない限り、たとえば、ファイルブラウザまたはワードプロセッサ)。
さらに、OPのコードでは、nullのクローズ可能なパスをテストするために、tryブロックを保護されたメソッドにリファクタリングし、サブクラス化し、NOOP実装を提供する必要があります。 。
ほとんどのチェックされた例外ボイラープレートを扱う小さなJava 8ライブラリ io.earcam.unexceptional ( Maven Central で)を書きました。
この質問に関連します:AutoCloseable
sのゼロブランチ、ワンライナーの束を提供し、チェック済み例外を未チェックに変換します。
例:無料のポートファインダー
_int port = Closing.closeAfterApplying(ServerSocket::new, 0, ServerSocket::getLocalPort);
_
Jacocoは最近この問題、リリース0.8.0(2018/01/02)を修正しました
「レポートの作成中に、コンパイラによって生成されたさまざまなアーティファクトが除外されます。そうしないと、カバレッジが部分的または見逃されないようにするために、不必要で、時には不可能なトリックが必要になります。
私はこのようなもので同様の問題がありました:
try {
...
} finally {
if (a && b) {
...
}
}
8支店のうち2支店がカバーされていないと訴えた。結局これをやった:
try {
...
} finally {
ab(a,b);
}
void ab(a, b) {
if (a && b) {
...
}
}
他の変更はなく、100%に達しました。..