こんにちは。
仕事の方でテストカバレッジをGUIなしに集計する必要が出たので、
の集計をするために、PHPUnitが出力するClover形式のXMLと格闘して得られた、XMLの構造と扱い方についてまとめてみました
レガシーなPHPと戦っており、PHPUnitのバージョンは3.4です
最高につらい
なので出力されるXMLの構造や属性名に差異があるかもしれません
また、カバレッジレポートの出力方法はこちらのドキュメントを参照して下さい
なお、PHPUnitのカバレッジレポート単体ではC0のカバレッジしか計測できませんでした
後述するlineタグのnum属性とcount属性の値を使って対象プログラムの静的解析かければ、解析できなくはないかもしれませんが、
カバレッジレポート単体ではC0のレポートしか出ません。
この当時のPHPUnitには--coverage-clover
というオプションがあります
これがXML形式のカバレッジレポートを出力してくれるオプションです
XMLはざっくり、こんな感じになりました
<coverage>
<project>
<file name="ファイルパス">
<class
name="クラス名"
namespace="名前空間"
>
<!-- クラス単位でのメトリクス -->
<metrics
methods="クラス内のメソッド数"
coveredmethods="カバレッジ100%のメソッド数"
statements="クラス内の有効行数"
coveredstatements="クラス内の行カバーしている有効行数"
/>
</class>
<!-- ファイル内に定義されているクラスの分だけ上記繰り返し -->
<line
num="左記メソッドの定義開始行"
type="method"
name="メソッド名"
count="テストでこの行を通過した回数"
/>
<line
num="行番号"
type="stmt"
count="テストでこの行を通過した回数"
/>
<!-- メソッド定義のたびに type="method" name="..."が出現。それ以外は type="stmt" -->
<!-- ファイル単位でのメトリクス -->
<metrics
loc="ファイル内の有効行数"
ncloc="カバーされていない有効行数"
classes="ファイル内のクラス数"
methods="ファイル内のメソッド数"
coveredmethods="ファイル内の100%カバーされているメソッド数"
statements="ファイル内の定義行を除いた有効行数"
coveredstatements="行カバーされているファイル内の定義行を除いた有効行数"
/>
</file>
<!-- 対象カバレッジのメトリクス総まとめ -->
<metrics
files="カバレッジ集計対象のファイル数"
loc="カバレッジ集計対象の有効行数"
ncloc="カバレッジ集計対象のうちカバーされていない行数"
classes="カバレッジ集計対象のクラス数"
methods="カバレッジ集計対象のメソッド数"
coveredmethods="カバレッジ集計対象のうち100%カバーされているメソッド数"
statements="カバレッジ集計対象の有効行数(定義行を除く)"
coveredstatements="カバレッジ集計対象のカバーされている有効行数(定義行を除く)"
/>
</project>
</coverage>
有効行数
は、空白行やコメントアウトなどを除いた、PHPのコードとして評価される行数を指しています。
jsで書くと、
const el = document.querySelector('class[name="クラス名"][namespace="名前空間"]>metrics')
console.log(el.coveredstatements / el.statements)
に相当します
classタグ1つにつきmetricsタグが1つはいっているので、
目的のクラスの中にあるmetricsタグを抽出し、有効行数とカバーしている有効行数で比較できます。
namespaceを入れないと衝突する恐れがあります。
もしその辺考慮しなくていいならnamespace属性は無視できます。
ファイルも同じ要領で、fileタグ1つの直下にmetricsタグが1つ入っているので、それを比較します。
const el = document.querySelector('file[name="ファイルパス"]>metrics')
console.log(el.coveredstatements / el.statements)
ファイル名(basepath相当)ではなく、フルパスな点に注意です。
テストを実行(カバレッジ集計)した環境によって変わるのでご注意下さい。
カバレッジレポートはfileタグ単位で纏まっているので、ディレクトリごとのカバレッジを完璧に取ることは困難です。
もしソースの実体があれば ディレクトリの中身を漁って有効行数を出すことが可能ですが、
ソースの実体を持たない限り、カバレッジレポートに記載されているファイルしか計測対象になりません。
その不完全な状態であれば、
const metricses = document.querySelectorAll('file[name^="ディレクトリまでのパス"]>metrics')
const dirMetrics = metricses.reduce((acc, metrics) => ({
statements: acc.statements + metrics.statements,
coveredstatements: acc.coveredstatements + metrics.coveredstatements,
}), { coveredstatements: 0, statements: 0 })
console.log(dirMetrics.coveredstatements / dirMetrics.statements)
相当で取得可能です。
ディレクトリ内部のファイルのメトリクスをかき集めて、最後に合算すれば算出可能です
※前置きでも話しましたが、バージョンアップによって改善されている可能性もあります。あくまで古いPHPUnitについて言及します。
メソッド単位も、完全な情報は出せません
いや、正確にはメソッドに関するレポートなら出せます。が、関数に関するレポートが出せません しかも不完全な情報の収集ですら地味に面倒でした
lineタグのtype属性はstmt
かmethod
にしかならず、関数の定義開始行はtype=stmt
になってしまいます
関数対して判別可能な値が何もありません。計測不可能です
これもソースの実体があれば静的解析と絡めてレポート可能だとは思いますが、レポート単体では計測不可能でした
なので関数のレポートは出ないという前提で良ければ、
const classes = document.querySelectorAll('file[name="探したいファイル"]>class[]')
const statementCounts = classes.map(cls => parseInt(cls.querySelector('metrics').statements, 10))
const lines = Array.from(document.querySelectorAll('file[name="探したいファイル"]>line'))
let currentClass = classes[0].name
let currentMethod = null
let currentStatements = 0
let metrics = {}
lines.forEach(line => {
if (line.type === 'method') {
currentMethod = line.name
} else if (line.type === 'stmt') {
if (!metrics[currentClass]) {
metrics[currentClass] = {}
}
if (!metrics[currentClass][currentMethod]) {
metrics[currentClass][currentMethod] = { statements: 0, coveredStatements: 0 }
}
if (parseInt(line.count, 10) > 0) {
metrics[currentClass][currentMethod].coveredStatements += 1
}
metrics[currentClass][currentMethod].statements += 1
currentStatements += 1
} else {
throw new Error('知らないタイプ:' + line.type)
}
if (currentStatements === parseInt(classes[0].statements)) {
classes.shift()
currentClass = classes[0].name
currentStatements = 0
}
})
const methodMetrics = metrics[集計したいクラス][集計したいメソッド]
console.log(methodMetrics.coveredStatements / methodMetrics.statements)
ただし、これは不完全です
例えばクラスに属さない関数がファイルに含まれている場合に対応できません
完全なカバレッジを得るためにはソースコードと静的解析が必要です。
PHPUnitの設定ファイルにてカバレッジ集計対象の設定をして、
テストに登場しなかったファイルもカバレッジ集計対象に加えることは可能ですが、結局のところ関数には対応できません
結果的な感想としては「ツラい」のただ一言でした。