【Python】unittestで単体テストを行う(カバレッジの計測についても紹介)

Pythonで単体テストを行う方法の解説ページアイキャッチ

このページにはプロモーションが含まれています

このページでは、Python で単体テストを行う方法について解説していきます!

単体テストを行う方法:unittest を利用する

まず、Python で単体テストを行う方法について解説していきます。Python では unittest モジュールを利用することで簡単に単体テストを実施することができます。

unittest モジュール

unittest モジュールは Python の組み込みモジュール、すなわち標準モジュールであり、Python をインストールすれば一緒にくっついてインストールされます。そのため、別途インストールを行わなくても、Python さえインストールすれば単体テストを実施することが可能です。

この unittest は Python 用の単体テストフレームワークであり、単体テストを実施するための様々な機能が用意されています。例えば、テストを実行して OK / NG 判定をしたり、その判定結果を簡単なレポートとして出力する機能などがあります。

ただし、テストを実装するのは開発者やテスト実施者(今後、テスト担当者と呼びます)であり、unittest の期待する形式のテストケースをテスト担当者が実装する必要があります。また、テストの OK / NG 判定を unittest が正しく行えるよう、これもテスト担当者が実装する必要があります。

つまり、テストを実行する仕組みや結果の OK / NG 判定を行う仕組みは unittest に存在し、それらを利用すれば楽にテストは実行できますが、結局テストはテスト担当者が実装しなくてはなりません。そのため、unittest を利用する場合の「テストの実装の仕方」や unittest を利用した「テストの実施手順」等をテスト担当者がしっかり理解しておく必要があります。そして、以降では、これらのテストの実装の仕方やテストの実施手順を説明していきます。

スポンサーリンク

coverage モジュール

また、今回は unittest だけでなく coverage モジュールの使い方についても紹介していきたいと思います。

coverage を利用することで単体テストのカバレッジを計測できるようになります。単体テストのカバレッジとは、そのテストの網羅率を示す指標となります。カバレッジにも C0 や C1 といった様々な指標があるのですが、ここでは一番簡単な C0 カバレッジを単にカバレッジと呼んで説明を行なっていきます。

この場合、カバレッジとは命令網羅率のことで、簡単に言えばソースコード全体の内の実行された処理(行)の割合を示す指標になります。

例えば、if 文で条件分岐が行われる場合、テストが不足していると一方の分岐の処理の動作しか確認できないような場合があります。もしかしたら、その動作の確認ができていない処理にバグがあるかもしれません。

テストの漏れによりバグが見つけられない様子

そのため、単体テストでは全ての処理の動作を確認しておくのが望ましいです。全ての処理の動作が確認できていないのであれば、そのテストには漏れがあると考えられます。

ですが、単にテストの OK / NG 判定を行っているだけだとテストに漏れがあったとしても、そのことに気付きにくいです。テストが全て OK 判定されていたとしても、テストが不足しており、その不足しているテストを追加すれば NG 判定されるテストが出てくるかもしれません。

テストのOK/NG判定のみからテストの漏れを確認する様子

それに対し、カバレッジを計測しておけば、ソースコード全体の内の実行された処理の割合を知ることができます。カバレッジが低いということは、用意したテストだけでは動作確認できていない処理が多く存在することを示しています。つまり、カバレッジを計測すれば、テストの漏れ・テストの不足に簡単に気づくことができます。

カバレッからテストの漏れを確認する様子

さらに、coverage では、単にカバレッジを値として示すだけでなく、テスト時に実行された行を視覚化したレポートを出力する機能もあります。そして、そのレポートを確認することで、どの処理が実行されていないかを確認することができ、その処理を実行するためのテストを追加してやればテストの漏れを減らすことができます。

is_valid_dateのカバレッジ

ただし、あくまでもカバレッジはテストの漏れを防ぐための指標の1つであり、カバレッジが100%であってもテストが不足している場合があります。テストの網羅率は他の観点でも考える必要があります。ですが、カバレッジが100%ではないということは単体テストで動作確認できていない処理が存在することを示しているため、少なくともテストが不足していることはカバレッジ計測結果から判断することができます。ということで、coverage は計測しておいて損はないと思います。

そして、coverage を利用した場合、カバレッジは unittest の実行結果からカバレッジの計測を行うことが可能です。ということで、まずは unittest で単体テストを行う手順を示し、その次に、coverage を利用して unittest の実行結果からカバレッジを計測する手順を説明していきたいと思います。

unittest を利用して単体テストを行う手順

ということで、まずは unittest を利用して単体テストを行う手順について解説していきます!

unittest はインストールの必要なし

前述の通り、unittest は Python の組み込みモジュールですので Python をインストールすれば利用可能なはずです。なので、別途 unittest をインストールする必要はありません。

スポンサーリンク

単体テストの対象となるモジュール(クラスや関数等)を作成する

また、単体テストを行うわけですから、単体テストの対象となるモジュールが必要となります。そして、そのモジュールにはテストの対象となるクラスや関数を定義しておく必要があります。

これらのクラスのメソッドや関数を単体テストのコードの中から呼び出しすることで、単体テストを実施していくことになります。

テストを実施するモジュールがテスト対象のモジュールを利用する様子

unittest と テスト対象のモジュールimport する

ここまでは事前準備で、ここから unittest を利用した単体テストコードの実装手順を解説していきます。

まず最初にやることは unittestテスト対象のモジュールimport になります。

import
import unittest
import テスト対象のモジュール

TestCase のサブクラスを定義する

次は、unittest.TestCase という unittest モジュールから提供されるクラスのサブクラスを定義します。以降、unittest.TestCase のことは単に TestCase と呼びます。

unittest.TestCaseのサブクラスの定義
import unittest
import テスト対象のモジュール

class UnitTest(unittest.TestCase)
    pass

上記では TestCase のサブクラスを UnitTest としていますが、このクラス名はテストの観点や、テスト対象となる関数やクラス・メソッドに応じて名前を適切に設定するのが良いと思います。後述の通り、テスト結果のレポートではこのクラス名も表示されますので、このクラス名を適切に設定しておけば、テストのレポートから何のテストを行なっているかが分かりやすくなります。

また、この TestCase のサブクラスにおいては、test から始まる名前のメソッドがテストケースとして扱われることになります。

テストケースとは、下の図における各行に相当するもので、確認内容やテストの実行条件(入力など)・テストの手順・期待する結果(出力など)等をまとめたものになります。つまり、これらを test から始まるメソッドに実装していくことになります。

テストケースの説明図

そして、後述で紹介するコマンドを実行すれば、TestCase のサブクラスの持つ「名前が test から始まるメソッド」が全て実行され、そのメソッドの中で行われる OK / NG 判定に従った結果のレポートが出力されるようになります。

また、TestCase のサブクラスは同じファイル内に複数定義することができます。そして、それらのクラスに用意されたテストケース(testから始まるメソッド)を全て一括で実行することもできますし、クラス毎に分けてテストを行うようなことも可能です。

スポンサーリンク

メソッドとしてテストケースを記述する

ということで、次は名前が test から始まるメソッドを定義し、その中にテストケースを処理として記述していきます。

test から始まる名前のメソッドを用意する

まず、unittestモジュールを利用する場合、前述の通り unittest.TestCase のサブクラスに定義した test から名前の始まるメソッドがテストケースとして扱われることになります。

ということで、テストケースを追加する場合には、まず test からはじまる名前のメソッド定義する必要があります。このメソッド名はテストレポートに出力されるため、test_テスト内容 のように、メソッド名からテスト内容が分かるような名前をつけてやることをオススメします。

テストケースの定義
import unittest
import テスト対象のモジュール

class UnitTest(unittest.TestCase)
    def test_テスト内容(self):
        pass

テストの実行条件を満たすための処理を実装する

続いてメソッドの中にテストの実行条件を満たすための処理の実装を行います。

要は、そのテストケースのテストを行うための前準備を処理として実装します。例えばテストの対象がファイルを入力とするのであれば事前にファイルを用意したり、テストの対象が引数を入力とするのであれば、引数用のデータを準備したりします。

一番簡単なのが「入力データが引数のみの関数」に対するテストで、この場合は引数に入力するデータのみを準備しておけば良いことになります。

ただ、実施したいテストによっては環境変数をセットしたり、状態の設定を行なったりする必要があるかもしれません。また、メソッドに対するテストであれば、事前にクラスのインスタンスを生成しておく必要もあります。いずれにせよ、実行したいテストの観点に応じ、そのテストの観点が確認できるように事前準備を行う処理を実装しておく必要があります。

実行条件の記述
import unittest
import テスト対象のモジュール

class UnitTest(unittest.TestCase)
    def test_テスト内容(self):
        テストの実行条件を満たすための処理(引数の準備など)

テストの手順に応じた処理を実装する

続いて、テストの手順に応じた処理をメソッドの中に実装していきます。

要は、ここでテストの対象となるものを実行します。単体テストの場合は、ここで関数やメソッドを実行するケースが多いと思います。

また、単一の関数を実行するだけでなく、複数の関数を順々に実行してテストを行う場合もあります。

ここで重要なのは、これらの関数やメソッドの出力(実行結果)は入力によって異なるという点になります。意図したテストが行えるよう、テストの実行条件を満たすための処理を記述する で示したようにテストの事前準備を行なっておく必要があります。

テストの手順の実施
import unittest
import テスト対象のモジュール

class UnitTest(unittest.TestCase)
    def test_テスト内容(self):
        テストの実行条件を満たすための処理(引数の準備など)
        テスト手順の実施(関数の実行など)

テストの実行結果と期待する結果と一致するかを確認する

また、各テストケースにおいては必ずテストによって得られる結果の期待値が存在するはずです。期待する結果が得られなければ、そのテストは失敗したことになります。こういった成功 / 失敗の判断を行うためには、テストの手順を実施したのちに「得られた結果」と「期待する結果」とが一致するかどうかを確認する必要があります。

そして、この確認は、単に if 文等で行うのではなく、TestCase クラスのメソッドを利用して行う必要があります。

具体的には、TestCase には例えば下記のような assert から名前が始まるメソッドが用意されており、これらのメソッドを利用して出力が期待する結果であるかどうかを確認します。

  • assertEqual(x, y)x == y が成立すれば OK と判断、それ以外は NG と判断
  • assertTrue(x)x is True が成立すれば OK と判断、それ以外は NG と判断
  • assertFalse(x)x is False が成立すれば OK と判断、それ以外は NG と判断
  • assertIn(x, y)x in y が成立すれば OK と判断、それ以外は NG と判断
  • assertNotIn(x, y)not x in y が成立すれば OK と判断、それ以外は NG と判断

分かりやすいのが assertEqual(x, y) で、x にテスト手順を実施することで得られた出力(結果)、y に期待する結果を指定してやれば、assertEqual(x, y) 実行により得られた出力が期待する結果と一致するかどうかを確認できます。一致した場合は OK と判断され、OK と判断され場合はテストレポートに OK と出力されます。一致しなかった場合は NG と判断され、テストレポートに NG と出力されます。

他の assert 系のメソッドに関しても if 文での判断を代替するものなので、大体意味合いは理解していただけるのではないかと思います。

また、1つのテストの中で複数の出力が期待するものであるかどうかを確認する場合は assert 系のメソッドを複数回実行しても良いです。この場合、1つでも NG と判断されればテストレポートに NG と出力されることになります。

テスト結果の確認
import unittest
import テスト対象のモジュール

class UnitTest(unittest.TestCase)
    def test_テスト内容(self):
        テストの実行条件を満たすための処理(引数の準備など)
        テスト手順の実施(関数の実行など)
        self.assertXxxx(略)

1つ注意点を挙げると、前述の通り assert 系のメソッドは TestCase クラスのメソッドとなりますので、テストケース内で実行するためには上記のように self からメソッドを実行させる必要があります。

必要なテストケースの分だけメソッドを定義する

以上が、1つのテストケースを実装する大まかな流れになります。

テストケースは1つだけとは限らないので、実施したいテストケースの分だけメソッドを実装していくことになります。前述の通り、テストケースとして扱われるためにはメソッド名は test から始まる必要があります。

テストケースの拡充
import unittest
import テスト対象のモジュール

class UnitTest(unittest.TestCase)
    def test_テスト内容(self):
        テストの実行条件を満たすための処理(引数の準備など)
        テスト手順の実施(関数の実行など)
        self.assertXxxx(略)

    def test_テスト内容(self):
        テストの実行条件を満たすための処理(引数の準備など)
        テスト手順の実施(関数の実行など)
        self.assertXxxx(略)

    def test_テスト内容(self):
        テストの実行条件を満たすための処理(引数の準備など)
        テスト手順の実施(関数の実行など)
        self.assertXxxx(略)

また、クラスを分けて別のクラスのメソッドとしてテストケースを実装しても良いです。ただし、この場合もクラスは TestCase のサブクラスとして定義する必要があります。

テストを実施する

テストが実装できれば、次は実際にテストを実施します。

unittest では様々なテストの実施方法が用意されていますので、ここでは代表的なものをいくつか紹介したいと思います。また、ここではテストを実装したスクリプトが test_main.py という名前であること、さらに現在の作業フォルダが test_main.py が存在するフォルダであることを前提に説明していきます。

スクリプトとして実行してテストを実施する

1番分かりやすいテストの実施方法は「テストを実装したスクリプトを直接 python コマンドで実行する」になります。

この場合、テストを実装したスクリプトが実行された際に unittest.main() が実行されるようにスクリプトの変更を行なっておく必要があります。

スクリプトの実行で手順の実施
import unittest
import テスト対象のモジュール

class UnitTest(unittest.TestCase)
    def test_テスト内容(self):
        テストの実行条件を満たすための処理(引数の準備など)
        テスト手順の実施(関数の実行など)
        self.assertXxxx(略)

略

if __name__ == '__main__':
    unittest.main()

このようにスクリプトを作成しておけば、python コマンドで「テストを実装したスクリプト」を実行した際に  unittest.main が実行され、これによりスクリプト内に定義されているクラスの全テストケースのテストが実行されます。

% python test_main.py

unittest.main を実行する処理が記述されていないと、単にクラスの解析だけが行われてテスト自体が行われないことになるので注意してください。

モジュール全体のテストを実施する

先ほどはpython コマンドから直接スクリプトを実行してテストを実施する方法を紹介しましたが、ここからは unittest モジュールからテストを実施する方法を紹介していきます。unittest モジュールからテストを実施することで、テストを実施する単位などを指定できるようになります。

unittest モジュールからテストを実施する場合の基本的なコマンドの形式は下記のようなものになります。

% python -m unittest テスト対象

まず、上記のコマンドの テスト対象 にテストを実装したスクリプトをモジュール名として指定することで、そのスクリプトで実装されたテストケースのテスト全てを実施することができます。

例えば、下記のように テスト対象 にモジュール名 test_main を指定すれば、モジュール test_main (ファイル test_main.py)に定義されたテストケースのテストが全て実施されることになります。

% python -m unittest test_main

クラス単位でテストを実施する

また、先ほど示したコマンドの形式において、テスト対象 部分に モジュール名.クラス名 を指定すれば、指定したクラスのテストケースのテストのみが実施されることになります。より具体的には、そのクラスの持つ test から名前の始まるメソッドが全て実行されることになります。

% python -m unittest test_main.クラス名

このクラス名で指定するクラスは TestCase のサブクラスである必要があります。

関数単位でテストを実施する

さらに、テスト対象 部分に モジュール名.クラス名.メソッド名 を記述すれば、指定したメソッドのみが実施されることになります。つまり、1つのテストケースのテストのみが実施されることになります。

% python -m unittest test_main.クラス名.メソッド名

大量のテストを実施すると時間がかかることもありますので、そういう場合はクラス単位 or メソッド単位でテストを実施することで短時間でテストを実施することができます。

テストレポートを確認する

テストを実施した後は、テストレポートを見てテスト結果の確認を行います。

テストレポートは、先ほど示した手順でテストを実施すれば標準エラー出力に自動的に出力されることになります。

出力結果は下記のようなものになります。

FE..
======================================================================
ERROR: test_valid_date (test_main.TestDate)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/daeu/Documents/python/unittest_test/test_main.py", line 27, in test_valid_date
    ret = main.is_valid_date('2008/03/31')
  File "/Users/daeu/Documents/python/unittest_test/main.py", line 7, in is_valid_date
    ret = 100/0
ZeroDivisionError: division by zero

======================================================================
FAIL: test_invalid_format_date (test_main.TestDate)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/daeu/Documents/python/unittest_test/test_main.py", line 35, in test_invalid_format_date
    self.assertFalse(ret)
AssertionError: True is not false

----------------------------------------------------------------------
Ran 4 tests in 0.004s

FAILED (failures=1, errors=1)

Ran 4 tests in 0.004s はテストを実施したテストケースの数とテストに要した時間になります。

また、最後の行の FAILED (failures=1, errors=1) はテストに失敗したテストケース数を示しています。失敗には failureerror との2種類があって、failures が示すのは assert 関連のメソッドで出力結果と期待する結果が一致しないと判断された場合の失敗となります。また、error は何らかの原因でテストの手順が完遂できなかった場合の失敗となります。例えば、関数を実行した際に関数の中で例外が発生した場合などは error となります。

そして、これらの「テストに失敗したと判断された箇所」が、テストのレポートの上側に表示されています。

こんな感じで、テストを実施するとレポートが出力され、失敗したテストケースの数や失敗したと判断された箇所を特定することができます。

テストレポートの詳細を出力する

ちなみに、テストが全て成功と判断された場合のレポートは下記のようなものになります。これで全てのテスト結果が OK であることは分かるのですが、このレポートだけでは、どんなテストケースのテストが実行されたのかを把握することができません。

....
----------------------------------------------------------------------
Ran 4 tests in 0.001s

OK

unittest においては、テストの詳細を出力する機能が備えられており、それを利用すればテストが実施されたテストケースを表示することができるようになります。

具体的には、テストを実施する で紹介したコマンドにおいて、unittest にオプション -v を指定すれば、実施されたテストのテストケース(メソッド名)が表示されるようになります。

例えば下記のようにコマンドを実行した場合、test_main で定義されたテストケースのテストが全て実行されます。

% python -m unittest test_main -v

そして、オプション -v を指定しているため、上記コマンドを実行して出力されるレポートは下記のようなものになります。実施されたテストケース、すなわちメソッド名が表示されるので、どんなテストが実行されたのかをレポートから読み取れるようになります。

test_invalid_format_date (test_main.TestDate) ... ok
test_valid_date (test_main.TestDate) ... ok
test_invalid_format_time (test_main.TestTime) ... ok
test_valid_time (test_main.TestTime) ... ok

----------------------------------------------------------------------
Ran 4 tests in 0.001s

OK

ただ、メソッド名をてきとうにしてしまうと、レポートから実施されたテストケースは分かるものの、そのテストケースの内容が理解できない可能性が高いです。そのため、テスト用のソースコードではあるものの、メソッド名にはテストケースの内容が分かるような名前をつけておくことをオススメします。

また、例えば下記のように各メソッドに docstring を付加しておけば、その docstring も一緒にレポートに出力されることになります。

docstringの利用
class クラス名(unittest.TestCase):

    def test_テストの内容(self):
        '''
        有効な時刻が入力された時のテスト
        '''

        # 略

これにより、テストのレポートに更に詳細な情報を出力することができるようになり、よりテスト内容が理解しやすいレポートに仕立てることができます。

test_invalid_format_date (test_main.TestDate)
日付のフォーマットが不正な場合のテスト ... ok
test_valid_date (test_main.TestDate)
有効な時刻が入力された場合のテスト ... ok
test_invalid_format_time (test_main.TestTime)
時刻のフォーマットが不正な場合のテスト ... ok
test_valid_time (test_main.TestTime)
有効な時刻が入力された時のテスト ... ok

----------------------------------------------------------------------
Ran 4 tests in 0.001s

これは憶測になるのですが、この docstring のレポート出力は最近の Python で追加されたようで、私の PC に入っている Python3.8 の場合はレポートに docstring は出力されませんでした。それに対し、Python3.10 の場合は docstring-v 指定により出力されるようになっていますので、docstring が出力されない場合は新しい Python で試してみると良いと思います。

スポンサーリンク

coverage を利用してカバレッジを計測する手順

ここまで説明してきた手順を踏むことで unittest を利用したテストを実施し、さらにテスト結果をレポートとして出力することができるようになりました。レポートから、テストの成功失敗に関しても確認することが可能です。

ですが、このレポートからでは「テストの漏れ」「テストが十分かどうか」という観点での確認ができません。単に成功失敗しか分からないので、unittest を利用するだけではこういった観点の確認ができません。

こういった「テストが十分であるか」どうかを確認したい時に便利なのが、ここで紹介する coverage になります。これを利用すれば、カバレッジを計測することができるようになりますし、テストで実行された処理(行)を示すレポートも出力することができ、これによって「不足しているテスト」を洗い出すことができるようになります。

coverage をインストールする

ここからは、この coverage を利用する手順を説明していきます。

coverageunittest とは異なって Python の標準モジュールではないため、別途インストールが必要となります。ただ、pip から簡単にインストールすることが可能で、具体的には下記コマンドを実行すればインストールすることができます。

% python -m pip install coverage

coverage を利用して単体テストを実行する

coverage をインストールした後は、この coverage を利用して単体テストを実行し、そのテストのカバレッジを計測していきます。

カバレッジを計測する

具体的には、下記のようなコマンドを実行することでカバレッジの計測を行うことができます。テスト対象 の部分は テストを実施する で説明したようにモジュール名やクラス名、メソッド名等を指定することが可能です。

% python -m coverage run -m unittest テスト対象 オプション

このようなコマンドを実行すれば、まず テスト対象 に対する単体テストが実行され、その際にカバレッジが計測されるようになります。このカバレッジの計測結果の確認方法は次の節で説明します。

また、前述の通り、単に unittest で単体テストを行う場合のコマンドは下記のようになっています。上記のコマンドは下記のコマンドに対して -m coverage run が追加されているだけで、-m unittest 以降は unittest を利用して単体テストを実施するときと同様のコマンドを指定すれば良いだけになります。

% python -m unittest テスト対象 オプション

例えば、test_main というモジュール(test_main.py)に対してテストを実施したいのであれば、下記のようにコマンドを実行すればテストが実施されて詳細なレポートが出力されるとともに、カバレッジの計測も行われます。

% python -m coverage run -m unittest test_main -v

カバレッジレポートを確認する

ただし、カバレッジを計測したとしてもカバレッジの計測結果は自動的には表示されません。カバレッジの計測結果を確認するためには別途コマンドを実行する必要があります。

具体的には、まずは先ほど示した手順でカバレッジの計測を行い、その後に下記コマンドを実行することでカバレッジレポートを確認することが可能となります。

% python -m coverage report -m

コマンド実行結果は下記のようなものになります。

% python -m coverage report -m
Name           Stmts   Miss  Cover   Missing
--------------------------------------------
main.py           17      6    65%   7, 14-15, 19, 23, 27
test_main.py       9      1    89%   14
--------------------------------------------
TOTAL             26      7    73%

Name の列は単体テスト実施時に実行されたファイルを示しており、Stmts はそのファイルに存在する全命令の数、簡単に言えば、各ファイルにおけるコメントや空行を除く全行数が記載されています。

そして、その中で単体テスト実施時に実行されていない命令、簡単に言えば通過していない行の数が Miss に記載されています。さらに Cover は全命令に対して単体テストで実行された命令の割合が示され、Missing には単体テスト実行時に通過しなかった行の行番号が示されています。

これを見れば、単体テストでどれくらいの命令の実行を網羅できているかを確認することができ、さらに不足しているテストに関しても確認することができます。

上の例で言えば、main.py に関しては 714-15192327 の行が単体テスト実施時に通過していないため、これらの行を実行するためのテストが不足していることを確認することができます。

カバレッジ計測結果を HTML で視覚化する

また、単体テスト実施時にカバレッジを計測しておけば、その結果を HTML 形式のレポートを出力することで視覚化することもできます。

HTML 形式のレポートを出力する際にはカバレッジ計測後に下記のコマンドを実行します。

% python -m coverage html
Wrote HTML report to htmlcov/index.html

実行すれば、上の結果で示すように、現在の作業フォルダの中に htmlcov というフォルダが生成され、そのフォルダの中の index.html に HTML 形式のレポートが出力されます。

このファイルを開けば、下の図のようなレポートを確認することができます。

カバレッジの計測結果が表示される様子

さらに、このレポートの中で Module の列のファイル名をクリックすれば、そのファイルのカバレッジの詳細を確認することができます。この詳細は下の図のようになっており、赤色の行がテスト実施時に実行されていない行を示しています。要は、赤色の行の動作を確認するためのテストが実施されていないということになります。

is_valid_dateのカバレッジ

赤色の行が実行されるような実行条件のテストを追加してやれば、その行がテスト実施時に実行されるようになり、赤色の行を減らすことができます。全ての行が赤色でなくなれば、カバレッジが100%になるということになります。

こんな感じで、カバレッジを計測すれば網羅率や不足しているテストを確認することができ、それをカバーしていくことでソフトウェアの品質を向上させていくことができます。

スポンサーリンク

単体テストの実施例

ここまでは手順についてのみ説明してきましたので、ここからは実際に単体テストを行い、さらに単体テストの結果の確認、カバレッジの確認等を行う実例を示していきたいと思います。

テスト対象の準備

まずは、今回のテスト対象となるモジュールを用意していきたいと思います。今回は、中身が下記のようなスクリプトである main.py をテスト対象としたいと思います。

main.py
def is_valid_date(date):
    
    # フォーマットが YYYY/MM/DD であるかをチェック
    date_list = date.split('/')
    if len(date_list) != 3:
        return False

    # 年月日が整数であるかをチェック
    try:
        year = int(date_list[0])
        month = int(date_list[1])
        day = int(date_list[2])
    except ValueError:
        return False

    # 年の妥当性チェック
    if year < 0:
        return False

    # 月の妥当性チェック
    if month > 12:
        return True

    # 日の妥当性チェック
    if day > 31:
        return False
    
    return True
    
def is_valid_time(time):

    # フォーマットが hh:mm:ss であるかをチェック
    time_list = time.split(':')
    if len(time_list) != 3:
        return False

    # 時分秒が整数であるかをチェック
    try:
        hour = int(time_list[0])
        minutes = int(time_list[1])
        second = int(time_list[2])
    except ValueError:
        return False

    # 時の妥当性チェック
    if hour > 24:
        return False
    
    # 分の妥当性チェック
    if minutes > 60:
        return False

    # 秒の妥当性チェック
    if second > 60:
        return False
    
    return True

is_valid_dateis_valid_time はそれぞれ入力された日付を表す文字列、時刻を表す文字列の妥当性をチェックする関数となります。要は、日付として or 時刻として正しい入力であるかどうかをチェックします。

これらの関数の仕様について簡単に説明しておきます。前もって伝えておくと、これらは関数の仕様の “定義” であり、実際の is_valid_dateis_valid_time はこれらの仕様を満たせていません。つまりバグっています。このバグを、以降の手順の中で見つけていきたいと思います。

まず、入力される文字列の日付のフォーマット、時刻のフォーマットは下記であることを想定しています。

  • 日付:YYYY/MM/DD
  • 時刻:hh:mm:ss

例えば、日付であれば年を表す YYYY は4桁の整数であり、月を表す MM は2桁の整数であり、日を表す DD は 2桁の整数であり、さらに、これらの年月日はそれぞれ / で区切られていることを条件としたフォーマットなっています。このフォーマットに合致していない日付の文字列が入力された場合、is_valid_date は入力が日付として妥当でないと判断して False を返却します。

また、フォーマットが合致していたとしても、例えば 2023/12/32 などの日付は、日が 32 になっていて不正な日付であることになります。こういった、暦上あり得ない日付が入力された場合も is_valid_dateFalse を返却します。

そして、入力された日付の文字列が妥当である場合のみ、is_valid_dateTrue を返却するものとします。ちょっとざっくりした説明になりますが、これが is_valid_date の関数仕様となります。is_valid_time も同様で、正しい時刻かどうかを判断し、正しい場合のみ True を返却し、それ以外は False を返却する仕様となります。

テストの実装

次は、先ほど用意した main.py に対してテストを行う側のモジュールの実装を行なっていきます。今回は、このモジュールを test_main.py とし、このファイルで unittest を利用して単体テスト用のテストコードの実装を行なっていきます。

まずは、unittestmainimport を行い、続いて TestCase のサブクラスを定義していきます。ここで import する main がテスト対象のモジュールとなります。さらに、そのサブクラスのメソッドとしてテストケースを実装していきます。

最初なので、簡単なテストとして '2023/07/08' を入力して is_valid_date を実行するテストと、'12:34:56' を入力して is_valid_time を実行するテストの2つを用意したいと思います。これら2つのテストの実行結果として得られる出力の期待する値はともに True となります。

この場合、test_main.py には下記のような実装を行うこととなります。python コマンドで直接実行した場合もテストが実行されるよう、unittest.main の実行処理も記述しています。

テストケースの実装
import unittest
import main

class TestIsValidDate(unittest.TestCase):

    def test_valid_date(self):
        '''有効な時刻が入力された場合のテスト'''

        ret = main.is_valid_date('2023/07/08')
        self.assertTrue(ret)

class TestIsValidTime(unittest.TestCase):


    def test_valid_time(self):
        '''有効な時刻が入力された時のテスト'''
        ret = main.is_valid_time('12:34:56')
        self.assertTrue(ret)


if __name__ == '__main__':
    unittest.main()

上記では2つのメソッドを定義しており、これらのメソッドは メソッドとしてテストケースを記述する で説明したように、テストケースとして扱われるようメソッド名は test から始まるようにしています。また、これらのメソッドは、これも メソッドとしてテストケースを記述する で説明したように、大きく分けて「実行条件を満たす処理」「テスト手順を実施する処理」「実行結果を確認する処理」の3つの処理の構成と行なっています。

  • 実行条件を満たす処理:input_dateinput_time を用意する
  • テスト手順を実施する処理:用意したデータを引数に指定して is_valid_dateis_valid_time を実行する
  • 実行結果を確認する処理:返却値に対して assertTrue を実行して期待する結果となっているかを確認する

念の為補足しておくと、結果確認に assertTrue を利用しているのは各テストケースにおける期待する結果が True であるからであり、期待する結果が異なるのであれば、その期待する結果に応じて利用する assert 関連のメソッドも変更する必要があります。

スポンサーリンク

テストの実施と結果の確認

2つだけですが、ひとまずテストが準備できたため、次はテストを実施していきたいと思います。

まず、下記コマンドを実行してテストを実施してみましょう!後ほどカバレッジも確認するため、ここでカバレッジも計測するように coverage を利用したコマンドとなっています。

コマンドを実行すれば、テスト結果のレポートが表示されることも確認できると思います。

% python -m coverage run -m unittest test_main -v
test_valid_date (test_main.TestIsValidDate)
有効な時刻が入力された場合のテスト ... ok
test_valid_time (test_main.TestIsValidTime)
有効な時刻が入力された時のテスト ... ok

----------------------------------------------------------------------
Ran 2 tests in 0.001s

OK

テスト結果が OK と表示されていますね!

これでテスト完了!

is_valid_dateis_valid_time も完璧な仕上がり!

… というわけではなく、前述でも述べたように、これらの関数にはバグがあり、テスト結果が OK なのは単にテストが足りていないからです。次の節でカバレッジを確認し、不足しているテストを確認していきたいと思います。

また、上記のテスト結果からも分かるように、今回用意したサブクラスの両方のテストが実施されていることが確認できると思います。これは、テスト対象としてモジュール(test_main)を指定しているからであって、この場合はモジュールで定義されている全テストケースのテストが実施されることになります。

例えば、下記のようにテスト対象にクラスを指定してやれば、実施するテストケースを絞ることができることも確認できると思います。

% python -m coverage run -m unittest test_main.TestIsValidTime -v
test_valid_time (test_main.TestIsValidTime)
正常系:有効な時刻が入力された時のテスト ... ok

----------------------------------------------------------------------
Ran 1 test in 0.000s

OK

今回の単体テストの実施の例で2つのクラスを用意したのは、このテスト対象の絞り込みができる点をお伝えしたかったことが理由となります。この点については上記で説明することができたので、ここからは is_valid_date のみに焦点を当てて解説していきたいと思います。

カバレッジの確認

さて、話は元に戻して、次は先ほど計測したカバレッジを確認していきたいと思います。

下記を実行してカバレッジを HTML として出力し、index.thml をウェブブラウザで表示してみてください!

% python -m coverage html                        
Wrote HTML report to htmlcov/index.html

表示すると下の図のようなページが表示されると思います。

カバレッジの計測結果例

今回のテスト対象は main.py なので、main.py のカバレッジを確認してみましょう。65% と表示されているので、main.py の実行可能な全ての行のうち、実行された行は僅か 65% であることが確認できます。

次は、main.py をクリックして main.py で実行されていない行を確認してみましょう。赤色の行がテスト実施時に実行されていない行となります。is_valid_date 関数の中にも赤色の行が存在することが確認できると思います。先ほどのテスト結果は OK でしたが、この赤色の行の動作確認はできていなため、この赤色の行にバグが存在する可能性があります。が、それに関しては先ほどのテストでは確認できていないということになります。

is_valid_dateのカバレッジ

ホワイトボックステストの実施

ということで、次はこれらの赤色の行を実行するためにテストを追加してみましょう!ソースコードを読めば、どんなテストを実施すれば良いかは大体想像がつくと思います。赤色の行を全て実行させるためのテストを追加した場合、TestIsValidDate クラスは下記のような定義になります。

今回は日付として不正なデータを入力するテストケースを追加しており、これらのテストケースにおける期待する結果は False となります。そのため、追加したメソッドでは assertFalse を利用して is_valid_date の返却値が False であることを確認するようにしています。

テストの追加(ホワイトボックス)
class TestIsValidDate(unittest.TestCase):

    def test_valid_date(self):
        '''有効な時刻が入力された場合のテスト'''

        input_date = '2023/07/08'
        ret = main.is_valid_date(input_date)
        self.assertTrue(ret)

    def test_invalid_format(self):
        '''日付のフォーマットが不正な場合のテスト'''

        input_date = '20230708'
        ret = main.is_valid_date(input_date)
        self.assertFalse(ret)

    def test_invalid_type(self):
        '''年が整数でない場合のテスト'''

        input_date = 'YYYY/07/08'
        ret = main.is_valid_date(input_date)
        self.assertFalse(ret)

    def test_valid_year(self):
        '''年が負数の場合のテスト'''

        input_date = '-123/07/08'
        ret = main.is_valid_date(input_date)
        self.assertFalse(ret)

    def test_valid_month(self):
        '''月が13以上の場合のテスト'''

        input_date = '2023/13/08'
        ret = main.is_valid_date(input_date)
        self.assertFalse(ret)

    def test_valid_day(self):
        '''日が32以上の場合のテスト'''

        input_date = '2023/07/32'
        ret = main.is_valid_date(input_date)
        self.assertFalse(ret)

先ほどと同様にテストを実施してみましょう!今回のテスト結果は下記のようになります。テスト結果が今度は FAILED になっていることが確認できると思います。

% python -m coverage run -m unittest test_main -v
test_invalid_format (test_main.TestIsValidDate)
日付のフォーマットが不正な場合のテスト ... ok
test_invalid_type (test_main.TestIsValidDate)
年が整数でない場合のテスト ... ok
test_valid_date (test_main.TestIsValidDate)
有効な時刻が入力された場合のテスト ... ok
test_valid_day (test_main.TestIsValidDate)
日が32以上の場合のテスト ... ok
test_valid_month (test_main.TestIsValidDate)
月が13以上の場合のテスト ... FAIL
test_valid_year (test_main.TestIsValidDate)
年が負数の場合のテスト ... ok
test_valid_time (test_main.TestIsValidTime)
正常系:有効な時刻が入力された時のテスト ... ok

======================================================================
FAIL: test_valid_month (test_main.TestIsValidDate)
月が13以上の場合のテスト
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/daeu/Documents/python/unittest_test/test_main.py", line 39, in test_valid_month
    self.assertFalse(ret)
AssertionError: True is not false

----------------------------------------------------------------------
Ran 7 tests in 0.003s

FAILED (failures=1)

テストが FAILED になっているのは test_valid_month であり、このメソッドの実装内容より月が 13 以上の場合にも is_valid_test メソッドが True を返却してしまっていることが確認できます。月は 12 以下でなければならないため、この返却値は期待する結果ではないことになります。

ということで、テストを追加することで is_valid_test のバグを1つ見つけ出すことができたことになります。具体的にバグっているのは下記部分で、ここの return する値を False に変更すればバグを修正することができます。

見つかったバグ
# 月の妥当性チェック
if month > 12:
    return True

修正後にテストを再度実施すれば、テスト結果が OK になることも確認できると思います。また、カバレッジを確認すれば、is_valid_test の全ての行が赤色でなくなっていることが確認できると思います。

ホワイトボックステスト追加後のis_valid_dateのカバレッジ

今回はカバレッジを確認し、実行されていない行が実行されるようにテストの追加を行いました。このように、プログラム内部、つまりソースコードの構造や処理の流れを考慮して行うテストを「ホワイトボックステスト」と呼びます。有名なテスト手法になるので、是非ホワイトボックステストについては覚えておいてください。

ホワイトボックステストにおいて、不足しているテストの指標の1つがカバレッジとなります。カバレッジの計測結果から実行されていない行に対するテストが不足していることが分かるため、その行を減らすためのテストを追加していくことになります。これにより、動作確認できていない行を減らすことができ、バグの検出や品質向上を行うことが可能となります。

ただし、あくまでもカバレッジは1つの指標であり、ホワイトボックステストにおいても他の観点でテストを追加していくことが必要となります。

スポンサーリンク

ブラックボックステストの実施

さて、先ほどのテスト結果は OK で、is_valid_test 関数に関して言えばカバレッジも 100%  になっています。では、これで is_valid_test 関数のテストは不足していないと言えるでしょうか?また、is_valid_test は完全に仕様を満たした関数であると言えるでしょうか?

これはどちらも No です。確かに is_valid_test 関数に対するテストはカバレッジという観点では足りています。ただし、is_valid_test 関数にはまだバグがたくさん存在しています。

カバレッジは、あくまでも現状のソースコードにおける命令網羅率を示しているだけであり、そもそも必要な命令や処理がソースコード内に存在しない場合があります。そして、このような場合でも、カバレッジの計測結果は 100% にできてしまいます。つまり、カバレッジが 100% になったからといってテストが十分であるとは言い切れないということです。

ということで、次はカバレッジ計測結果ではなく異なる観点で必要なテストを考えていきたいと思います。

ここでは、関数の仕様に着目してテストを追加したいと思います。

前述の通り、is_valid_test に入力する文字列のフォーマットは YYYY/MM/DD であり、これ以外のフォーマットが入力された場合 False を返却するのが関数の仕様となっています。従って、YYYY/MM/DD 以外のフォーマットが入力された場合、例えば YYYYY/M/DD のフォーマットの文字列が入力された場合は False が返却されるのが期待する動作となります。

では、本当に is_valid_test がこのような動作になっているのかどうかをテストで確認したいと思います。そのために、TestIsValidDate に下記のメソッドの追加を行います。

テストの追加(ブラックボックス)
class TestIsValidDate(unittest.TestCase):

    略
    def test_invalid_format_2(self):
        '''年が5桁の場合のテスト'''

        input_date = '20230/7/08'
        ret = main.is_valid_date(input_date)
        self.assertFalse(ret)

テストの実行結果のレポートは下記のようになります。途中を略していますが、追加したテストが FAILED になっていることが確認できます。

% python -m coverage run -m unittest test_main -v
test_invalid_format (test_main.TestIsValidDate)
日付のフォーマットが不正な場合のテスト ... ok
test_invalid_format_2 (test_main.TestIsValidDate)
年が5桁の場合のテスト ... FAIL

======================================================================
FAIL: test_invalid_format_2 (test_main.TestIsValidDate)
年が5桁の場合のテスト
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/daeu/Documents/python/unittest_test/test_main.py", line 53, in test_invalid_format_2
    self.assertFalse(ret)
AssertionError: True is not false

----------------------------------------------------------------------
Ran 8 tests in 0.003s

FAILED (failures=1)

で、ここで is_valid_date 関数に着目すると、確かに年月日の桁数がそれぞれ正しいかどうかのチェックが抜けていることを確認することができます。

そこで、下記のように main.pyis_valid_date 関数の前半部分を修正します。

バグの修正
def is_valid_date(date):
    
    # フォーマットが YYYY/MM/DD であるかをチェック
    date_list = date.split('/')
    if len(date_list) != 3:
        return False
    
    # 年月日の桁数チェック
    if len(date_list[0]) != 4:
        return False
    
    if len(date_list[1]) != 2:
        return False
    
    if len(date_list[2]) != 2:
        return True

    略

この変更を行なったのちに、再度テストを実施すれば、実行結果のレポートは下記のようになります!またテスト結果が OK になりましたね!

% python -m coverage run -m unittest test_main -v
test_invalid_format (test_main.TestIsValidDate)
日付のフォーマットが不正な場合のテスト ... ok
test_invalid_format_2 (test_main.TestIsValidDate)
年が5桁の場合のテスト ... ok
test_invalid_type (test_main.TestIsValidDate)
年が整数でない場合のテスト ... ok
test_valid_date (test_main.TestIsValidDate)
有効な時刻が入力された場合のテスト ... ok
test_valid_day (test_main.TestIsValidDate)
日が32以上の場合のテスト ... ok
test_valid_month (test_main.TestIsValidDate)
月が13以上の場合のテスト ... ok
test_valid_year (test_main.TestIsValidDate)
年が負数の場合のテスト ... ok
test_valid_time (test_main.TestIsValidTime)
正常系:有効な時刻が入力された時のテスト ... ok

----------------------------------------------------------------------
Ran 8 tests in 0.002s

OK

先ほどのテスト追加時のように、プログラム内部の処理やソースコードの構造ではなく、関数の仕様に基づいてテストを行う手法を「ブラックボックステスト」と呼びます。関数内部を知らなくても、仕様さえ定義されていれば、その仕様から入力と期待する結果の関係を考えることは可能です。そして、本当にその入力に対して期待する結果が得られるのかどうか?という観点からテストを行うことで、テスト対象が本当に仕様を満たしているかどうかを確認していくのがブラックボックステストとなります。

ただし、is_valid_date 関数に新たに行が追加されたことでテスト結果は OK になったもののカバレッジが低下してしまっています。実際にレポートを確認すれば、下の図のように赤色の行が増えていることが確認できるはずです。

ブラックボックステスト追加後のis_valid_dateのカバレッジ

もうお気づきかもしれませんが、実は先ほど is_valid_date 関数に追加した処理にはバグが含まれています。ですが、このバグに関しては、前述で紹介したホワイトボックステストの追加、今回の場合はカバレッジに基づいたテストの追加を行えば、そのバグも見つけ出すことができます。

また、先ほどは日付のフォーマットの仕様に注目してブラックボックステストを行いましたが、is_valid_date 関数の他の仕様、例えば「暦上あり得ない日付」の場合に False を返却するという仕様に対してブラックボックステストを行えば、また新たなバグも見つけ出すことができます(具体的には、月や日に負の値を指定した場合でも True が返却されるようになっています)。そして、これらのバグを修正する際に関数内の条件分岐が増えるのであれば、またホワイトボックステストを追加してバグの有無を確認してやれば良いです。

こんな感じで、ブラックボックステストとホワイトボックステストの追加を繰り返し行いながらバグを修正していくことで、テスト対象の品質をどんどん向上させていくことができます。

今回はホワイトボックステストとブラックボックステストを順々に繰り返す例を示しましたが、個人的には、まずは必要なテストをブラックボックステストで行なって仕様通りの関数をしっかり作成したのちに、足りないテストをホワイトボックステストを追加していくのが効率が良いかなぁと思っています。

ブラックボックステストでテストケースを洗い出すための手法としては、例えば、境界値テスト、デシジョンテーブルテストなどが存在します。これらを利用しながらテストケースを洗い出し、テスト結果をフィードバックしながらまずは仕様を満たす関数を作成していきましょう!それからホワイトボックステストで足りないテストを補っていくのが良いと思います。

いずれにせよ、ブラックボックステストもホワイトボックステストも重要なので、ぜひ両方を使いこなせるようにしておきましょう!また、特にホワイトボックス観点で足りないテストの追加はカバレッジが参考になりますし、カバレッジは今回紹介した手法で簡単に計測することができるため、是非これらのやり方についても覚えておいてください!

まとめ

このページでは、Python で unittest を利用して単体テストを行う手順や実例について解説しました!

また、coverage を利用したカバレッジの計測についても解説しています。

単体テストを実装するのも大変ですが、一度単体テストを実装してしまえば以降はコマンドの実行のみでテストを実施することができます。もちろん、Jenkins などの CI ツールを利用すればデイリーでテストを実施し、日々ソフトウェアの品質状況を管理することもできるようになります。

ただし、今回説明した内容を読んでいただければ分かるように、単体テストは単に実施すれば良いというものではなく、テスト対象の仕様や内部構造に対して網羅的にテストを実施する必要があります。そのため、用意するテストケースも重要となりますので、こういったテストケースの作り方についての手法についても合わせて理解しておくのが良いと思います。今回取り扱ったカバレッジは、その手法の1つとなります。

今回は unittest の基本的な使い方のみを解説しましたが、今後もう少し詳細な使い方についても解説したいと思っていますので、また機会があれば是非サイトに訪れてみていただければと思います!

同じカテゴリのページ一覧を表示