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

確認する手法はおおまかに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 未踏プロジェクト採択

詳細(英語)はこちら