Python単体テスト チュートリアルとベストプラクティス
By JoeVu, at: 2024年6月3日11:13
Estimated Reading Time: __READING_TIME__ minutes


はじめに
概要
At Glintecoでは、ユニットテスト/統合テスト/自動化テスト/ストレステストは、すべてのプロジェクトにおいて非常に重要です。私たちは「優れた開発者は、自身のコードに対するテストを書かなければならない」と信じています。
ユニットテストは、ソフトウェア開発における基本的な実践であり、ソフトウェアアプリケーションの個々のユニットまたはコンポーネントをテストして、期待通りに動作することを確認することを含みます。ユニットとは、ソフトウェアの最小限でテスト可能な部分であり、通常は関数またはメソッドです。これらのユニットを分離してテストすることにより、開発者は開発サイクルの早期にバグを特定して修正し、より堅牢で信頼性の高いソフトウェアを作成できます。
ユニットテストは重要です。なぜなら、以下のような利点があるからです。
- コードの正確性を検証します。
- 変更によって既存の機能が壊れないようにすることで、コードのリファクタリングを容易にします。
- コードの動作方法を示すコードのドキュメントとして機能します。
- 早期にバグを発見し、修正コストを削減します。
- ソースコードの品質を向上させます。
多くの理由により、多くのシニア開発者がこれをうまく管理することはできませんが、プログラミングの専門知識を向上させるためには、ユニットテストを書くことが不可欠です。
目的
この記事では、Pythonのunittest
フレームワークを使用してユニットテストを作成および実行するための完全なガイドを提供します(pytestについてはここでは説明しません。pytestとunittestの比較記事があります)。初心者でも経験豊富な開発者でも、このチュートリアルはunittest
の基本を理解し、効果的なテストを作成するためのベストプラクティスを学ぶのに役立ちます。
この記事の最後には、以下ができるようになります。
unittest
テストケースの基本構造を理解します。
- テストの作成、実行、整理方法を学びます。
- テストフィクスチャ、モッキング、テストスイートなどの高度な機能について探ります。
- テストの品質と保守性を向上させるためのベストプラクティスを発見します。
- 一般的な落とし穴とその回避方法を特定します。
unittest
を使用したユニットテストは、コードの信頼性と保守性を確保するための強力な方法であり、より良い製品につながります。unittest
の冒険を始めましょう。
Unittest入門
インストール
unittest
はPythonの標準ライブラリに含まれているため、Pythonでのユニットテストを開始するために追加のパッケージをインストールする必要はありません。スクリプトにunittest
をインポートするだけで、テストを作成する準備が整います。
基本構造
unittest
フレームワークの中心はTestCase
クラスです。テストケースはunittest.TestCase
をサブクラス化して作成され、個々のテストメソッドはこのクラス内で定義されます。各テストメソッドはtest
という単語で始まる必要があります。これにより、unittest
テストランナーによって自動的に認識され、実行されます。これは少しPythonicではないように見えますね?
以下は、unittest
テストケースの基本構造です。このファイルをtest.py
という名前で保存してください。
import unittest
def count_e_letters(name):
count = 0
for letter in name:
if letter == 'e':
count += 1
return count
class TestCountELetters(unittest.TestCase):
def test_count_e_letters(self):
count = count_e_letters("Joe")
self.assertEqual(1, count)
if __name__ == '__main__':
unittest.main()
テストの実行
テストを実行するには、スクリプトを保存し、コマンドラインから実行します。
❯ python test.py
.
----------------------------------------------------------------------
Ran 1 test in 0.000s
OK
テストが失敗すると、unittest
は期待される結果と実際の結果を含む、失敗に関する詳細情報を提供し、問題の診断に役立ちます。
❯ python -m unittest tests/test_strings.py
F.
======================================================================
FAIL: test_count_e_letters_with_a_none_input (tests.test_strings.TestCountELetters.test_count_e_letters_with_a_none_input)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/joe/Documents/WORK/GLINTECO/PROJECTS/INTERNAL/samples/unittest_tutorials/tests/test_strings.py", line 13, in test_count_e_letters_with_a_none_input
self.assertEqual(1, count)
AssertionError: 1 != 0
これらの基本事項を理解すれば、unittest
を使用して単純なテストを作成し始めることができます。次のセクションでは、セットアップとティアダウンメソッドを使用してテスト環境を効果的に管理するテストケースの作成について詳しく説明します。
テストケースの作成
セットアップとティアダウン
ユニットテストでは、テストの実行前に特定の環境を準備し、後処理を行うことが一般的です。unittest
は、これらを処理するためにsetUp
メソッドとtearDown
メソッドを提供します。setUp
メソッドは、各テストメソッドの前に呼び出され、テスト間で共有される状態を設定し、tearDown
メソッドは各テストメソッドの後に呼び出され、後処理を行います。これはRubyに似ています
例を以下に示します。
import unittest
class TestExample(unittest.TestCase):
def setUp(self):
self.user = User()
def tearDown(self):
self.user = None
def test_name(self):
self.assertEqual("Joe", self.user.name)
def test_action(self):
self.assertEqual("Playing Game", self.user.action())
上記からわかるように:
setUp
メソッドは、各テストの前にself.user
を新しいユーザーとして初期化します。
tearDown
メソッドは、各テストの後にself.user
をNone
に設定することで後処理を行います。
- 2つのテストメソッド(
test_name
とtest_action
)は、self.user
を使用してアサーションを実行します。
setUp
/tearDown
とsetUpClass/tearDownClass
の違いは何ですか?
setUp
とtearDown
- 目的:
setUp
とtearDown
メソッドは、各テストメソッドに必要なリソースのセットアップとクリーンアップに使用されます。
- 実行:
setUp
は各テストメソッドの実行前に呼び出され、tearDown
は各テストメソッドの終了後に呼び出されます。
- スコープ:これらのメソッドのセットアップとティアダウンのアクションは、
TestCase
クラス内の各テストメソッドに適用されます。つまり、複数のテストメソッドがある場合、setUp
とtearDown
は複数回(各テストメソッドごとに1回)実行されます。
import unittest
class TestExample(unittest.TestCase):
def setUp(self):
self.number = 1
print("Setting up before a test method")
def tearDown(self):
self.number = None
print("Tearing down after a test method")
def test_addition(self):
self.assertEqual(self.number + 1, 2)
def test_subtraction(self):
self.assertEqual(self.number - 1, 0)
出力:
Setting up before a test method
Tearing down after a test method
Setting up before a test method
Tearing down after a test method
setUpClass
とtearDownClass
- 目的:
setUpClass
とtearDownClass
は、個々のテストメソッドだけでなく、テストケースクラス全体に必要なリソースのセットアップとクリーンアップに使用されます。
- 実行:
setUpClass
はテストメソッドが実行される前に1回呼び出され、tearDownClass
はすべてのテストメソッドの実行が終了した後に1回呼び出されます。
- スコープ:これらのメソッドのセットアップとティアダウンのアクションは、テストケースクラス全体に適用されます。つまり、複数のテストメソッドがある場合、
setUpClass
とtearDownClass
はクラス全体に対して1回だけ実行されます。
import unittest
class TestExample(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.shared_resource = "Shared Resource"
print("Setting up class resources")
@classmethod
def tearDownClass(cls):
cls.shared_resource = None
print("Tearing down class resources")
def test_use_shared_resource(self):
self.assertEqual(self.shared_resource, "Shared Resource")
def test_another_use_of_shared_resource(self):
self.assertEqual(self.shared_resource, "Shared Resource")
出力:
Setting up class resources
Tearing down class resources
アサーション
アサーションは、テストの主要な構成要素です。条件が真かどうかをチェックし、そうでない場合は、テストが失敗したことを示すエラーを発生させます。unittest
は、さまざまな条件をチェックするためのさまざまなアサーションメソッドを提供します。一般的に使用されるアサーションをいくつか示します。
assertEqual(a, b)
:a
がb
と等しいかどうかをチェックします。
assertNotEqual(a, b)
:a
がb
と等しくないかどうかをチェックします。
assertTrue(x)
:x
がTrue
かどうかをチェックします。
assertFalse(x)
:x
がFalse
かどうかをチェックします。
assertIs(a, b)
:a
がb
かどうかをチェックします。
assertIsNot(a, b)
:a
がb
ではないかどうかをチェックします。
assertIsNone(x)
:x
がNone
かどうかをチェックします。
assertIsNotNone(x)
:x
がNone
ではないかどうかをチェックします。
assertIn(a, b)
:a
がb
内にあるかどうかをチェックします。
assertNotIn(a, b)
:a
がb
内になくかどうかをチェックします。
assertIsInstance(a, b)
:a
がb
のインスタンスかどうかをチェックします。
assertNotIsInstance(a, b)
:a
がb
のインスタンスではないかどうかをチェックします。
これらのアサーションステートメントを見てください。camelCaseの関数を含んでいるため、これもPythonicではないように見えます。
さまざまなアサーションを使用した例を以下に示します。
import unittest
class TestAssertions(unittest.TestCase):
def test_assertions(self):
self.assertEqual(1 + 1, 2)
self.assertNotEqual(2 + 2, 5)
self.assertTrue(3 < 5)
self.assertFalse(5 < 3)
self.assertIs(None, None)
self.assertIsNot(1, None)
self.assertIsNone(None)
self.assertIsNotNone(1)
self.assertIn(3, [1, 2, 3])
self.assertNotIn(4, [1, 2, 3])
self.assertIsInstance(3, int)
self.assertNotIsInstance(3, str)
テストの実行
テストはいくつかの方法で実行できます。
-
コマンドライン:コマンドラインからテストスクリプトを直接実行します。
python test_script.py
-
テストの検出:
unittest
の組み込みテスト検出メカニズムを使用して、テストを自動的に見つけて実行します。これは、多くのテストファイルを持つ大規模なプロジェクトに役立ちます。python -m unittest discover
このコマンドは、現在のディレクトリとそのサブディレクトリでテストモジュールを検索します。
-
IDEからの実行:PyCharm、VS Code、Eclipseなどのほとんどの統合開発環境(IDE)は、
unittest
テストの実行をサポートしており、テストの実行とデバッグのための便利なグラフィカルインターフェースを提供します。
project/
├── src/
│ └── example.py
└── tests/
├── __init__.py
└── test_example.py
tests/test_example.py
内:
import unittest
from src.example import add
class TestMathFunctions(unittest.TestCase):
def test_add(self):
self.assertEqual(add(1, 2), 3)
次を使用してテストを実行します。
python -m unittest discover -s tests
このコマンドは、unittest
にtests
ディレクトリ内のすべてのテストを検出して実行するように指示します。
高度な機能
モッキング
モッキングは、ユニットテストで使用されるテクニックであり、実際のオブジェクトを、実際のオブジェクトの動作をシミュレートするモックオブジェクトに置き換えます。これは、テスト対象のコードをその依存関係から分離する場合に特に役立ちます。
サンプルユースケース:関数AとBの2つの関数があります。関数Aのテストをすでに書いており、関数Bは関数Aを呼び出します。関数Bのユニットテストを書く必要があります。モックを使用する必要があります。
Pythonのunittest.mock
モジュールは、オブジェクトをモックするための強力なフレームワークを提供します。unittest.mock
の使用方法の例を以下に示します。
from unittest import TestCase
from unittest.mock import MagicMock, patch
class TestMocking(TestCase):
@patch('path.to.module.ClassName')
def test_mocking(self, mock_class):
instance = mock_class.return_value
instance.method.return_value = 'mocked!'
result = instance.method()
self.assertEqual(result, 'mocked!')
mock_class.assert_called_once()
instance.method.assert_called_once()
この例では:
@patch('path.to.module.ClassName')
は、path.to.module
のClassName
をモックオブジェクトに置き換えます。
mock_class.return_value
は、クラスのモックインスタンスです。
instance.method.return_value = 'mocked!'
は、メソッドの戻り値を'mocked!'
に設定します。
テストスイート
テストスイートを使用すると、複数のテストケースとテストメソッドを単一のスイートにグループ化し、まとめて実行できます。これは、テストの整理や特定のテストのサブセットの実行に役立ちます。
テストスイートの作成と実行方法の例を以下に示します。
import unittest
class TestMath(unittest.TestCase):
def test_add(self):
self.assertEqual(1 + 1, 2)
def test_subtract(self):
self.assertEqual(2 - 1, 1)
class TestStringMethods(unittest.TestCase):
def test_upper(self):
self.assertEqual('foo'.upper(), 'FOO')
def test_isupper(self):
self.assertTrue('FOO'.isupper())
self.assertFalse('Foo'.isupper())
if __name__ == '__main__':
suite = unittest.TestSuite()
suite.addTest(TestMath('test_add'))
suite.addTest(TestMath('test_subtract'))
suite.addTest(TestStringMethods('test_upper'))
suite.addTest(TestStringMethods('test_isupper'))
runner = unittest.TextTestRunner()
runner.run(suite)
この例では:
unittest.TestSuite()
はテストスイートを作成します。
suite.addTest(TestMath('test_add'))
は、個々のテストメソッドをスイートに追加します。
unittest.TextTestRunner()
はテストスイートを実行します。
テストのスキップ
特定のテストをスキップしたい場合があります。unittest
は、この目的のためにデコレーターを提供します。
@unittest.skip(reason)
:テストを無条件にスキップします。
@unittest.skipIf(condition, reason)
:条件が真の場合、テストをスキップします。
@unittest.skipUnless(condition, reason)
:条件が偽の場合、テストをスキップします。
例を以下に示します。
import unittest
class TestExample(unittest.TestCase):
@unittest.skip("demonstrating skipping")
def test_skip(self):
self.fail("shouldn't happen")
@unittest.skipIf(1 == 1, "not testing this right now")
def test_skip_if(self):
self.fail("shouldn't happen")
@unittest.skipUnless(1 == 0, "not testing this right now")
def test_skip_unless(self):
self.fail("shouldn't happen")
この例では、3つのテストすべてがさまざまな理由でスキップされます。
ベストプラクティス
ユニットテストでベストプラクティスを採用すると、テストの効果、保守性、信頼性を確保するのに役立ちます。unittest
フレームワークを使用するための重要なベストプラクティスをいくつか紹介します。
テストカバレッジ
高いカバレッジを目指しましょう:大部分のコードがテストされるように、高いテストカバレッジを目指しましょう。coverage.py
などのツールを使用して、コードのどの程度がテストによってカバーされているかを測定します。
pip install coverage
coverage run -m unittest discover
coverage report -m
Name Stmts Miss Cover Missing
-----------------------------------------------------
libs/__init__.py 0 0 100%
libs/strings.py 19 8 58% 15-24
tests/test_strings.py 19 0