Python Unittest|効率的なテストコードの書き方とリファクタリングの実践ガイド
この記事では、PythonのUnittestを活用してテストコードを作成し、リファクタリングを効率よく、かつ、安全に進める方法を解説します。テスト駆動型開発やテストコードを準備する有用性を感じてもらえればと思います。
ユニットテスト(単体テスト)
ユニットテストとは
ユニットテスト(単体テスト)とは、個々の関数やクラスに期待通りの機能が実装されているか確認するためのテストです。
ユニットテストは、関数やメソッド単位のテストで、プログラミング開発では最小単位のテストとなります。
このテストにより、関数やメソッドが正しく動作することを保証することができます。
関数やメソッド単位できっちりテストしておくと、結合テスト時にバグが出にくくなります。デバッグは、あとになるほど大変になるので、ここできっちりテストしておくと結果として、時間短縮や手間を減らすことができます。
ユニットテストのメリット
ユニットテストのメリットは、以下になります。
- バグが早期に発見できる
ユニットテストを実行することで、開発の初期段階でバグを発見することができます。システムを結合してからのデバッグは大変なので、ここでバグを発見しておくことは大切です。 - リファクタリングがやりやすい
ユニットテストがあれば、リファクタリングしたコードの動作確認が容易です。基本的に、テストを通過すればコードが正しく動作していることを保証できるので、リファクタリングミスを抑制することができます。 - ドキュメンテーションの役割
テストコードを見ると、コードの利用方法と期待する動作がわかります。テストコードは実際に動作するコードですので、ドキュメントと異なり、動作が保証されています。なので、正確なドキュメントとしての役割を果たしてくれます。 - 開発効率の向上
ユニットテストを自動化すれば、開発効率をアップできます。例えば、Github Actionsと組み合わせて、プルリクエストやタグ付けのタイミングでテストを行うようにすれば、自動的にテストを走らせてコードの品質を確認することができます。特にプルリクエスト時にテストを走らせれば、プルリクエスト承認者の工数を削減できます。
以下、テストコードの書き方と、実際のリファクタリング例を挙げながら、テストコードがリファクタリング時にどれだけ便利か解説します。
Python標準のunittest
Pythonにはunittestというモジュールが存在します。これを使えば、ユニットテストを簡単に記述、実行することができます。
unittest以外にもユニットテスト向けのライブラリは色々ありますが、個人的には標準があるなら標準のものを使う派です。
unittestの使い方(基本)
unittestを使うにはユニットテスト用の.pyファイルを用意します。このファイルでは、まず、
import unittest
でunittest
をインポートします。
続けて、テストターゲットのモジュールをインポートします(下プログラムのfrom
行)。
あとは、unittest.TestCase
を継承したクラスを作成し、テスト内容を記述していきます。
最後にif __name__ == '__main__'
で、unittest.main()
を呼び出すコードを書けばテストコードは完成です。
import unittest
from myModule import myFunc
class TestMyFunc(unittest.TestCase):
def test_xxxxx(self):
# ここにテストコードを書く
if __name__ == '__main__':
unittest.main()
テストの仕方
全てのテストを行う場合は、以下のようにユニットテストコードを実行します。
python3 ユニットテストのコード.py
一部のテストだけしたい場合は、以下のように実行するテストを続けて記述します。
python3 ユニットテストのコード.py TestMyFunc.test_xxxxx
以上のようにテストコードの記述と実行は簡単です。
テストコードのファイル名ですが、myfunc.pyというモジュールがある場合、私の場合、とりあえず同じフォルダにtest_myfunc.pyというテストコードを置いています。
unittestを使った例
以下、unittestを使って開発する例を説明します。
開発する関数
今回、開発するターゲットの関数は「素数判定」を行う関数です。関数名はisPrime(x)
とします。引数xは整数で戻り値は、素数であればTrue, 素数でなければFalseを返すものとします。
関数仕様は、なるべく細かく決めておいた方がよいので、今回は以下のような仕様を定義します。
- 素数の場合True, 素数ではない場合はFalseを返す
- 0または負の値は素数ではない(Falseを返す)
- 1は素数ではない(Falseを返す)
まずはテストを書いてみる
今回は、テスト駆動開発(TDD)っぽく、関数の実装前にテストコードを記述しておきます。
テストコードを書いていると、仕様漏れを見つけることがあります。先にテストを書くのは結構有用です。
テストコードは以下のようになります。
import unittest
from isPrime import isPrime
class TestIsPrime(unittest.TestCase):
prime = [
2, 3, 5, 7, 11, 13, 17, 19, 23, 29,
31, 37, 41, 43, 47, 53, 59, 61, 67, 71,
73, 79, 83, 89, 97, 101, 103, 107, 109, 113,
127, 131, 137, 139, 149, 151, 157, 163, 167, 173,
179, 181, 191, 193, 197, 199, 211, 223, 227, 229,
233, 239, 241, 251, 257, 263, 269, 271, 277, 281,
283, 293, 307, 311, 313, 317, 331, 337, 347, 349,
353, 359, 367, 373, 379, 383, 389, 397, 401, 409,
419, 421, 431, 433, 439, 443, 449, 457, 461, 463,
467, 479, 487, 491, 499, 503, 509, 521, 523, 541,
547, 557, 563, 569, 571, 577, 587, 593, 599, 601,
607, 613, 617, 619, 631, 641, 643, 647, 653, 659,
661, 673, 677, 683, 691, 701, 709, 719, 727, 733,
739, 743, 751, 757, 761, 769, 773, 787, 797, 809,
811, 821, 823, 827, 829, 839, 853, 857, 859, 863,
877, 881, 883, 887, 907, 911, 919, 929, 937, 941,
947, 953, 967, 971, 977, 983, 991, 997]
def test_prime(self):
for e in self.prime:
self.assertTrue(isPrime(e))
def test_not_prime(self):
for e in range(1000) :
if e in self.prime : continue
self.assertFalse(isPrime(e))
def test_zero(self) :
self.assertFalse(isPrime(0))
def test_minus(self) :
for e in range(-10, 0) :
self.assertFalse(isPrime(e))
def test_not_int(self) :
with self.assertRaises(TypeError):
isPrime(3.14)
isPrime("abc")
if __name__ == '__main__':
unittest.main()
最初の配列は1000以下の素数のリストです。このリストを使ってテストを実施します。
prime = [
2, 3, 5, 7, 11, 13, 17, 19, 23, 29,
31, 37, 41, 43, 47, 53, 59, 61, 67, 71,
73, 79, 83, 89, 97, 101, 103, 107, 109, 113,
127, 131, 137, 139, 149, 151, 157, 163, 167, 173,
179, 181, 191, 193, 197, 199, 211, 223, 227, 229,
233, 239, 241, 251, 257, 263, 269, 271, 277, 281,
283, 293, 307, 311, 313, 317, 331, 337, 347, 349,
353, 359, 367, 373, 379, 383, 389, 397, 401, 409,
419, 421, 431, 433, 439, 443, 449, 457, 461, 463,
467, 479, 487, 491, 499, 503, 509, 521, 523, 541,
547, 557, 563, 569, 571, 577, 587, 593, 599, 601,
607, 613, 617, 619, 631, 641, 643, 647, 653, 659,
661, 673, 677, 683, 691, 701, 709, 719, 727, 733,
739, 743, 751, 757, 761, 769, 773, 787, 797, 809,
811, 821, 823, 827, 829, 839, 853, 857, 859, 863,
877, 881, 883, 887, 907, 911, 919, 929, 937, 941,
947, 953, 967, 971, 977, 983, 991, 997]
テスト項目は仕様から以下のものを考えました。
- test_prime()
素数が正しく素数と判定されるか - test_not_prime()
0から999までの値で素数が意外が素数でないと判定されるか - test_zero()
0を素数でないと判定するか - test_minus()
負の値を素数でないと判定するか - test_not_int()
整数値以外が入力されたら例外が発生するか(オプション)
Trueが返されるかどうかはassertTrue
で、Falseが返されるかはassertFalse
で確認できます。例外が発生するかどうかはwith self.assertRaises
で確認できます。
assertXXXXは色々な種類があります。詳しくは公式ドキュメントを参照してください。記事作成時のドキュメントのリンクはこちらです。
これで、仕様からテストコードが作成できました。
プログラムを作成
素数を調べるプログラムを作成します。
2~(x-1)
までの値で割り切れなければ素数、割り切れれば素数ではないので、xが素数かどうかは以下のようなプログラムで判定可能です。
def isPrime(x: int) -> bool :
if x <= 1 : return False
for e in range(2, x) :
if x%e == 0 : return False
return True
コードが書けたので、テストしてみます。
> python3 test_isPrime.py
.....
----------------------------------------------------------------------
Ran 5 tests in 0.001s
OK
上記は実行結果です。
トータルで5つのテストがありますが、テスト結果はRan 5 tests ... OK
となっているので、全てのテストが通過したことがわかります。
テストをすべて通過したので、isPrime
関数は正しく実装できたようです。
ところで、ここで作成したisPrime
関数は「2以上x未満の数字で試し割をして、割れなければ素数」という処理を愚直に実装したものです。
このプログラムは最悪x回のループを行います。xが大きくなると処理が重くなるので、以下では速度改善を検討します。
リファクタリング
リファクタリングは、ソフトウェアの外部の動作(インタフェース)を変えずに、内部構造を改善するプロセスのことです。今回は、速度改善を目標としてリファクタリングを行なっていきます。
実は、ユニットテストが存在していると、リファクタリングが楽になります。というのも、内部構造を書き換えてた後にテストをパスすれば良いからです。
テストコードがしっかりしていれば、リファクタリングした影響で、システム全体も問題がでる可能性がほぼゼロにすることができます。
ループ数を減らす
リファクタリングの第一弾として、ループ回数を減らすことを考えます。
最初のプログラムでは、「2からx-1までループ」させていましたが、xがyで割り切れてzになる場合、x = yzと表すことができます。
たとえば、12は2で割ると6になります。6で割ると2になります。yとzの関係を見ると以下のようになります。これを見るとわかりますが、半分でyとzが逆になります。
つまり「$\sqrt x$までループ」すれば良いことになります。
y z
1 12
2 6
3 4
4 3
6 2
12 1
ということでコードを以下のように書き直します。e=2
を代入してe*e <= x
までループさせることで$\sqrt x$までのループを実現しています。
def isPrime(x: int) -> bool :
if x <= 1 : return False
e = 2
while e*e <= x :
if x%e == 0 : return False
e += 1
return True
書き直した後に、テストを実行して、テストがパスできるか確認します。
最初書き直した時に、条件をe*e < x
としていてテストでエラーが発生しました。テストコードを作成してたので早期にバグが発見できました。
偶数の処理をしない
もう少しだけ高速化してみます。「偶数で素数なのは2だけ」なので、2以外の偶数ではFalseを返し、奇数だけループさせるように修正してみます。
プログラムは以下のようになります。
def isPrime(x: int) -> bool :
if x <= 1 : return False
if x%2 == 0 :
if x == 2 : return True
else : return False
e = 3
while e*e <= x :
if x%e == 0 : return False
e += 2
return True
こちらも、書き換えた後にユニットテストし、パスするかどうか確認します。
リファクタリングにおけるユニットテストコードのメリット
以上、速度向上のためのリファクタリングを行いました。また、リファクタリングを行ったあとにテストがOKになるかの確認も行いました。
リファクタリングによりエラーが発生するというのは避けたいものです。
あらかじめユニットテストのコードが用意されている場合、書き換えミスの発生を簡単に確認することができます。
テストコードがきちんと書かれていれば、テストを走らせるだけでリファクタリング後のコードの安全性を確認できるのでリファクタリングがやりやすくなります。
もし、ユニットテストがなかったらリファクタリングでの影響の確認が大変になります。最悪、性能向上が期待できるにも関わらず、リファクタリングを諦めることににもなりかねません。
まとめ
Pythonのunittestの使い方と、テストコードをつかったリファクタリングについて解説しました。