unittestフレームワークを使用する際、HTML形式のレポートを生成するために`HTMLRunnerCN.py`をダウンロードすることが多いです。しかし、自分でHTMLRunnerを作成することも可能です。
HTMLRunnerはunittestのTextTestRunner()を模倣して作成されます。まずはTextTestRunner()の動作フローを見てみましょう。
TextTestRunnerの使用方法
import unittest
suite = unittest.defaultTestLoader.discover("./")
with open("report.txt", "w") as f: # テスト結果をtxtファイルに保存
unittest.TextTestRunner().run(suite)
動作フロー
- TextTestRunnerは内部で`TextTestResult`(unittest.TestResultクラスを継承)を使用してテスト結果を記録します。
- `TextTestRunner().run()`は実際には`suite(result)`つまり`suite.run(result)`を呼び出します(resultは結果を記録するためのオブジェクト)。
- `suite.run(result)`はsuite内のテストケースを順番に走査し、それぞれの`case(result)`つまり`case.run(result)`を呼び出します。
- `case.run(result)`ではまず`result.testRun += 1`を実行し、その後テストメソッド`testMethod()`を実行します。テストが失敗、エラー、スキップされた場合、それぞれ`result.addSuccess()`, `result.addFailure()`などのメソッドが呼び出され、対応する`result.failures`や`result.errors`リストにテスト情報が追加されます。成功したテストについてはデフォルトでは何も処理しません。
- 最後にテスト結果オブジェクトであるresultを返します。
`unittest.TextTestRunner`とネット上のHTMLRunnerはどちらもstreamを利用してファイルに書き込む方式で、各テストケースの実行結果を逐次的にファイルに書き込んでいきます。この方法は多くの詳細な制御が必要で複雑です。
代わりに、テスト結果を解析し、Jinja2テンプレートエンジンを使ってデータをレンダリングすることで、レポートファイルを作成することができます。
Jinja2は外部パッケージであり、テンプレートコード中の{{変数名}}などのプレースホルダーに変数の値をレンダリングすることができます。ループやif文もサポートしています。インストール方法は`pip install jinjia2`です。
実装手順
- まずテンプレートを作成します。
- `{{title}}`, `{{description}}`は渡されたデータの対応する変数の値で置き換えられます。 - `{% for case in cases %}` ... `{% endfor %}`は`cases`リスト内の各テストケースデータを繰り返し処理し、それぞれの行を生成します。TPL = ''' <html lang="en"> <head> <meta charset="UTF-8"> <title>{{title}}</title> </head> <body> <h2>{{title}}</h2> <h3>{{description}}</h3> <br/> <table border="1"> {% for case in cases %} <tr> <td>{{case.name}}</td> <td>{{case.status}}</td> <td>{{case.exec_info}}</td> </tr> {% endfor %} </table> </body> </html> ''' - カスタムResultクラスを作成します。
- `addSuccess`などのメソッドはテストケースが成功または他の状態になったときに結果オブジェクトに対する操作を行います。 - `_exc_info_to_string`: テストから渡されるexec_infoは通常Tracebackオブジェクトであり、これを文字列に変換し、`\n`を`<br/>`に置き換えます。class CustomResult(unittest.TestResult): def __init__(self): super().__init__() self.cases = [] def addSuccess(self, test): self.cases.append({"name": test.id(), "status": "pass", "exec_info": ""}) def addError(self, test, exec_info): self.cases.append({"name": test.id(), "status": "error", "exec_info": self._exc_info_to_string(exec_info, test) .replace("\n", "<br/>")}) def addFailure(self, test, exec_info): self.cases.append({"name": test.id(), "status": "fail", "exec_info": self._exc_info_to_string(exec_info, test) .replace("\n", "<br/>")}) def addSkip(self, test, reason): self.cases.append({"name": test.id(), "status": "skip", "exec_info": reason}) - HTMLRunnerクラスを実装します。
class HTMLReportGenerator(object): def __init__(self, output, title="Test Report", description=""): self.file = output self.title = title self.description = description def run(self, suite): result = CustomResult() # テスト結果を保存する suite(result) # テストを実行 # データをテンプレートにレンダリング content = Template(TPL).render({"title": self.title, "description": self.description, "cases": result.cases}) with open(self.file, "w") as f: f.write(content) # ファイルに書き込み return result - 使用方法(いくつかのテストケースを準備)
suite = unittest.defaultTestLoader.discover("./") HTMLReportGenerator(output="report.html", title="テストレポート", description="テストレポートの説明").run(suite)
全体のコード
フォーマットを整え、実行統計情報を追加します。
import time
import unittest
from jinja2 import Template
TPL = '''
<html lang="en">
<head>
<meta charset="UTF-8">
<title>{{title}}</title>
<link rel="stylesheet" href="https://cdn.staticfile.org/twitter-bootstrap/4.1.0/css/bootstrap.min.css">
</head>
<body>
<div class="container">
<h1 class="pt-4">テストレポート</h1>
<h6>テストレポートの説明情報</h6>
<h6>実行: {{run_num}} 通過: {{pass_num}} 失敗: {{fail_num}} エラー: {{error_num}} スキップ: {{skipped_num}}</h6>
<h6 class="pb-2">実行時間: {{duration}}s</h6>
<table class="table table-striped">
<thead><tr><th>テスト名</th><th>状態</th><th>実行情報</th></tr></thead>
<tbody>
{% for case in cases %}
<tr><td>{{case.name}}</td><td>{{case.status}}</td><td>{{case.exec_info}}</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</body>
</html>
'''
class CustomResult(unittest.TestResult):
def __init__(self):
super().__init__()
self.success = []
self.cases = []
def addSuccess(self, test):
self.success.append(test)
self.cases.append({"name": test.id(), "status": "pass", "exec_info": ""})
def addError(self, test, exec_info):
self.errors.append((test, exec_info))
self.cases.append({"name": test.id(), "status": "error",
"exec_info": self._exc_info_to_string(exec_info, test)
.replace("\n", "<br/>")})
def addFailure(self, test, exec_info):
self.failures.append((test, exec_info))
self.cases.append({"name": test.id(), "status": "fail",
"exec_info": self._exc_info_to_string(exec_info, test)
.replace("\n", "<br/>")})
def addSkip(self, test, exec_info):
self.skipped.append((test, exec_info))
self.cases.append({"name": test.id(), "status": "skip",
"exec_info": self._exc_info_to_string(exec_info, test)
.replace("\n", "<br/>")})
def addExpectedFailure(self, test, exec_info):
self.success.append(test)
self.cases.append({"name": test.id(), "status": "pass",
"exec_info": self._exc_info_to_string(exec_info, test)
.replace("\n", "<br/>")})
def addUnexpectedSuccess(self, test):
self.failures.append((test, "UnexpectedSuccess"))
self.cases.append({"name": test.id(), "status": "fail", "exec_info": "UnexpectedSuccess"})
class HTMLReportGenerator(object):
def __init__(self, output, title="Test Report", description=""):
self.file = output
self.title = title
self.description = description
def run(self, suite):
result = CustomResult() # テスト結果を保存する
start_time = time.time()
suite(result) # テストを実行
duration = round(time.time() - start_time, 6)
print(len(result.success), len(result.failures))
# データをテンプレートにレンダリング
content = Template(TPL).render({"title": self.title,
"description": self.description,
"cases": result.cases,
"run_num": result.testsRun,
"pass_num": len(result.success),
"fail_num": len(result.failures),
"skipped_num": len(result.skipped),
"error_num": len(result.errors),
"duration": duration})
with open(self.file, "w") as f:
f.write(content) # ファイルに書き込み
return result
if __name__ == "__main__":
suite = unittest.defaultTestLoader.discover("./")
HTMLReportGenerator(output="report.html",
title="テストレポート",
description="テストレポートの説明").run(suite)