Pythonで学ぶ 基礎からのプログラミング入門 (34) Pythonのテスト手法

ニュース
トップ

【連載】

Pythonで学ぶ 基礎からのプログラミング入門

34 Pythonのテスト手法

伊藤裕一  [2016/01/20]
  • >
  • >>

34/34

今回からはプログラムを書くというよりも書かれたプログラムが期待どおりに動いているかどうかを確認する手法について扱います。

確認する手法はおおまかに2つあり、ひとつはプログラムを「書いている段階」でそれが期待どおりに動くかどうかを確認する「テスト」と呼ばれる手法です。もうひとつは書かれたプログラムが期待どおりに動いていないことがわかった「あと」で行う「デバッグ」と呼ばれる手法です。

あまりにもバグだらけのコードをデバッグするのは時間がかかる大変な作業なので、理想的には「現在開発している小さいパーツ単位でのテスト」をパスしたコードをどんどん結合していくというのが一般的な開発の流れになると思います。テストで見つかった「パーツを書いているとき」の問題の修正は比較的容易な場合が多いです。

テストに通ったうえでトラブルが発生した場合に、デバッグで何が原因で問題が発生しているかを特定し修正します。デバッグで大規模なプログラムから問題を見つけて修正することも可能ではありますが、テスト時の修正に比べて難易度が高いため、可能な限りテスト段階でバグを潰してしまうことを心がけてください。

本記事ではまずはじめにテスト手法について扱います。

assertを使ったテスト

テストでは、開発段階でプログラムの問題点を洗い出すことで品質を向上させます。テストの基本的なスタンスは「特定のコンポーネント(パーツ)に入力を与えた場合に期待される出力(挙動)はしているか」「パフォーマンスなどにも問題はないか」といったことの確認です。

実はこのテストは例外処理のなかで扱った「assert」と呼ばれる仕組みで実現できます。本格的なテストをするには少し非力ですが、簡単なのでシンプルなプログラムはこれで十分かもしれません。

assertの基本的な使い方は、assert文の中がTrueになるかの確認です。まず、TrueとFalseの挙動の違いについて確認してみます。

>>> assert(True)
>>> assert(False)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AssertionError

みてもらうとわかるようにassert関数の引数がTrueだと何も問題が起きていません。そしてFalseの場合はAssertionErrorが発生しています。スクリプトを実行している場合はtry/catchしていなければここで処理が中断されます。

また、メッセージ付きでassertエラーを出したい場合は () を外して以下のように書きます。

>>> assert True, 'Hello'
>>> assert False, 'Hello'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AssertionError: Hello

このassertをプログラムの本文中の「テストをしたい場所」に挟みこんだり、関数やクラスをテストするためのコードを新たに書いてそこで利用したりします。その際、先ほどのFalseになると問題を検知できる仕組みを利用して、

assert(テストしたい変数等 == 期待される値)

などとすることで、テスト項目の値が期待される値になっているか確認できます。

仮にこの変数が期待されていない値になっていたら、AssertionErrorが発生するのでどこに問題があるかすぐにわかります。期待されない値を持ったままプログラムが稼働してエラーが起きるより、「期待されない値を持っているとわかったタイミング」でわざとエラーにしてしまいます。

そうすることで問題の根本原因の特定が容易になります。なお、当然ながらassertの中には == だけでなく、ほかのBool値を返す式である != や in なども使えます。

>>> assert(5 in range(10))
>>> assert(15 in range(10))
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AssertionError

ほかには関数の引数のチェックなどにも使えます。

def myfun(a):
  assert(type(a) == type(5))
  print(a)

myfun(3)
myfun('hello') # ERROR

上記は引数の型が期待されるもの(int)になっているかチェックしています。

unittestを使ったテスト

先ほどのassertは単純な文のレベルにおける「値のチェック」に過ぎませんでした。もう少し体系化されたテストをするにはテストをするための「専用のツール」を利用すべきです。これを使うことによって、たとえばテストの内容が明確になり、テストの漏れなどもわかりやすくなります。assertをプログラムの本文に埋め込んで使う方法は、プログラム本体のコードのなかにテスト用のコードが混ざってしまいます。きちんとルールを守ってassertを使うぶんには問題ないのですが、そこまでするのであれば思い切ってツールに頼るというのもひとつの解決策かもしれません。

今回紹介するツールは「ユニットテスト」と呼ばれるもので、これはさまざまなプログラミング言語で実装されています。ツールといってもコードの中に「テストを実施するクラス」を書き、それを使ってテストをするだけですのでそれほど難しくはありません。

Pythonでユニットテストを行うにはunittestというそのままな名前のモジュールを使います。これはJavaのJUnitを参考にして開発されており、ほかの言語のユニットテストもこのJUnitと似ているので、その根本概念を覚えれば使い回しができる場合が多いです。なお、Pythonにはunittest以外にもさまざまなテスト手法やモジュールがありますが、それらはあくまでもunittestを知っていることを前提に作られている場合が多いです。そのため、最終的に別のものを使いたい場合でもベースとなるunittestについてはある程度は知っておいたほうがよいと思います。

さて、話が長くなったのでさっそくunittestの使い方に移りましょう。ユニットテストは「プログラムとして実行したいコード」に対応するかたちでテスト用のコードをunittestの継承クラスとして記載します。そのテスト用のクラスの中で以下に記載されるようなメソッドを使ってプログラムが期待どおりの「返り値」を持っているか、挙動を行うかを確認します。

  • assertEqual()
  • assertTrue()
  • assertIs()
  • assertRaise()

このほかにもいろいろあるのですが、assertRaise以外のものはそれほど難しくなく、assertEqualは同一か、assertTrueはTrueか、assertIsはIS関係かどうかといったことをチェックします。assertRaiseは「期待どおりに例外が発生するか」を調べるものです。

今回は既存のrandomモジュールの挙動のテストをしてみます。既存のクラスのテストは多くの人に無縁かと思います。そのような場合はこのrandomモジュールの代わりに、自分が書いたクラスを使うことになります。

import random
import unittest

class TestSequenceFunctions(unittest.TestCase):

    # INITIALIZE
    def setUp(self):
        self.seq = range(10)

    # TEST 1
    def test_shuffle(self):
        random.shuffle(self.seq)
        self.seq.sort()
        self.assertEqual(self.seq, range(10))
        self.assertRaises(TypeError, random.shuffle, (1,2,3))

    # TEST 2
    def test_choice(self):
        element = random.choice(self.seq)
        self.assertTrue(element in self.seq)

if __name__ == '__main__':
    # START TEST
    unittest.main()

上記プログラムは import、クラス定義、そして最下部の実行コードに分かれています。以前話したことがありますが、このPythonスクリプトから起動された場合は特殊な変数である__name__が '__main__' となっています。その場合のみ、このif文がTrueとなります。unittest.main()がユニットテストの実行コードです。

定義したクラスの詳細については後述するのでひとますおいておいて、とりあえず実行してみましょうか。

# python test.py
…
----------------------------------------------------------------------
Ran 2 tests in 0.000s

OK

上記の出力から以下のことがわかります。

  • エラーは発生しなかった
  • 2つのテストを実行した
  • 実行時間は0.000sだった

このエラーの発生の有無のチェックに使っているのが先にお伝えした3つのassert系のメソッドです。そしてそれらのメソッドが使われているのが、自分で定義した「test_XXXX」という名前の2つのメソッドです。

ユニットテストの実施の仕方は以下のようになります。

  • unittestのTestCaseクラスを継承
  • そのなかでassertメソッドを使うtest_XXXXというメソッドを定義する
  • テストを実施する関数を呼び出すと、作成したクラスのtest_XXXXというメソッドをテストする
  • 各testメソッドがassertに通った(OK)か否か(Fail)が表示される

まずはこれだけ理解してください。

上記のtestの出力が味気なく、より詳細を見たいと思われるのであれば、先ほどのmain()メソッドの呼び出しを以下のようにすれば詳細が表示されます。

if __name__ == '__main__':
    suite = unittest.TestLoader().loadTestsFromTestCase(TestSequenceFunctions)
    unittest.TextTestRunner(verbosity=2).run(suite)

上記は基本的にコピペでかまいません。プログラムの意味まで理解する必要はないですが、簡単にいうと「作ったテストケースのクラスをロード」し、テストスイートを作る。そしてそのテストスイートを「冗長な表示の指定」とともに実行という流れです。

この出力は以下のように変化します。どのメソッドをテストして、結果がどうであったか教えてくれていますね。

# python test.py
test_choice (__main__.TestSequenceFunctions) ... ok
test_shuffle (__main__.TestSequenceFunctions) ... ok

----------------------------------------------------------------------
Ran 2 tests in 0.000s

OK

さて、ひととおりテストがどういうものか、出力がどのようなものかわかっていただけたと思うので、unittestの継承クラスのコードの説明を行います。

まず作成したクラスの最初のメソッドです。

    def setUp(self):
        self.seq = range(10)

これはテストではなく、テストを開始する前に行う作業です。いわゆる初期化処理です。今回はself.seqにrange(10)で生成した数字のシーケンス [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] を格納しています。次にテスト項目に入ります。

    def test_shuffle(self):
        random.shuffle(self.seq)
        self.seq.sort()
        self.assertEqual(self.seq, range(10))
        self.assertRaises(TypeError, random.shuffle, (1,2,3))

これはまずself.seqに格納された文字列のシーケンスをシャッフルし、それをソートしなおしています。そしてそれをassertEqualで再度生成した数字のシーケンスと同一であるか比較しています。最後のassertRaiseは例外が発生するかの確認で、第一引数が例外クラス、第二が関数、第三が第二引数の関数に与える引数です。

試しにこの関数と引数で例外を発生させてみます。

>>> random.shuffle((1,2,3))
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/random.py", line 291, in shuffle
    x[i], x[j] = x[j], x[i]
TypeError: 'tuple' object does not support item assignment

TypeError が発生していますね。assertRaiseは「第二引数の関数に第三引数を与えたら第一引数の例外が発生するか」をチェックし、発生すればOKです。次のメソッドtest_choiceについては簡単かと思うので特に言及することはないのですが、あえてわざとassertに失敗するようにコードを変えて実験してみたいと思います。以下のように書き換えました。

    def test_choice(self):
        element = random.choice(self.seq)
        self.assertTrue(element in range(10,20))

[1,2,……,9] の中からランダムに値をピックアップし、それが [10,11,……,19] の中にあるかをチェックするので、当然ながら"element in range(10,20)"はFalseとなり、assertTrueはfailします。テスト結果を見てみます。

# python test.py
test_choice (__main__.TestSequenceFunctions) ... FAIL
test_shuffle (__main__.TestSequenceFunctions) ... ok

======================================================================
FAIL: test_choice (__main__.TestSequenceFunctions)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "test.py", line 17, in test_choice
    self.assertTrue(element in range(10,20))
AssertionError: False is not true

----------------------------------------------------------------------
Ran 2 tests in 0.000s

FAILED (failures=1)

問題があることがわかり、例外という形で教えてくれていますね。また、ひとつのfailが発生してもほかのテストは実施されていることもわかります。unittestを使ったassertメソッドではなく普通のassertを使う場合、try/catchしないと処理が中断されてしまいますので、大規模なテストではこの特性は便利かもしれません。

unittestの詳細については以下のドキュメントを参照願います。このなかにどのようなassertメソッドが利用できるのかも記載されています。

継続的な開発とテスト

テストは1回書いて実施すれば終わりというものではありません。短い使い捨てのコードであれば書きなぐってテストもしなくてもかまわないのですが、長年使うコードは改修作業が発生すると思うべきです。

詳細は自分で調べていただきたいと思うのですが、昨今の開発スタイルでは「継続的インテグレーション」と呼ばれるものが採用される割合が増えており、それを採用することで上記のような「長年使うコードをメンテする」ことが容易になります。Jenkinsは継続的インテグレーションを実現するためのツールとして有名になりましたね。

この継続的インテグレーションを実行するためには「頻繁にコードを書き換える」ことが必要であり、それが問題になっていないかを確かめるため「コードを書き換えた度にビルドとテストを自動で実施」する必要があります。ユニットテストはこの開発スタイルのなかのひとつである「テストの自動化」において重要なものとなっています。

「コードを変更していないから安全」と思うことをやめ「頻繁に変更し、テストを行う」ことでコードと製品のクオリティを保つことを心がけるといいかもしれません。頻繁にコードを変更していると内部挙動についても精通し、なおかつプログラミングのレベルも向上しますよ。一方、腫れ物に触るかのように改変を避けていると、コードがブラックボックス化し、なおかつプログラミング力向上のチャンスも失ってしまいます。


次回は、デバッグについて扱います。

執筆者紹介

伊藤裕一(ITO Yuichi)

シスコシステムズでの業務と大学での研究活動でコンピュータネットワークに6年関わる。専門はL2/L3 Switching とデータセンター関連技術およびSDN。TACとしてシスコ顧客のテクニカルサポート業務に従事。社内向けのソフトウェア関連のトレーニングおよびデータセンタとSDN関係の外部講演なども行う。

もともと仮想ネットワーク関連技術の研究開発に従事していたこともあり、ネットワークだけでなくプログラミングやLinux関連技術にも精通。Cisco社内外向けのトラブルシューティングツールの開発や、趣味で音声合成処理のアプリケーションやサービスを開発。

Cisco CCIE R&S, Red Hat Certified Engineer, Oracle Java Gold,2009年度 IPA 未踏プロジェクト採択

詳細(英語)はこちら

  • >
  • >>

34/34

インデックス

連載目次
第34回 Pythonのテスト手法
第33回 マルチスレッド処理を理解しよう(後編)
第32回 マルチスレッド処理を理解しよう(前編)
第31回 例外処理について学ぼう(後編)
第30回 例外処理について学ぼう(中編)
第29回 例外処理について学ぼう(前編)
第28回 【番外編コラム】Pythonプログラムの配布方法
第27回 第19回~第25回の演習の解説と解答例
第26回 オブジェクト指向について学ぼう(8)
第25回 オブジェクト指向について学ぼう(7)
第24回 オブジェクト指向について学ぼう(6)
第23回 オブジェクト指向について学ぼう(5) - ゲーム作りにチャレンジ
第22回 オブジェクト指向について学ぼう(4)
第21回 オブジェクト指向について学ぼう(3)
第20回 オブジェクト指向について学ぼう(2)
第19回 オブジェクト指向について学ぼう(1)
第18回 【番外編コラム】関数型プログラミングとPython
第17回 第11回~第16回の演習の解説と解答例
第16回 Pythonをシェルスクリプトのように使ってみよう(後編)
第15回 Pythonをシェルスクリプトのように使ってみよう(前編)
第14回 Pythonで日本語を扱うには? - 文字コードについて理解しよう
第13回 正規表現をマスターしよう
第12回 Pythonでテキスト処理/ファイル処理をしてみよう
第11回 知っていると便利な「型」について学ぼう
第10回 【番外編コラム】著者が開発した音声合成サービス/業務アプリはどう作った?
第9回 第8回までの演習の解説と解答例
第8回 ユーザーからプログラムへの入力をする方法
第7回 関数とモジュールを使いこなそう
第6回 プログラムの制御構造を理解しよう - 条件分岐とループ処理
第5回 「型」と「変数」について学ぼう(後編)
第4回 「型」と「変数」について学ぼう(前編)
第3回 まずは手を動かしてプログラミングの全体像を知ろう!
第2回 プログラミングの環境を整えよう
第1回 Pythonでプログラミングを学ぶ理由とは?

もっと見る

関連したタグ


転職ノウハウ

あなたの仕事適性診断
あなたの仕事適性診断

4つの診断で、自分の適性を見つめなおそう!

Heroes File ~挑戦者たち~
Heroes File ~挑戦者たち~

働くこと・挑戦し続けることへの思いを綴ったインタビュー

はじめての転職診断
はじめての転職診断

あなたにピッタリのアドバイスを読むことができます。

転職Q&A
転職Q&A

転職に必要な情報が収集できます

ドS美人面接官 vs モテたいエンジニア
ドS美人面接官 vs モテたいエンジニア

入室しようとしたら、マサカリ投げられちゃいました!?

特別企画 PR

人気記事

一覧

イチオシ記事

新着記事

近鉄3/19ダイヤ変更 - 名阪特急の停車駅追加、大阪線の快速急行・準急減少
[00:13 1/21] 旅行
マウスコンピューター、ブランド名を「mouse」に変更 - ロゴも刷新
[00:00 1/21] パソコン
金沢一郎氏が死去、皇室医療統括 - 元宮内庁皇室医務主管
[23:34 1/20] 共同通信
「ULTRAMAN」主人公の“宿命”がスタイリッシュなTシャツに
[23:31 1/20] ホビー
宮崎夏次系が装画を手がけた一穂ミチの小説、冒頭部分のコミカライズも公開
[23:13 1/20] ホビー

特別企画 PR

求人情報