web-dev-qa-db-ja.com

リソースを試すための8つのブランチ-jacocoカバレッジは可能ですか?

リソースでtryを使用するコードがいくつかありますが、jacocoでは半分しかカバーされていません。ソースコードの行はすべて緑色ですが、8つのブランチのうち4つしかカバーされていないことを示す小さな黄色のシンボルが表示されます。

enter image description here

私はすべてのブランチが何であるか、そしてそれらをカバーするコードをどのように書くかを理解するのに苦労しています。 3つの可能な場所がPipelineExceptionをスローします。これらは、createStageList()processItem()、および暗黙のclose()です。

  1. 例外をスローしない、
  2. createStageList()から例外をスローする
  3. processItem()から例外をスローする
  4. close()から例外をスローする
  5. processItem()およびclose()から例外をスローする

他のケースは考えられませんが、まだ8分の4しかカバーされていません。

誰かがそれがなぜ8分の4であり、とにかく8つすべてのブランチにヒットする理由があるかを説明できますか?私はバイトコードの解読/読み取り/解釈に精通していませんが、おそらくあなたは... :)私はすでに見ました https://github.com/jacoco/jacoco/issues/82 、しかし、それもそれが参照する問題も非常に助けになりません(これはコンパイラが生成したブロックによるものであることに注意する以外)

うーん、これを書き終えた直後に、上記の説明ではどのケースがテストされないのかを考えました。私はこの質問と答えがどんな場合でも誰かを助けると確信しています。

EDIT:いいえ、見つかりませんでした。 RuntimeExceptionsのスロー(catchブロックで処理されない)は、これ以上ブランチをカバーしませんでした

61
Gus

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)
}
56
Antimony

enter image description here

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;
  }
}
8

本当の質問はありませんが、もっと研究をそこに放り出したかったのです。 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%の回線、命令、およびブランチのカバレッジを取得できました。

  • 基本的なグリースパス(ステートメントはnullではなく、execute()は通常どおり実行されます)
  • execute()は例外をスローします
  • foo()は例外をスローし、ステートメントはnullとして返されます
  • nullとして返されたステートメント

分析: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が到達不能な経路を作成しているようです。

6
Jeff Bennett

4歳ですが、それでも...

  1. Null以外のAutoCloseableを使用したハッピーパス
  2. Null AutoCloseableを含むハッピーパス
  3. 書き込み時にスロー
  4. 閉じる時にスロー
  5. 書き込みおよびクローズ時にスロー
  6. リソース仕様のスロー(with部分、例えばコンストラクター呼び出し)
  7. 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ブロックはありません。

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)));
        }

    }
}
_

Eclipse Coverage

警告

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.unexceptionalMaven Central で)を書きました。

この質問に関連します:AutoCloseablesのゼロブランチ、ワンライナーの束を提供し、チェック済み例外を未チェックに変換します。

例:無料のポートファインダー

_int port = Closing.closeAfterApplying(ServerSocket::new, 0, ServerSocket::getLocalPort);
_
5
earcam

Jacocoは最近この問題、リリース0.8.0(2018/01/02)を修正しました

「レポートの作成中に、コンパイラによって生成されたさまざまなアーティファクトが除外されます。そうしないと、カバレッジが部分的または見逃されないようにするために、不必要で、時には不可能なトリックが必要になります。

  • Try-with-resourcesステートメントのバイトコードの一部(GitHub#500)。」

http://www.jacoco.org/jacoco/trunk/doc/changes.html

2
John Bedalov

私はこのようなもので同様の問題がありました:

try {
...
} finally {
 if (a && b) {
  ...
 }
}

8支店のうち2支店がカバーされていないと訴えた。結局これをやった:

try {
...
} finally {
 ab(a,b);
}

void ab(a, b) {
 if (a && b) {
...
 }
}

他の変更はなく、100%に達しました。..

1
mdeanda