コード・カバレッジのコマンドライン・ツール

GraalVMでは、ユーザーがコードの特定の実行に関するソース・コード・カバレッジを記録および分析できるコード・カバレッジのコマンドライン・ツールが提供されています。

コード・カバレッジとはソース・コードの行、関数または文のカバーされている割合のことで、特定のソース・コード実行を理解するための重要なメトリックとして、一般的に、テスト品質(テスト・カバレッジ)に関連付けられます。コードの個々の行に対するカバレッジの概要が視覚的に提供され、コード・パスのうち、カバーされているものとそうでないものが示されることで、開発者が実行の性質に関する洞察を得て、今後のテストの方針の判断などに生かすことができます。

次のサンプル・アプリケーションを使用して、GraalVMのコード・カバレッジ機能を示します。このアプリケーションにより、エラトステネスのふるいアルゴリズムに基づいて基本的な素数計算機を使用してn番目の素数を計算する、getPrime関数を定義します。また、最初の20個の素数のある程度単純なキャッシュも含まれます。

  1. 次のコードをprimes.jsという名前の新しいファイルにコピーします:
class AcceptFilter {
    accept(n) {
        return true
    }
}
class DivisibleByFilter {
    constructor(number, next) {
        this.number = number;
        this.next = next;
    }
    accept(n) {
        var filter = this;
        while (filter != null) {
            if (n % filter.number === 0) {
                    return false;
            }
            filter = filter.next;
        }
        return true;
    }
}
class Primes {
    constructor() {
        this.number = 2;
        this.filter = new AcceptFilter();
    }
    next() {
        while (!this.filter.accept(this.number)) {
            this.number++;
        }
        this.filter = new DivisibleByFilter(this.number, this.filter);
        return this.number;
    }
}
function calculatePrime(n) {
    var primes = new Primes();
    var primesArray = [];
    for (let i = 0; i < n; i++) {
        primesArray.push(primes.next());
    }
    return primesArray[n-1];
}
function getPrime(n) {
    var cache = [2,3,5,7,11,13,17,19,23,29,31,37,41,43,47,53,59,61,67,71];
    var n = arguments[0];
    if (n > cache.length) { return calculatePrime(n); }
    return cache[n-1];
}
// TESTS
console.assert(getPrime(1) == 2);
console.assert(getPrime(10) == 29);

最後の数行は、ユニット・テストとして処理されるアサーションであることに注意してください。

  1. js primes.jsを実行します。すべてのアサーションが成功するため、サンプル・アプリケーションからの出力はありません。では、アサーションによる実装のテストはどの程度適切だったのでしょうか。

  2. js primes.js --coverageを実行して、コード・カバレッジを有効にします。サンプル・アプリケーションの出力は、コード・カバレッジ・ツールによって次のように出力されます:
    js primes.js --coverage
    --------------------------------------------------------
    Code coverage histogram.
    Shows what percent of each element was covered during execution
    --------------------------------------------------------
     Path               |  Statements |    Lines |    Roots
    --------------------------------------------------------
     /path/to/primes.js |      20.69% |   26.67% |   22.22%
    --------------------------------------------------------
    

    トレーサにより、各ソース・ファイルのカバレッジ・ヒストグラムが出力されます。文のカバレッジは約20%、行のカバレッジは約26%、ルート・カバレッジ(ルートという用語には、関数やメソッドなどが含まれます)は22.22%であることがわかります。これにより、このソース・コードについては、この単純なテストがあまり適切ではないことがわかります。次に、コードのどの部分がカバーされていないかを確認します。

  3. js primes.js --coverage --coverage.Output=detailedを実行します。出力はある程度詳細になります。出力をdetailedとして指定すると、すべてのソース・コード行が先頭にカバレッジ注釈が付いた状態で出力されます。出力が大量になる可能性があるため、この出力モードを使用する場合は、出力をファイルに直接出力する--coverage.OutputFileオプションと組み合せることをお薦めします。このサンプル・アプリケーションの出力は次のようになります:
js primes.js --coverage --coverage.Output=detailed
--------------------------------------------------------
Code coverage per line of code and what percent of each element was covered during execution (per source)
  + indicates the line is covered during execution
  - indicates the line is not covered during execution
  p indicates the line is part of a statement that was incidentally covered during execution
    e.g. a not-taken branch of a covered if statement
--------------------------------------------------------
 Path               |  Statements |    Lines |    Roots
 /path/to/primes.js |      20.69% |   26.67% |   22.22%

  class AcceptFilter {
      accept(n) {
-         return true
      }
  }
  class DivisibleByFilter {
      constructor(number, next) {
-         this.number = number;
-         this.next = next;
      }
      accept(n) {
-         var filter = this;
-         while (filter != null) {
-             if (n % filter.number === 0) {
-                     return false;
-             }
-             filter = filter.next;
          }
-         return true;
      }
  }
  class Primes {
      constructor() {
-         this.number = 2;
-         this.filter = new AcceptFilter();
      }
      next() {
-         while (!this.filter.accept(this.number)) {
-             this.number++;
          }
-         this.filter = new DivisibleByFilter(this.number, this.filter);
-         return this.number;
      }
  }
  function calculatePrime(n) {
-     var primes = new Primes();
-     var primesArray = [];
-     for (let i = 0; i < n; i++) {
-         primesArray.push(primes.next());
      }
-     return primesArray[n-1];
  }
  function getPrime(n) {
+     var cache = [2,3,5,7,11,13,17,19,23,29,31,37,41,43,47,53,59,61,67,71];
+     var n = arguments[0];
p     if (n > cache.length) { return calculatePrime(n); }
+     return cache[n-1];
  }
  // TESTS
+ console.assert(getPrime(1) == 2);
+ console.assert(getPrime(10) == 29);
--------------------------------------------------------

出力の先頭にある凡例で説明されているように、実行によってカバーされる行の前には+が付けられます。実行によってカバーされない行の前には、-が付けられます。部分的にカバーされている行の前にはpが付けられます(たとえば、if文の一方のブランチのみがカバーされている場合は、もう一方のブランチも付随してカバーされているとみなされます)。

出力を確認すると、calculatePrime関数とそのすべてのコールが実行されていないことがわかります。アサーションおよびgetPrime関数をもう一度確認すると、テストが常にキャッシュにヒットすることがわかります。したがって、ほとんどのコードは実行されません。このことは改善できます。

  1. primes.jsファイルの最後にconsole.assert(getPrime(30) == 113);を追加し、js primes.js --coverageを実行します。新しいアサーションにより、30が指定されたコールgetPrimeが追加されたため(キャッシュには20エントリのみが含まれています)、カバレッジは次のようになります:
js primes.js --coverage
-------------------------------------------------------
Code coverage histogram.
  Shows what percent of each element was covered during execution
-------------------------------------------------------
 Path               |  Statements |    Lines |    Roots
-------------------------------------------------------
 /path/to/primes.js |     100.00% |  100.00% |  100.00%
-------------------------------------------------------

他のツールとの統合

コード・カバレッジ・ツールは、他のツールと統合する方法を提供します。--coverage.Output=lcovを指定して実行すると、カバレッジ・データを表示するために複数のツール(genhtmlなど)で使用される、使用頻度の高いlcov形式で出力が生成されます。次の例では、Visual Studio Codeを使用してNode.jsアプリケーションのカバレッジを視覚化する方法を示します。

  1. 次のコードをnodeapp.jsという名前の新しいファイルにコピーします:
const express = require('express')
const app = express()
const port = 3000

app.get('/', (req, res) => {
  res.send('Hello World!')
})

app.get('/neverCalled', (req, res) => {
  res.send('You should not be here')
})

app.get('/shutdown', (req, res) => {
  process.exit();
})
app.listen(port, () => console.log(`Example app listening on port ${port}!`))
  1. expressモジュールの依存性をインストールします:
    $JAVA_HOME/bin/npm install express
    
  2. Visual Studio Codeを起動し、lcovをサポートするコード・カバレッジ・プラグインをインストールします。この例ではコード・カバレッジのハイライト・ツールが使用されていますが、他のプラグインも同様に機能します。

  3. カバレッジを有効にして構成し、nodeapp.jsファイルを実行します:
    $JAVA_HOME/bin/node --coverage --coverage.Output=lcov \
    --coverage.OutputFile=coverage/lcov.info \
    nodeapp.js
    

コード・カバレッジのハイライト・ツール・プラグインは、デフォルトでcoverageディレクトリにあるlcov.infoファイルを検索するため、コード・カバレッジ・ツールの出力をこのディレクトリに指定します。

  1. ブラウザでlocalhost:3000/にアクセスしてから、localhost:3000/shutdownにアクセスし、アプリケーションを閉じます。

  2. Visual Studio Codeを開き、nodeapp.jsファイルとcoverageディレクトリを含むフォルダを開くと、次のようなイメージが表示されます:

Visual Studio Codeカバレッジ

GraalVMコード・カバレッジ・ツールによって収集されたデータを独自の視覚化と統合する場合、--coverage.Output=jsonオプションを指定すると、出力はトラッカによって収集されたRAWデータを含むJSONファイルになります。