Django with unittest で CircleCI の Splitting by Timing Data を使う
概要
ルー大柴みたいなタイトルになってしまいましたが、Django で unittest を使用する場合に、 CircleCI で Splitting by Timing Data(過去のテストの実行時間を基にテストスイートを分割して並列実行)を行う方法について書いていきます。
ちなみに Django の unittest で実現するのは結構大変なので、unittest の資産がなくてこれから新しく Django のプロジェクトを始めていく方は落ち着いてdjango-noseを使いましょう。
この記事は unittest から逃れられない人の為の記事です。
CirleCI も
Django は、django-nose テストランナーを使用して設定する必要があります。
と言っています。
(ref: https://circleci.com/docs/ja/2.0/collect-test-data/#%E3%83%95%E3%82%A9%E3%83%BC%E3%83%9E%E3%83%83%E3%82%BF%E3%81%AE%E6%9C%89%E5%8A%B9%E5%8C%96)
そもそも Splitting by Timing Data とは
Circle CI にはテストを並列実行する仕組みがありますが、テストの分割方法にはいくつかの種類があります。
上から順に最適な分割方法になっていて、もちろん実際のテスト実行時間を基にして分割すれば(理論的には)どのコンテナでも同じような時間に終わるようにテストを分割できるわけです。
例えば3の方法だと、あるプロジェクトではこのぐらいのばらつきが発生してしまいました。
うまく平均すれば6分前後でJobが終了するはずですが、テストの分割方法が悪くて8分かかってしまっています。
これを実行時間を基に分割することでここまで平均化することができるのです。
Splitting by Timing Data 最高でしょ?
Django で Splitting by Timing Data をするには
結論
やらないといけないことが多いのですが、書いていきます。
再度言いますが unittest の資産がなくて、これから Django のプロジェクトを始める人は落ち着いて django-nose を使いましょう。
$ pip install unittest-xml-reporting
- run:
name: Generate test target file
command: |
./.circleci/get_all_test_class.sh > split_test_modules.txt
- run:
name: test
command: |
circleci tests split --split-by=timings --timings-type=classname < split_test_modules.txt | \
xargs ./manage.py test
- store_test_results:
path: unittest-results
#!/bin/bash
# テストファイルの中を見て、実行するクラスをすべて取得する
filelist=`find /path/to/tests -name 'test_*.py'
for file in $filelist; do
module=`echo $file | sed -e 's/\//\./g' | sed -e 's/\.py//'`
classes=`cat $file | grep class | grep -oP '(?<=class )\w+(?=\(.+)'`
for class in $classes; do
echo $module.$class
done
done
import xmlrunner
from xmlrunner.extra.djangotestrunner import \
XMLTestRunner as DjangoXMLTestRunner
class XMLTestRunnerForCI(DjangoXMLTestRunner):
test_runner = xmlrunner.XMLTestRunner
def run_suite(self, suite, **kwargs):
runner_kwargs = self.get_test_runner_kwargs()
# outsuffix='' にしないとテスト実行結果に実行時間のsuffixがついてしまって、CircleCIの --split-by=timings が有効にならない
runner = self.test_runner(outsuffix='', **runner_kwargs)
results = runner.run(suite)
if hasattr(runner_kwargs['output'], 'close'):
runner_kwargs['output'].close()
return results
TEST_RUNNER = 'path.to.xml_test_runner.XMLTestRunnerForCI'
TEST_OUTPUT_DIR = './unittest-results/unittest'
TEST_OUTPUT_FILE_NAME = 'results.xml'
上で何をやっているのか説明
JUnit XMLの出力形式について
CircleCIで実行時間を基にテストを分割するには、テストの実行時間の結果を JUnit XML か Cucumber JSON 形式で出力する必要があります。
unittest で JUnix XML を出力するためには unittest-xml-reporting ぐらいしか見当たらないのですが、これが CircleCI との相性が悪く、デフォルトで実行結果のログを [テストクラス名]-[timestamp]
の形式で吐き出します。
こんな感じ
<testsuite errors="0" failures="0" file="tests/test_huga.py" name="tests.test_huga.FooTest-20191211001126" skipped="0" tests="3" time="2.251" timestamp="2019-12-11T00:11:28">
次回CircleCIでテストを実行する際には、この file
か name
パラメータに対して実行テスト名を検索して実行時間を知る動きをします。
name
側はタイムスタンプが入っていて邪魔なので、file
の方を使いたいのですが、Django の unittest ではファイル名を引数にしてテストの実行ができません。。 ( $ ./manage.py test /path/to/test_file.py
ができない)
仕方ないのでname
に記されているテストクラス名を使う方針で、タイムスタンプを消しに行きます。name="tests.test_huga.FooTest"
になって欲しい訳です。
unittest-xml-reporting の実装該当部分を見ると、タイムスタンプを消すには XMLTestRunner
クラスの初期化で outsuffix
の引数に空文字 ''
を渡してあげれば良いことがわかりますが、このライブラリが提供しているDjango向け実装ではここへの変更に settings.py
などから手を加えられないようになっています。
そのため、上のコードに書いたように XMLTestRunnerForCI
を自分で実装して
runner = self.test_runner(outsuffix='', **runner_kwargs)
の部分だけ書き換えています。
これにより JUnit XML の出力形式がタイムスタンプなしのテストクラス名だけになりました。
テスト実行の引数に対して
続いて .circleci/get_all_test_class.sh
の説明です。
先程
次回CircleCIでテストを実行する際には、この
file
かname
パラメータに対して実行テスト名を検索して実行時間を知る動きをします。
と書きましたが、
$ ./manage.py test hoge.test_huga.FooTest
と実行するとXMLの name="hoge.test_huga.FooTest"
な部分を見に行ってくれる訳です。
つまりテストファイルの中身を全部見にいって [ファイル名].[クラス名]
の一覧を生成する必要があり、それをやってくれるのが .circleci/get_all_test_class.sh
です。
テストファイルの置き場所によっては少しコードを修正する必要があると思うので注意してください。
実行結果が以下のようになれば、期待通りです。
hoge.test_huga.FooTest
hoge.test_huga.FooTest1
hoge.test_huga.FooTest2
hoge.test_huga.FooTest3
hoge.test_bar.BarTest1
hoge.test_bar.BarTest2
hoge.test_bar.BarTest3
まとめ
ここまでやるのに1~2日ぐらいかかって気がします。まさかタイムスタンプが出ているとは思わずにJUnit XMLだしても過去の実行結果見つからないって言われるしどういうことなの〜とかなり悩みました。
(ファイルを書き出す場所が悪いのかなーとか…)
誰かの参考になれば幸いです。(最近みんなunittest使ってないのかな…)
Author And Source
この問題について(Django with unittest で CircleCI の Splitting by Timing Data を使う), 我々は、より多くの情報をここで見つけました https://qiita.com/showwin/items/796feae450f8848c4c73著者帰属:元の著者の情報は、元のURLに含まれています。著作権は原作者に属する。
Content is automatically searched and collected through network algorithms . If there is a violation . Please contact us . We will adjust (correct author information ,or delete content ) as soon as possible .