フリーキーズ | 独学プログラミング

Python初心者向けunittestの使い方!標準テストフレームワークで始めるテスト駆動開発

リンドくん

リンドくん

たなべ先生、プログラミング学習してると「テスト」って言葉をよく聞くんですけど、これって何ですか?

たなべ

たなべ

テストはプログラムが正しく動作するかを自動的に確認する仕組みなんだ。
Pythonには標準で「unittest」というテストフレームワークが付いているから、今日はその使い方を学んでいこう。

プログラミングを学んでいると、作ったプログラムが「本当に正しく動いているのか」という不安を感じたことはありませんか?
手動でいちいち確認するのは面倒だし、複雑なプログラムになればなるほど、すべての動作を確認するのは現実的ではありませんよね。

そんな時に役立つのがテストフレームワークです。
中でもPythonの標準ライブラリに含まれている「unittest」は、追加のインストールが不要で、すぐに使い始められる優れたツールです。

この記事では、プログラミング初心者の方でもunittestを活用できるよう、基本的な概念から実際のコードまで、わかりやすく解説していきます。

プログラミング学習でお悩みの方へ

HackATAは、エンジニアを目指す方のためのプログラミング学習コーチングサービスです。 経験豊富な現役エンジニアがあなたの学習をサポートします。

✓ 質問し放題

✓ β版公開中(2025年内の特別割引)

HackATAの詳細を見る

unittestとは?なぜテストが重要なのか

リンドくん

リンドくん

そもそも、なぜテストを書く必要があるんでしょうか?

たなべ

たなべ

プログラムが大きくなると、一部の変更が別の部分に影響することがあるんだ。
テストがあれば、変更後も既存の機能が正常に動作するか自動的に確認できるから、安心してプログラムを改良できるんだよ。

unittestの基本概念

unittestは、Pythonの標準ライブラリに含まれているテストフレームワークです。
他の言語のJUnitやNUnitに影響を受けて作られており、以下のような特徴があります。

  • 標準ライブラリなので追加インストール不要
  • オブジェクト指向的なテスト記述
  • 豊富なアサーション(検証)メソッド
  • テスト結果の詳細なレポート機能

テストを書くメリット

プログラムにテストを書くことで、以下のようなメリットが得られます。

バグの早期発見
プログラムの問題点を開発段階で発見できるため、後から修正するコストを削減できます。

安全なリファクタリング
コードの構造を改善する際も、テストが通ることを確認しながら作業できるため、既存機能を壊すリスクが大幅に減ります。

仕様の明文化
テストコードを読むことで、そのプログラムがどのような動作をするべきかが明確になります。

開発効率の向上
手動でのテストが不要になるため、長期的には開発効率が大きく向上します。

実際の開発現場では、テストのないコードは「技術的負債」と呼ばれ、将来的な保守コストを増大させる要因として認識されています。
だからこそ、早い段階からテストを書く習慣を身につけることが重要なのです。

基本的なテストケースの書き方

リンドくん

リンドくん

実際にunittestはどうやって書くんですか?

たなべ

たなべ

まずは簡単な関数をテストする例から始めてみよう。
テストしたい機能を作って、そのテストを書くという流れで説明するね。

シンプルな関数をテストしよう

まず、テスト対象となる簡単な関数を作成しましょう。計算機能を持つcalculator.pyというファイルを作成します。

# calculator.py
def add(a, b):
    """2つの数値を足し算する関数"""
    return a + b

def subtract(a, b):
    """2つの数値を引き算する関数"""
    return a - b

def multiply(a, b):
    """2つの数値を掛け算する関数"""
    return a * b

def divide(a, b):
    """2つの数値を割り算する関数"""
    if b == 0:
        raise ValueError("ゼロで割ることはできません")
    return a / b

基本的なテストの書き方

次に、この計算機能をテストするtest_calculator.pyを作成します。

# test_calculator.py
import unittest
from calculator import add, subtract, multiply, divide

class TestCalculator(unittest.TestCase):
    """計算機能のテストクラス"""
    
    def test_add(self):
        """足し算のテスト"""
        # 正常なケース
        self.assertEqual(add(2, 3), 5)
        self.assertEqual(add(-1, 1), 0)
        self.assertEqual(add(0, 0), 0)
    
    def test_subtract(self):
        """引き算のテスト"""
        self.assertEqual(subtract(5, 3), 2)
        self.assertEqual(subtract(1, 1), 0)
        self.assertEqual(subtract(-1, -1), 0)
    
    def test_multiply(self):
        """掛け算のテスト"""
        self.assertEqual(multiply(3, 4), 12)
        self.assertEqual(multiply(-2, 3), -6)
        self.assertEqual(multiply(0, 5), 0)
    
    def test_divide(self):
        """割り算のテスト"""
        self.assertEqual(divide(6, 2), 3)
        self.assertEqual(divide(5, 2), 2.5)
    
    def test_divide_by_zero(self):
        """ゼロ除算エラーのテスト"""
        with self.assertRaises(ValueError):
            divide(10, 0)

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

テストの実行方法

テストを実行するには、以下のコマンドをターミナルで実行します。

python test_calculator.py

または、より詳細な出力が欲しい場合は-vオプションを付けて実行します。

python -m unittest test_calculator.py -v

実行結果は以下のようになります。

test_add (test_calculator.TestCalculator) ... OK
test_divide (test_calculator.TestCalculator) ... OK
test_divide_by_zero (test_calculator.TestCalculator) ... OK
test_multiply (test_calculator.TestCalculator) ... OK
test_subtract (test_calculator.TestCalculator) ... OK

----------------------------------------------------------------------
Ran 5 tests in 0.001s

OK

この結果から、すべてのテストが正常に通過したことがわかります。

よく使うアサーションメソッド

リンドくん

リンドくん

assertEqual以外にも、いろんなテストの方法があるんですか?

たなべ

たなべ

そうだね!unittestには様々なアサーションメソッドが用意されているんだ。
状況に応じて使い分けることで、より正確で読みやすいテストが書けるよ。

基本的なアサーションメソッド

unittestでよく使われるアサーションメソッドを、実例とともに紹介します。

import unittest

class TestAssertions(unittest.TestCase):
    """各種アサーションメソッドの使用例"""
    
    def test_equality(self):
        """等値性のテスト"""
        # 値が等しいかどうか
        self.assertEqual(1 + 1, 2)
        self.assertNotEqual(1 + 1, 3)
    
    def test_boolean(self):
        """真偽値のテスト"""
        self.assertTrue(1 < 2)
        self.assertFalse(1 > 2)
    
    def test_none(self):
        """None値のテスト"""
        value = None
        self.assertIsNone(value)
        
        value = "something"
        self.assertIsNotNone(value)
    
    def test_membership(self):
        """要素の包含テスト"""
        my_list = [1, 2, 3, 4, 5]
        self.assertIn(3, my_list)
        self.assertNotIn(6, my_list)
    
    def test_string_methods(self):
        """文字列関連のテスト"""
        text = "Hello World"
        self.assertStartsWith(text, "Hello")  # 存在しないメソッドの例
        # 正しくは以下のような方法で確認
        self.assertTrue(text.startswith("Hello"))
        self.assertTrue(text.endswith("World"))
    
    def test_numeric_comparison(self):
        """数値比較のテスト"""
        self.assertGreater(5, 3)
        self.assertLess(2, 4)
        self.assertGreaterEqual(5, 5)
        self.assertLessEqual(3, 3)
    
    def test_approximate_equality(self):
        """浮動小数点数の近似等価テスト"""
        result = 0.1 + 0.2
        # 浮動小数点の計算誤差を考慮
        self.assertAlmostEqual(result, 0.3, places=7)

例外のテスト

プログラムが適切なエラーを発生させるかどうかをテストすることも重要です。

def test_exceptions(self):
    """例外処理のテスト"""
    # 特定の例外が発生することを確認
    with self.assertRaises(ValueError):
        int("not_a_number")
    
    # 例外メッセージも確認したい場合
    with self.assertRaisesRegex(ValueError, "invalid literal"):
        int("not_a_number")

これらのアサーションメソッドを適切に使い分けることで、テストの意図がより明確になり、失敗時のエラーメッセージもより分かりやすくなります。

テストクラスの構成とsetUp/tearDown

リンドくん

リンドくん

同じような準備を毎回のテストでする場合はどうすればいいんですか?

たなべ

たなべ

それにはsetUptearDownメソッドを使うんだ。
テストの前後で自動的に実行されるから、重複するコードを減らせるよ。

setUp/tearDownの活用

テストの前後で共通の処理を実行したい場合は、setUptearDownメソッドを使用します。

import unittest
import tempfile
import os

class TestFileOperations(unittest.TestCase):
    """ファイル操作のテスト例"""
    
    def setUp(self):
        """各テストの実行前に呼び出される"""
        # テスト用の一時ディレクトリを作成
        self.test_dir = tempfile.mkdtemp()
        self.test_file = os.path.join(self.test_dir, "test.txt")
        
        # テスト用ファイルの作成
        with open(self.test_file, 'w') as f:
            f.write("Hello, unittest!")
    
    def tearDown(self):
        """各テストの実行後に呼び出される"""
        # テスト用ファイルとディレクトリの削除
        if os.path.exists(self.test_file):
            os.remove(self.test_file)
        if os.path.exists(self.test_dir):
            os.rmdir(self.test_dir)
    
    def test_file_exists(self):
        """ファイルの存在確認テスト"""
        self.assertTrue(os.path.exists(self.test_file))
    
    def test_file_content(self):
        """ファイル内容の確認テスト"""
        with open(self.test_file, 'r') as f:
            content = f.read()
        self.assertEqual(content, "Hello, unittest!")
    
    def test_file_modification(self):
        """ファイル変更のテスト"""
        new_content = "Modified content"
        with open(self.test_file, 'w') as f:
            f.write(new_content)
        
        with open(self.test_file, 'r') as f:
            content = f.read()
        self.assertEqual(content, new_content)

クラス全体の準備とクリーンアップ

個々のテストではなく、テストクラス全体に対して一度だけ実行したい処理がある場合は、setUpClasstearDownClassを使用します。

class TestDatabaseConnection(unittest.TestCase):
    """データベース接続のテスト例(疑似コード)"""
    
    @classmethod
    def setUpClass(cls):
        """テストクラス全体の実行前に一度だけ呼び出される"""
        print("データベース接続を開始")
        # cls.db_connection = create_test_database()
    
    @classmethod
    def tearDownClass(cls):
        """テストクラス全体の実行後に一度だけ呼び出される"""
        print("データベース接続を終了")
        # cls.db_connection.close()
    
    def setUp(self):
        """各テストの前に実行"""
        print("テストデータの準備")
    
    def tearDown(self):
        """各テストの後に実行"""
        print("テストデータのクリーンアップ")
    
    def test_database_query(self):
        """データベースクエリのテスト"""
        # 実際のテストロジック
        self.assertTrue(True)

このような構成により、テストコードの重複を減らし、より保守しやすいテストスイートを作成できます。

クラスのテスト

リンドくん

リンドくん

関数のテストはわかりましたが、クラスのテストはどう書けばいいんでしょうか?

たなべ

たなべ

クラスのテストでは、オブジェクトの状態変化メソッドの相互作用を確認することが重要だね。
実際のクラスを例に見てみよう。

テスト対象のクラス

まず、銀行口座を模擬したクラスを作成します。

# account.py
class BankAccount:
    """銀行口座クラス"""
    
    def __init__(self, account_holder, initial_balance=0):
        self.account_holder = account_holder
        self.balance = initial_balance
        self.transaction_history = []
    
    def deposit(self, amount):
        """入金処理"""
        if amount <= 0:
            raise ValueError("入金額は正の数である必要があります")
        
        self.balance += amount
        self.transaction_history.append(f"入金: {amount}円")
        return self.balance
    
    def withdraw(self, amount):
        """出金処理"""
        if amount <= 0:
            raise ValueError("出金額は正の数である必要があります")
        if amount > self.balance:
            raise ValueError("残高不足です")
        
        self.balance -= amount
        self.transaction_history.append(f"出金: {amount}円")
        return self.balance
    
    def get_balance(self):
        """残高確認"""
        return self.balance
    
    def get_transaction_history(self):
        """取引履歴の取得"""
        return self.transaction_history.copy()

クラステストの実装

このクラスをテストするテストコードを作成します。

# test_account.py
import unittest
from account import BankAccount

class TestBankAccount(unittest.TestCase):
    """銀行口座クラスのテスト"""
    
    def setUp(self):
        """各テストの前に新しい口座を作成"""
        self.account = BankAccount("田中太郎", 1000)
    
    def test_initial_balance(self):
        """初期残高のテスト"""
        self.assertEqual(self.account.get_balance(), 1000)
        self.assertEqual(self.account.account_holder, "田中太郎")
    
    def test_deposit_valid_amount(self):
        """正常な入金のテスト"""
        new_balance = self.account.deposit(500)
        self.assertEqual(new_balance, 1500)
        self.assertEqual(self.account.get_balance(), 1500)
    
    def test_deposit_invalid_amount(self):
        """不正な入金額のテスト"""
        with self.assertRaises(ValueError):
            self.account.deposit(-100)
        
        with self.assertRaises(ValueError):
            self.account.deposit(0)
        
        # 残高が変更されていないことを確認
        self.assertEqual(self.account.get_balance(), 1000)
    
    def test_withdraw_valid_amount(self):
        """正常な出金のテスト"""
        new_balance = self.account.withdraw(300)
        self.assertEqual(new_balance, 700)
        self.assertEqual(self.account.get_balance(), 700)
    
    def test_withdraw_insufficient_funds(self):
        """残高不足時の出金テスト"""
        with self.assertRaises(ValueError):
            self.account.withdraw(1500)
        
        # 残高が変更されていないことを確認
        self.assertEqual(self.account.get_balance(), 1000)
    
    def test_withdraw_invalid_amount(self):
        """不正な出金額のテスト"""
        with self.assertRaises(ValueError):
            self.account.withdraw(-50)
        
        with self.assertRaises(ValueError):
            self.account.withdraw(0)
    
    def test_transaction_history(self):
        """取引履歴のテスト"""
        self.account.deposit(200)
        self.account.withdraw(100)
        
        history = self.account.get_transaction_history()
        expected_history = ["入金: 200円", "出金: 100円"]
        self.assertEqual(history, expected_history)
    
    def test_multiple_operations(self):
        """複数操作の組み合わせテスト"""
        self.account.deposit(500)  # 1000 + 500 = 1500
        self.account.withdraw(200)  # 1500 - 200 = 1300
        self.account.deposit(100)   # 1300 + 100 = 1400
        
        self.assertEqual(self.account.get_balance(), 1400)
        
        history = self.account.get_transaction_history()
        self.assertEqual(len(history), 3)

このようなテストを通じて、クラスの各メソッドが期待通りに動作し、オブジェクトの状態が適切に管理されていることを確認できます。

テストの実行とカバレッジ

リンドくん

リンドくん

テストがどれくらいコードをカバーしているか確認する方法はありますか?

たなべ

たなべ

それはテストカバレッジという概念だね。
Pythonではcoverageというツールを使って、テストがコードのどの部分を実行しているかを可視化できるんだ。

coverageツールの使用

まず、coverageツールをインストールします。

pip install coverage

カバレッジの測定

テストのカバレッジを測定するには、以下のコマンドを実行します。

# テストを実行してカバレッジを測定
coverage run -m unittest test_calculator.py

# カバレッジレポートを表示
coverage report

HTMLレポートの生成

より詳細なレポートを見たい場合は、HTMLレポートを生成できます。

# HTMLレポートを生成
coverage html

# htmlcovディレクトリにレポートが生成される
# index.htmlをブラウザで開くと詳細を確認できる

テストの実行方法いろいろ

unittestには様々な実行方法があります。

# 特定のテストファイルを実行
python -m unittest test_calculator.py

# 詳細な出力で実行
python -m unittest test_calculator.py -v

# 特定のテストクラスのみ実行
python -m unittest test_calculator.TestCalculator

# 特定のテストメソッドのみ実行
python -m unittest test_calculator.TestCalculator.test_add

# ディスカバリー機能でテストを自動検出
python -m unittest discover

# 失敗時に即座に停止
python -m unittest test_calculator.py -f

これらの機能を活用することで、効率的にテストを実行し、コードの品質を継続的に監視できます。

まとめ

リンドくん

リンドくん

unittestってすごく便利ですね!これで安心してコードを書けそうです。

たなべ

たなべ

その通り!テストを書く習慣を身につけることで、より品質の高いプログラムを効率的に開発できるようになるよ。
まずは簡単な関数から始めて、徐々に複雑なテストにチャレンジしていこう。

この記事では、Pythonの標準テストフレームワークであるunittestの基本的な使い方から実践的な応用まで幅広く解説しました。

重要なポイントを再確認すると以下の通りです。

  • unittestは追加インストール不要でPythonに標準で付属している
  • テストケースはTestCaseクラスを継承して作成する
  • 様々なアサーションメソッドを使い分けることで、正確なテストができる
  • setUp/tearDownメソッドを活用して効率的なテストを作成できる
  • テストカバレッジを測定することで、テストの品質を客観的に評価できる

テスト駆動開発(TDD)は、現代のソフトウェア開発において必須のスキルとなっています。
最初は面倒に感じるかもしれませんが、テストを書く習慣を身につけることで、バグの少ない信頼性の高いプログラムを作れるようになります。

ぜひ今日から自分のプロジェクトにunittestを導入して、より安全で効率的なプログラミングライフを始めてみませんか?

この記事をシェア

関連するコンテンツ