カテゴリー別アーカイブ: ユニットテスト

Pythonでテスト 連載(2) ユニットテストの書き方

Pocket

こんにちは。技術開発部データサイエンティストチームの西岡です。

前回の連載ではなぜユニットテストを書くのかについて述べました。
今回は実際にPythonのunittestモジュールを使用しながら、テストの書き方を説明しようと思います。

Pythonのユニットテストの話ではありますが、ユニットテストの思想自体は他言語でも通用するものでもありますし、Pythonの使用者以外の方も自身のよく使われる言語のユニットテストフレームワークと照らし合わせながら読んでいただければと思います。

なお、ここではPython3を使用しています。

テストケース

calc.pyはテスト対象のスクリプト、test_calc.pyはテストスクリプトです。

calc.py

tests/test_calc.py

ディレクトリ構造は以下のようであるとします。

test_calc.pyでは、calc.pyのadd_three関数にある入力を渡し、その出力が期待通りであるかをテストしています。 これをテストケースと呼びます。

unittestには

  • unittest.TestCaseクラスを継承したクラスにテストメソッドを記述
  • テストメソッドはtest_*で始める

というルールがありますが、これさえ守ればコマンドラインからテストを実行することができます。

この例ではもちろん5+3≠7なので、

上のようにテストが失敗します。 テストが通ったときは下のような表示になります。

テストフィクスチャ

ここで、Rectangleという長方形を表すクラスがあるとします。 Rectangleクラスには、

  • 長方形の幅・高さを取得するgetメソッド
  • 長方形の幅・高さを設定するsetメソッド
  • 長方形の面積を取得するareaメソッド

があり、インスタンスの使用が終わるとfinalizeメソッドで幅と高さをNoneにしなければならないものとします。

階層構造

rect.py

tests/test_rect.py

という感じになるかと思います。

しかし、これではrec = rect.Rectangle(5, 3)rec.finalize()を全テストメソッドで書かなければならず、非常に手間です。

unittestでは、このようなテストに伴って発生する初期化処理・終了処理をまとめるためにsetUp・tearDownメソッドを使用します。

tests/test_rect.py

これにより初期化・終了処理をまとめることができ、すっきりしたテストコードになります。

ただし、setUpとtearDownによってインスタンスの生成が1回にまとまったわけではないことに注意が必要です(以下の実行結果を参照)。

これはテストが以前におこなわれた何らかの実行に依存しないようにするためです。

テストの書き方

テスト対象の関数が複数のことをおこなう場合

テストの書き方は、テスト対象のコードにより大きく書き方が変わります。

以下の関数をどうテストすればよいかを考えてみましょう。

このcalc_average_bmi関数では、全員のBMIの平均を返します(BMIが何かについては、こちらのサイトを参照)。

この関数をテストする際に問題が起こりうる箇所は2つあります。

  • 返される平均の値が正しくないとき、BMI計算が間違っているのか平均計算が間違っているのか特定できない

  • 引数に空の配列が渡された時、平均計算においてZeroDivisionErrorになる

  • person.height == 0のときにBMI計算においてZeroDivisionErrorになる

下2つの「引数に空の配列が渡される」「身長が0である」といった例外はcatchすれば済みますが、そもそもcalc_average_bmi関数は

  • 平均計算
  • BMI計算

の2つのことをやっているため、関数自身はどんどん読みづらく、肥大化していきます。

そこで、BMI計算と平均計算を分離しましょう。

bmi.py

test_bmi.py

と分離すれば、bmi計算と平均計算は各々のメソッドで完結し、calc_average_bmiはほぼユニットテストを書く必要もないほど単純なコードになりました。

ここでは省きましたが、身長の単位がセンチメートルかメートルかもこのメソッドの中でテストすることができます。

メソッドに1つのことだけをさせるように変更すれば、テストもその1つのことだけを確認すればよくなるため、テストが非常に書きやすくなり、またテスト漏れも防ぐことができます。

プログラムをテスタブルにするため、テストが書きやすいようにプロダクションコードの設計から見直すことが重要です。

外部サービスのテスト

ここでいう外部サービスとは、データベースやWeb APIなどです。

git commit時のテストで毎回データベースを丸々スキャンするようなクエリを流すわけにもいきませんし、外部APIは(あまり無いかとは思いますが)落ちている可能性もあるので、外部サービスとの連携テストにはモックオブジェクトを使用するのが一般的です。

今回はモックを使用したテストについては述べませんが、いずれ取り上げてみたいと思います。

まとめ

テスタブルなコードを書くことでバグを減らし、より高速な開発をおこなっていきましょう。

Pythonでテスト 連載(1) なぜユニットテストを書くのか?

Pocket

こんにちは。技術開発部データサイエンティストチームの西岡と申します。

データサイエンスチームでは、CTR・CVR予測や、ユーザターゲティングの精度向上に日々取り組んでいます。

前回の伊良子の連載でも述べてある通り広告配信をおこなうアドサーバにはLuaやGoなどが使用されていますが、
データサイエンスチームでは分析がメインとなるためPythonを使用し始めました。 Pythonにはscikit-learnpandasなどの分析用ライブラリが充実しており、モデリング等を迅速におこなえるためです。

もちろん、分析のみならず製品への機能追加もPythonでおこなっています。 そこで、この連載ではPythonのユニットテストの書き方・テストフレームワークを紹介します。
まず第1回となる今回は、そもそもなぜユニットテストを書くのかについて説明します。

そもそも「テスト」とは?

「テスト」が何なのかということについて、wikipediaには

ソフトウェアテスト(software test)は、コンピュータのプログラムを実行し、 正しく動作するか、目標とした品質に到達しているか、 意図しない動作をしないかどうかを確認する作業のことである。

とあります。

要はデバッグのための工程です。 そもそも、テストという大仰な名前をつけずとも

プログラムを書く -> 動作確認する -> 正しく動かなければ修正 -> 再度動作確認する -> …

という流れはエンジニアであれば意識せずとも普段からやっていることです。

「ユニットテスト」とは?

ユニットテストが何なのかについてはこちらのサイトが参考になります。

単体テスト(ユニットテストと呼ばれることもあります)は、プログラムを構成する比較的小さな単位(ユニット)が個々の機能を正しく果たしているかどうかを検証するテストです。通常、関数やメソッドが単体テストの単位(ユニット)となります。

例をあげると、

という関数があったとして、

というように、ユニットテストでは外部のプログラムからテスト対象の関数やメソッドを呼び出し、期待した結果が得られるかを検証します。

なぜユニットテストを書くのか?

ユニットテストを書く利点について、いろいろあるかと思いますがここでは以下の3点をあげておきます。

  • プログラムの理解が容易になる

    ユニットテストが存在することで、テスト対象のコードの振る舞いが理解しやすくなります。

    get_nested_arrayは名前からは配列を返すのか?と予想してしまいますが、このテストがあることで辞書型を返すということがわかります。

    多人数が関わる開発では、自分が書いたプログラムを他の人が触る、あるいは他の人が書いたプログラムを自分が触ることは日常茶飯事のため、ユニットテストはプログラムの理解を助けることにつながります。

  • プログラムのリファクタも兼ねる

    複雑な関数やメソッドではテストすべき項目が多く、テストもどんどん複雑になっていきます。

    関数・メソッドに多くのことをさせすぎず役割を1つに絞っておくことでテストも簡潔で分かりやすいものになります。 はじめからユニットテストを書くことを念頭においておけば、設計を意識しながらコードを書くことにつながり、プログラムもすっきりしたものになります(参考: テスト駆動開発)。

  • プログラムの変更が容易になる

    プログラムに変更を加えると予期せぬ箇所にも影響が出て動作がおかしくなる、ということはままあります。 プログラムにユニットテストが付属していれば他の部分に影響が出てもテストが失敗するため、バグの早期発見につながります。

ユニットテストは「銀の弾丸」ではない

前項ではユニットテストはいいことずくめであるかのような書き方をしましたが、ユニットテストにも「限界」があります。

  • 工数がかかる

    製品のプログラムとは別にテスト用のコードを書かなければならないため、当然ながら工数がかかります。 プロジェクトの規模や人数を考えてどの程度まで網羅するべきかをその都度決めておく必要があるでしょう。

  • 全てをユニットテストでテストできるわけではない

    ユニットテストでのテストが向いているのは、関数やメソッドなどのプログラムを構成する最小単位です。

    例えばWebアプリケーションを開発していると、画面に期待した通りHTML要素が配置されるかもテストしなければなりません。 しかしユニットテストでは画面のテストをおこなうのは難しく、しかも画面はデザインの変更などにより頻繁に変わる部分であるため、テストコードを書くには向いていません。

ユニットテストを書くのは「手段」に過ぎない

前述した通りユニットテストは「銀の弾丸」ではありませんが、それでもメンテナンスコストの低下、開発速度の向上など、ユニットテストを書くことで多大な恩恵が受けられると思います。

しかし、ユニットテストを書く際には気をつけておくべきことがあります。
あくまでユニットテストを書くのは「手段」にすぎず「目的」ではないということです。

最大の目的はバグをなくすことです。

そのために必要なのは適切なテストケースを考えることです。たとえどれだけ変更がそのプログラムに加えられても、このプログラムはこう動いてほしいと明示化することです。

終わりに

本連載の第一回では、なぜユニットテストを書くべきかを中心に書きました。

次回の連載では、実際にPythonのユニットテストフレームワークを使ってどうテストを記述するのかを解説します。