【Python】unittest入門:基礎から実務までわかりやすく解説

はじめに

Pythonのコードを書き始めると、動作を確かめるために「print」文を入れて確認するという方も多いでしょう。
しかし、開発が進むにつれテスト箇所が増え、状況によってはテストそのものに時間がかかることがあります。
そこで役立つのが、unittest というPython標準ライブラリのテストフレームワークです。
unittestを使うと、テストを自動で実行し、コードの変更で壊れた部分がないかを確かめることができます。
また、チーム開発ではユニットテストを組み込むことで、バグの早期発見に貢献し、安心してコードをリファクタリングすることができるようになります。

この記事では、プログラミング初心者の皆さんに向けて、unittestの基本的な仕組みから実務での活用方法までをわかりやすくまとめました。
コード例も交えながら一歩ずつ解説するので、具体的にどのようにテストコードを書けばいいのかイメージしやすくなるはずです。
これを読めば、テストを書く大切さや、テストがもたらすメリットがきっと理解できるでしょう。

この記事を読むとわかること

  • Pythonのunittestでテストを行う基本的な流れ
  • テストコードの具体的な書き方と実行方法
  • 実務での活用シーンとチーム開発におけるメリット
  • フレームワークの応用的な機能(skipやmockなど)の使い方

unittestとは何か

unittestが生まれた背景

unittest は、Pythonが標準で提供しているユニットテスト用のフレームワークです。
ユニットテストとは、プログラムの最小単位である関数やメソッドごとに、想定どおりの動作をするかを確認するテスト手法を指します。
なぜこの仕組みが必要になったかというと、大規模化したプロジェクトでは、手動でテストを行うのが負担になり、コードの品質を保つためにも自動テストが不可欠と考えられるようになったからです。

Pythonでは他にもテストフレームワークがありますが、unittestは標準ライブラリとして組み込まれているため、追加インストールが不要で気軽に始めやすいです。
スクリプトを書いた後にサクッとテストを書く、という流れを取り入れやすい点が魅力といえるでしょう。

unittestの目的

unittestの最大の目的は、コードの動作保証保守性の向上 にあります。
小さな機能を実装してはテストを書き、問題がなければ次に進むというサイクルを回すことで、致命的なエラーを初期の段階で防ぎやすくなるのです。
特に、時間が経ってからコードを変更しなければならない場面では、テストがすでにあることで「どこが壊れたのか」を素早く把握できるようになります。

このように、テストというと面倒だと感じるかもしれませんが、unittestを使うことで比較的スムーズに導入することができます。
プロジェクトを長期的に安定させるためにも、ぜひ最初からテストコードを書くことを習慣にしてみてください。

unittestのメリット

コードの信頼性を高める

unittestを利用すると、関数やメソッドが想定どおりの結果を返すかを細かくチェックできます。
結果として、開発者がコードに対して「ちゃんと動いているはず」という根拠を得られるようになります。
この根拠があることで、必要な変更やリファクタリングを安心して行うことが可能になります。

チーム開発がスムーズになる

複数の開発者が共同で作業するプロジェクトでは、誰かの変更がほかの人のコードを壊すリスクがあります。
unittestを導入しておけば、変更が原因でテストが失敗したときにすぐに気づけるため、早期に修正ができるのです。
結果的に大きな不具合に発展するリスクを減らし、トラブルシューティングにかかる時間も短縮できます。

長期的な保守コストを削減できる

一度書いたコードは、後から仕様変更やバグ修正のために再度手を入れることが少なくありません。
その際、unittestによるテストコードが既にあれば、変更が引き起こす予期せぬ不具合を検出しやすくなります。
これにより修正の手戻りを減らし、結果として保守コストを引き下げられます。

unittestの基本的な仕組み

テストケース(TestCase)

unittestでは、テストをクラス単位で管理します。
Pythonの unittest.TestCase クラスを継承したクラスの中に、テストしたいメソッドを定義していくのが一般的な書き方です。
テストメソッド名は test_ で始めることで、自動的に「テスト対象」として認識されます。

import unittest

class MyTestCase(unittest.TestCase):
    def test_example(self):
        self.assertEqual(1 + 1, 2)
        self.assertTrue(isinstance("Hello", str))

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

このようにクラスの中にテストをまとまった形で書くため、テスト内容を把握しやすくなります。
また、複数のテストメソッドを追加していくときも、同じクラスや別のクラスとして整理できるため、プロジェクト規模が大きくなっても管理がしやすくなるでしょう。

テストスイート(TestSuite)

テストケースを複数集めたものをテストスイートと呼びます。
複数のクラスに分割されたテストをひとまとめにし、連続して実行させたいときなどに利用されます。
とはいえ、最初のうちはテストスイートを明示的に意識しなくても、単純に unittest.main() を使ってテストを実行すれば、すべてのテストをまとめて走らせることが可能です。

テストランナー(TestRunner)

テストを実行し、結果をまとめて出力する仕組みがテストランナーです。
unittestでは、標準のテストランナーが unittest.main() によって提供され、コマンドラインから python -m unittest と実行する方法も一般的です。
これらを組み合わせることで、一度に複数のテストを実行し、成功・失敗をまとめて把握できるようになります。

unittestのインストールと準備

unittestは標準ライブラリ に含まれているので、追加のインストールは不要です。
Pythonをインストールした環境であれば、すぐに使い始められる点が大きなメリットです。
プロジェクトを新規で始めるなら、tests ディレクトリなどをあらかじめ用意し、その中にテストファイルを配置すると整理しやすくなります。

テストファイルはメインのコードファイルと区別しやすいように、ファイル名を test_XXXXX.py とする慣習があります。

基本的なテストの書き方

assert系メソッドを使ったテスト

unittestには便利なアサーション(assert)用メソッドが用意されています。
代表的なものとしては以下のようなメソッドがあります。

  • assertEqual (a, b) : a と b が等しいことを確認
  • assertTrue (x) : x が真であることを確認
  • assertFalse (x) : x が偽であることを確認
  • assertIn (a, b) : a が b の中に含まれていることを確認
  • assertRaises (Error) : 指定したエラーが発生することを確認

これらのメソッドを使って、期待する動作と実際の動作が一致するかをチェックします。
次の例は、足し算の結果と文字列の一致を検証しています。

import unittest

def add_numbers(x, y):
    return x + y

class TestAddNumbers(unittest.TestCase):
    def test_add_integers(self):
        result = add_numbers(2, 3)
        self.assertEqual(result, 5)

    def test_add_strings(self):
        result = add_numbers("Hello", "World")
        self.assertEqual(result, "HelloWorld")

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

assertEqual(result, 5) のように、期待する結果を第二引数に書いておくと、何が期待されていたのかが明確になります。
テストが失敗した場合は、どこでどのような値が違っていたのかをコンソールに表示してくれるので、トラブルシューティングの足がかりになります。

setUp / tearDown

テストを複数書く際に、毎回のテストメソッド実行前や実行後に共通で行う初期化や終了処理をまとめたい場合があります。
そのようなときには、setUp() メソッドと tearDown() メソッドを使うと便利です。

import unittest

class SampleTest(unittest.TestCase):
    def setUp(self):
        # テストの前処理。たとえばDBへの接続や共通データの用意
        self.data = []

    def tearDown(self):
        # テストの後処理。ファイルのクローズやDBセッションの終了など
        self.data = None

    def test_append(self):
        self.data.append(10)
        self.assertEqual(len(self.data), 1)

    def test_empty(self):
        self.assertEqual(len(self.data), 0)

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

setUp() はテストメソッドが呼ばれるたびに実行され、後処理をまとめる tearDown() も各テストメソッド終了後に実行されます。
テスト環境を毎回リセットしたい場合にも重宝するため、テストコードが増えてきたら積極的に活用すると良いでしょう。

setUpClass / tearDownClass

setUpClass()tearDownClass() はクラス全体で一度だけ実行されるメソッドです。
たとえば大規模なテストで、テストクラスごとに一回だけ重たい処理を実行したい場合に使われます。
書き方は以下のとおりで、クラスメソッドとして実装する点が特徴です。

import unittest

class BigTest(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        # クラス全体で最初に一度だけ呼ばれる
        cls.shared_resource = "Some big resource"

    @classmethod
    def tearDownClass(cls):
        # クラス全体で最後に一度だけ呼ばれる
        cls.shared_resource = None

    def test_use_resource(self):
        self.assertIsNotNone(self.shared_resource)

    def test_resource_value(self):
        self.assertEqual(self.shared_resource, "Some big resource")

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

これにより、テストクラス内で共通して使う大規模データや外部リソースのセットアップを最低限の回数で済ませられます。
ただし、必要のない場合は通常の setUp()tearDown() で十分です。

実装例1: シンプルな関数をテストする

テスト対象の関数

ここでは、割引後の価格を計算する関数を例に挙げます。
計算ロジックが増えたときにどう動作を担保するか、イメージしてみるとわかりやすいでしょう。

def calculate_discount_price(original_price, discount_rate):
    """
    original_price: 元の価格(数値)
    discount_rate: 割引率(0〜1の小数)
    """
    discounted = original_price * (1 - discount_rate)
    # 小数点以下は切り捨てる例
    return int(discounted)

割引率を 0.2 で指定すれば、20%オフした価格を整数で返すという単純なロジックです。

テストコード例

unittestを使ってこの関数をテストするには、次のように書くことができます。

import unittest
from discount import calculate_discount_price  # 上記の関数が入ったファイルを想定

class TestDiscount(unittest.TestCase):
    def test_normal_rate(self):
        # 20%オフ
        result = calculate_discount_price(1000, 0.2)
        self.assertEqual(result, 800)

    def test_zero_rate(self):
        # 割引率0の場合は元の値段がそのまま返る
        result = calculate_discount_price(500, 0.0)
        self.assertEqual(result, 500)

    def test_full_rate(self):
        # 100%オフ
        result = calculate_discount_price(1000, 1.0)
        self.assertEqual(result, 0)

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

このように、テストケースごとに「入力」と「期待する出力」を明確に分けておくと、テストの可読性が高まります。
たとえば将来、割引の計算方法が変わった際にも、このテストが失敗すればすぐに気づけるため、安心して修正ができます。

実装例2: クラスをテストする

テスト対象のクラス

今度は、ショッピングカートを簡易的に表現したクラスを例にします。
アイテムを追加したり、合計金額を返したりする機能があるとイメージすると、実務にも近いでしょう。

class ShoppingCart:
    def __init__(self):
        self.items = []

    def add_item(self, name, price):
        self.items.append({"name": name, "price": price})

    def total_price(self):
        return sum(item["price"] for item in self.items)

このクラスは単純ですが、アイテムのリスト管理と合計価格の計算という実務でよくある要素が含まれています。

テストコード例

以下のテストコードでは、アイテムを追加したあとのリスト内容と合計価格をチェックする例を示しています。

import unittest
from cart import ShoppingCart  # 先ほどのクラスが含まれているファイルを想定

class TestShoppingCart(unittest.TestCase):
    def test_add_item(self):
        cart = ShoppingCart()
        cart.add_item("Book", 1200)
        self.assertEqual(len(cart.items), 1)
        self.assertEqual(cart.items[0]["name"], "Book")
        self.assertEqual(cart.items[0]["price"], 1200)

    def test_total_price(self):
        cart = ShoppingCart()
        cart.add_item("Book", 1200)
        cart.add_item("Pen", 100)
        self.assertEqual(cart.total_price(), 1300)

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

このように、クラスのインスタンスを作ってメソッドを動かし、想定どおりにリストが更新されているか、合計が正しいかを確認できます。
実務でクラスの規模が大きくなっても、テストの書き方の基本は同じで、しっかりと必要なアサーションを積み重ねればOKです。

便利な機能

テストのスキップ機能

特定の条件下でテストを一時的に実行しないようにしたい場合、unittest.skip() デコレータが役に立ちます。
または、条件付きでスキップする unittest.skipIf(condition, reason) などもあります。

import unittest

class ConditionalSkipTest(unittest.TestCase):
    @unittest.skip("このテストは現在スキップ中")
    def test_skip_example(self):
        self.assertEqual(1, 2)

    @unittest.skipIf(True, "条件が真なのでスキップ")
    def test_skip_if_example(self):
        self.assertEqual(2, 2)

このように一部のテストだけを一時的に外すことで、メンテナンス中の機能や未実装の機能に対して無理に失敗を起こさなくて済むようになります。

例外のテスト

関数がある特定の条件で例外を発生させることをテストしたい場合は assertRaises を使います。

import unittest

def divide(a, b):
    return a / b

class TestException(unittest.TestCase):
    def test_divide_by_zero(self):
        with self.assertRaises(ZeroDivisionError):
            divide(10, 0)

こうすることで、「divide 関数が ZeroDivisionError を出すはず」と期待している場合に、それが正しく起きないとテストが失敗します。

サブテスト

サブテストは unittest で複数のパラメータをまとめてテストする際に使えるテクニックです。
self.subTest() を使うことで、テストメソッド内でループしながらパラメータごとに独立したテスト結果として扱うことができます。

import unittest

class TestSubTest(unittest.TestCase):
    def test_multiple_values(self):
        test_data = [(1, 2, 3), (2, 2, 4), (3, 3, 6)]
        for x, y, expected in test_data:
            with self.subTest(x=x, y=y, expected=expected):
                self.assertEqual(x + y, expected)

これにより、一度に多数のパラメータを検証できるうえ、どの組み合わせで失敗したかが分かりやすくレポートされます。

unittest.mock

外部APIにアクセスするコードや時間のかかる処理を行うコードをテストする場合には、unittest.mock モジュールが便利です。
モック化することで、実際の外部リソースに接続せずにテストが行えます。
以下は HTTPリクエストをモック化するイメージです。

import unittest
from unittest.mock import patch
import requests

def fetch_data(url):
    r = requests.get(url)
    return r.json()

class TestFetchData(unittest.TestCase):
    @patch("requests.get")
    def test_fetch_data(self, mock_get):
        # モックの戻り値を定義
        mock_get.return_value.json.return_value = {"status": "ok"}
        
        result = fetch_data("http://example.com")
        self.assertEqual(result["status"], "ok")

@patch デコレータで requests.get をモック化しているため、実際のHTTPリクエストが発生せず、テスト中に外部アクセスが行われることを防ぎます。

テストの実行方法

コマンドラインからの実行

最もシンプルなのは、テストファイルを直接 python test_XXXXX.py のように実行する方法です。
ファイル末尾で unittest.main() を呼んでおけば、テストメソッドが自動的に走り、結果が表示されます。

テストディスカバリを利用する

python -m unittest discover と実行すると、指定したディレクトリ以下の test_ から始まるファイルを一括で探して実行してくれます。
プロジェクト全体にまたがるテストを効率良く回せるので、大規模になった場合はこちらが便利です。

python -m unittest discover -s tests

-s はディレクトリを指定するオプションで、必要に応じて調整してください。

実務での活用

CI/CDパイプラインとの連携

チーム開発や運用で大きな利点を感じるのは、 継続的インテグレーション (CI) との連携です。
Gitなどでコードをプッシュすると自動的にunittestが実行され、テストが全てパスしないとマージできないように設定しておくのが一般的です。
これにより、品質を維持しながら開発をスピーディーに進めることが可能になります。

大規模開発やチーム開発での運用

ファイル数が増えても、unittestならテストディスカバリ機能を使って一気に実行できます。
また、機能ごとにテストクラスを分ければ、可読性と保守性を保ちやすいです。
「このクラスはどんなテストをしているのか」「どのメソッドが正しく動くか」という点を明文化できるので、メンバー同士でコードをレビューするときもスムーズです。

バグ再現テスト

実務では、バグが報告されたときに、そのバグを再現するテストを先に書いて、テストを失敗させる状態を作ってから修正する手法もよく使われます。
これを行うことで、同じバグが今後再発しないように監視する仕組みをテストコードに組み込めるのです。
バグが直った後もテストコードを残しておけば、バグの原因となったケースを何度でもチェックできます。

プロジェクトが大きくなるほど、バグ再現テストは後々の安心材料になることが多いです。

まとめ

ここまで、unittest を使ったテストの基本から、活用シーンや便利な機能までを見てきました。
最初は「テストを書くのは手間がかかりそう」と思うかもしれませんが、実際にはunittestの仕組みでテストを整理すれば、日々の開発がかなり楽になります。
特にチーム開発や長期運用を想定しているプロジェクトであれば、テストコードがあることによる安心感は大きいでしょう。

  • テストコードは unittest.TestCase を継承したクラス内に test_ で始まるメソッドとして書く
  • assertEqualassertTrue などのアサートメソッドを使うと、結果の差異がすぐにわかる
  • setUp()tearDown() を活用すると、テスト前後の準備や後処理をまとめて管理できる
  • unittest.mock やサブテストなどの機能を使えば、実際の動作をより正確にテストできる
  • 大規模プロジェクトでは、CI/CDと組み合わせることで品質を効率的に維持できる

これらを意識しながらテストコードを書く練習をすれば、自然と実務でも通用するテストスキルが身につくはずです。
特にバグ修正の際には「なぜバグが起きたのか」を検証するための便利な道具になるので、ぜひunittestを活用してみてください。

Pythonをマスターしよう

この記事で学んだPythonの知識をさらに伸ばしませんか?
Udemyには、現場ですぐ使えるスキルを身につけられる実践的な講座が揃っています。