ユニットテストツール「pytest」

Atsushi Odagiri氏(以下、Odagiri):では次にpytestです。pytestもけっこう有名になってきたツールです。flake8とかmypyは静的チェックのツールなので、あいつらは実行させない、つまりコードの内容として「何か危ないぞ」とか「型違うよ」というのをチェックしてくれるツールです。pytestは結局のところユニットテストツールなので、テストで実際にコードを動かしてエラーが出るかというのをやりますが、もちろんみなさんユニットテストはやっていらっしゃいますよね。挙手。

(会場挙手)

はい。pytestを使っている人。

(会場挙手)

両方まだ混ざっている。そんなもんですよね。まずpytestはどういうところがツールとして優秀かというと、エラーが起こった時に、そのエラーの周辺の変数の内容まで表示してくれるという詳細なテストレポートがあります。詳細すぎるので、たくさんエラーが出るとがんばってスクロールしないといけなくなっちゃうんですが。

あとユニットテストとの違いでいえば、アドオンがあること。そしてfixtureの仕組みです。unittestでは、用意をするsetUpと片付けをするtearDownという2つのメソッドがバラけています。pytestではコンテキストマネージャの仕組みでfixtureをするので、yieldを挟むんです。準備してyieldして、そうするとテストが終わったあとにまたこのyieldのところに戻ってくるので、ここで後片付けをする。準備と後片付けを一緒にしたfixtureというのをちゃんと書けるんです。

たまに「setUpで呼ぶけど、こいつはtearDownで呼ぶ必要はないのか?」みたいなやつとかありますよね。そういうのが2つセットになってfixtureとして使えるというのがpytestのfixtureのいいところです。

あとは、始めようとするとTestCaseクラスを継承してテストメソッドの中でassert〜というメソッドを使ってチェックをするというのがunittestなんですけど、そこもういいよねと。test〜という関数だったらもうテストだし、その中でassertすれば詳細なテストレポートの中で変数の中身がわかるので、いちいちassert〜というのをいろいろ使い分けなくてもいいでしょうと。これが、pytest側のいいところだという話です。

unittestはunittestで良いところはあるのですが、今日はpytestの話なので、pytestだけお話します。

Pytestのインストールは、「pip install pytest」です。

昔はpyというライブラリの中に入っているtestというライブラリだったんですけど、みんなpytestしか使わないので分離されているわけです。pytestはアドオンが使えます。どんなアドオンがあるかというと、まずpytest-cov、coverage収集。ユニットテストしたらみなさんcoverageは取りますよね。取っていないユニットテストを実行している人?

(挙手なし)

さすがにいないね。あとは、アドオンですとpytest-djangoがありまして、いろいろできるやつです。pytest-mockは、ユニットテストのモックが中にあります。fixtureとしてパッチをしたものをちゃんときれいにしてくれるとか、結局一度も呼ばれていないモックについて警告してくれたりするので、使うといいと思います。

あとpytest-freezegunは時間関係です。mock datetimeするためのものですけど、これについてのpytest fixtureで使うことができます。いろんなアドオンがあるので調べてみるといいでしょう。どうせそのうち手作りすることになりますがね(笑)。

pytest-randomlyでテスト順序をシャッフル

今日おすすめしたいpytestのアドオンは、pytest-randomlyです。これが何をするかというと、random seedを変更するのがまず1つ。そしてもう1つ、一番重要なのが、テスト順序をシャッフルするということです。

さあ、なぜテスト順序をシャッフルする必要があるのでしょうか? 例えばテスト対象で、このように固定のFILENAMEという定数の中に、何か受け取った内容をJSONで書き込むやつとJSONを読み込むやつがあります。すごいアホなことをやっているんですけど、そこは目を瞑ってください。

ではこれのテストを書きましょう。テストを書いちゃいました。まずtest_saveのほうはsaveメソッドを呼んで実際そのファイルの中身が正しいかを見るものですね。そしてtest_loadのほうですが、loadを呼んだresultの中身が書き込まれているというアホなことをやっていますが……。

これは非常に危険なテストと言いますか、「何をやっているの?」というテストなんですが、それでもユニットテストをやり始めるとこんなコードもたくさん出てきます。

まず何が問題かというと、test_loadはtest_saveの実行後じゃないと動かないんです。いきなり呼んでも動かないんですね。でもこのテストを実行すると、test_saveを呼んでtest_loadを実行するという、この順番で呼ばれてしまうので、うまくいってしまうわけです。

でも実際にはtest_saveを単独実行すると落ちてしまう。だけど、CI上ってわざわざ単独実行なんてしないので、全部実行するわけです。そうすると落ちないんですね。ということは、「じゃあこれはいつも全部テストを実行しないといけないのか?」という話になってしまう。対策として、テスト実行順序をシャッフルすることによって、順序依存のテストを発見することができる。

さっきのテストを書いたやつです。pytest-randomlyが入っていない状態で「pytest .\test_loader.py」とやって、普通に実行すると、test_save→test_loadという順番で実行させるのでうまくいってしまう。

ではここでテストを指定します。pytestがテストを1個実行するための書き方はこうなります。コロンでつないで関数を書いてください。これでちゃんとテストを実行できます。test_loaderだけ実行しようと思ったら、「pytest .\test_loader.py::test_load」となります。

ただ、今これを実行してみると実際にうまく動いてしまいます。さっきテストを実行したので、ちゃんとファイルが存在するので動いてしまうんですね。「なんで俺がfixtureの代わりをしなきゃいけないんだ」という憤りとともに、ちゃんと消してからやってみてください。そうするとエラーになるわけですね。

pytest-randomlyを入れましょう。「$ pip install pytest-randomly」とすると、pytestを実行したら自動でrandomlyがかかります。

ここでシャッフルされるのでたまにうまくいくわけですね。今回の場合2つテスト関数があるので、2分の1の確率でうまくいく。だから3回か4回ぐらいやって、だいたいうまくいけば「とりあえずいいんじゃないかな?」という感じなので、テスト順がシャッフルされるのを複数回実行して、ちゃんとうまくいったりうまくいかなかったりというのを確認しましょう。

テスト用virtualenvを管理する「tox」

次はtoxです。先ほどのpytestはテストランナーでしたが、toxは何かというと、テスト用の環境をいい感じで作ってくれるツールです。toxを使うとテスト用にvirtualenvを作ってその中でテストをするということができます。

あとその中でどういうテストを実行するかも設定で書いておけるので、これをつかってまずvirtualenvを切っていくので、それぞれバージョンの違うPythonのテストをするというのがまず簡単にできるようになります。プロダクションコードではそんなに使うことはないとは思いますが、ライブラリ作成者はだいたいtoxを使って、サポート対象のPythonバージョンを明確にするということをします。

あと、CI上でもtoxを一発呼ぶだけと設定しておくと、CI上でやっているテストをローカルでもすぐにtox一発で実行できます。CI上でしかできない複雑な手順を踏んだテストみたいなやつで落ちて、「ローカルで再現しねぇ」みたいなことになると「もう! あー!」となるわけですよ。そういうことがないようにtoxを使ってこのテストフローをきれいにしておくとまた効率アップできます。

toxをインストールする方法は「pip install tox」です。このへんのツールでpip installがうまくいかないようなツールはほとんどありませんので、安心してみなさん使ってくださいね。

tox.iniでどういうことを書くかというと、toxセクションでテストしたいバージョンを書いておきます。ここでpy36と37と書いてあるので、Python3.6と3.7に対するテストを、それぞれのバージョンのvirtual envを作って、その中でpytestをインストールしてあげて、pytestコマンドを実行すると。

設定ファイルがあって、ここでtoxと打ってあげると、3.6用のvirtual envを作って、その中にpytestを入れて、あと……あっ、skipsdistと書くの忘れた……。まあ、いいや(笑)。あと今開発中のパッケージもそこにインストールして、pytestコマンドを実行するというのを、Python3.6と3.7両方に対して実行してくれます。これが、手元でもtoxと打てばいいし、CI上でもtox一発でこのテストを実行することができる。

「手元に3.6ないので……」というときは、-eオプションを使ってenvを指定する。Python3.7だけ実行したければ、tox -e py37とやれば、その環境だけの実行になります。

エディタからツールを使えるようにする

以上、こういった効率化をするためのツールがありますが、こういったツールはまずCIで実行しないとみんなちゃんと守らないので、CIで実行します。これはレビュー前の最後の水際なので、ここでは必ず全部通しましょう。

でもCIに言われてからまた直すというと、「commitしてpushして何か実行されるのを待って……」みたいな感じで。最近CIもタスクが流れてくるのが遅いですから、それを待って「あっ、通った」とやるのは遅いです。

なのでもう一歩手前でいきましょう。そうすると、pre-commitです。commitフックもしくはpushのフックで走らせるというのがあります。

ただし、そこでもけっこうだるいです。そしたら、まずcommitする前にコマンドラインで手動実行しましょう。でも手動実行はそのうち忘れます。なので、エディタ上で自動で実行してくれて、編集したら即見えるというような感じにしましょう。

これでエディタからツールを使えるようにする。どういうときにするかというと、編集や保存と同時に警告したりフォーマッティングしてもらうようにしておけば、もう忘れないし、その場で直せる。「このcという変数使ってなくない?」ってピッと出てくれば「あっ!」と気づいてすぐ直せますよね。

あと、自分で実行して、たまに忘れてCIで落ちてがっかりというのはありますね。しかもCIけっこう遅いのに。しかもSlackでぴょーんと「落ちたよ」って感じで飛んできて、「あー」ってみんなの前で赤っ恥かくみたいな感じになるので。「お前、flake8落ちたぜ?」みたいな。そういうことがなくなります。エディタにツールを設定しましょう。

みなさんが使っているエディタはたぶんVS Code、emacs、Vim……。PyCharmはエディタかどうかわからないけど、そのぐらいかな? ほかのエディタを使っている人は信念があってやっていると思うので、がんばってちゃんと設定してあげてください。

エディタでどういう設定をしておくのが良いかというと、まず編集中にflake8やmypyの警告がバンバン出て直せるようにする。あと、保存したらすぐblackでフォーマットされるようにする。あとはテスト実行をエディタの中でやるとエラーメッセージが出ると思うんですけど、そこからすぐにそのエラーの箇所に飛べる機能も設定しておくとよいです。

あと、テストでカバーされた行がマーカーで表示されるというのまでいけるとすごくいいです。「あ、ここテスト通ってない」というのがすぐわかります。

VS codeでそれをやろうとすると、linting.flake8.enabled、mypy.enabled。……pylintはだいたいお役御免なのでenabled false、インストールしたらすぐにやる設定です。python.format.provider = “black”にして、あとformatOnSaveというのをやると、保存時にフォーマットが上がります。

次にemacsの場合は、静的チェックのフレームワークでflycheckとかflymakeがあります。そこを通して「flake8を使う」というのを入れます。flycheck-mypyとかもありますし、今後LSPのほうに寄っていくと思いますので、そこらへんはみなさんで調べてください。「emacsのLSP、まだちょっとなぁ……」とも思うので、まだ伝統的なsaveフックを使った設定方法がいいと思います。

Vim。使ってないのでわかりませんが、たぶん何かあるでしょう。

PyCharm。使ってないからわかりませんが、だいたいできるでしょう。

というわけで、今日はいろんなツールを説明しました。覚えて帰ってほしいことは、退屈なことは機械がやってくれますので、みなさん機械にガンガンチェックさせましょう。レビュアーの時間を奪わないために、機械を使いましょう。

レビュアーももっと突っ込んだレビューをしたいのに、その前段階のことがあると心がざわついて少し攻撃的になってしまうなんてこともあるわけです。flake8をチェックすればわかるようなことがレビュアーの目の前に来たら、ちょっと怒りたくもなるわけです。

そう言われたほうも直すのがちょっと嫌になって、お互いの精神衛生上良くないですから、事前に機械にチェックさせて、指摘されたところをそのまま直すということをやりましょう。

あとは、自分でやってみてほしいこととして、まだプロジェクトにこういったツールを導入していない場合は、「プロジェクトにこういう効果があるんですよ!」と導入してみましょう。CIを回しているプロジェクトであればCIに入れてみましょう。

というわけで、これでビギナーセッションの、開発を効率化するツール設定のお話を終わります。

(会場拍手)

toxのタスクをどう構成するか

司会者:ありがとうございました。せっかくの機会ですのでご質問ある方。はい。

質問者1:発表ありがとうございます。flake8とかblackとかmypyでチェックをかけるとき、tox.iniだとかsetup.cfgとかで書いていると思うんですが、それってflake8コマンドを一発で書くのか、toxでenvironを指定してflake8とかblackみたいなかたちで個別に指定する方法がいいのか。

例えばblackとmypyとflake8を一括でチェックするような設定がいいのか。flake8ならflake8であるとを一発でわかるようにしたほうがいいとか、こんなのがおすすめとかありますでしょうか。

Odagiri:toxのタスクをどう構成するかという話?

質問者1:はい。

Odagiri:大体やることは変わらないので、lintingとかそんな感じのタスクを作って、その中でもうflake8、mypy、blackみたいな感じでいっぺんに動かしちゃってますね。

質問者1:なるほど。ありがとうございます。

司会者:ほかにご質問のある方? 

Odagiri:ないですかね。

司会者:はい。じゃあご質問がなければ終わりに。

Odagiri:いいですかね。一応今日PSFブースの周りをうろうろしているときであれば質問を受け付けますので、そこらへんにいたら質問してみてください。

司会者:すみません。運営関係の質問ですけど、この資料はのちほど「#pyconjp」タグでTwitterに公開されますか?

Odagiri:例年どおり。

司会者:例年どおりしていただけるということで。ありがとうございます。

Odagiri:SlideshareのアップとURLをconnpassのに上げておきます。

司会者:ありがとうございます。connpassのほうに資料のURLが上がるそうですので、復習などをしていただけたらいいんじゃないかなと思います。

とくに質問等ないようでしたら、これでビギナーセッションを終わりたいと思います。Odagiriさん、どうもありがとうございました。

(会場拍手)