テストとCI/CDの基本を独学で身につける完全ガイド|unittestからGitHub Actionsまで
📋 この記事の目次
ここまでの回で、自己紹介カードにゲストブック機能とSPAとしての動きが揃いました。機能が増えてくると、新しい悩みが出てきます。
「ダークモード機能を追加したら、いつのまにかゲストブックの表示が崩れていた」 「直したつもりのバグが、別の場所で再発していた」
これを防ぐのが自動テストと、そのテストを毎回自動で実行してくれるCI/CD(継続的インテグレーション/デリバリー) です。
テストは「出荷前の品質検査」だと考える
工場で製品を出荷する前に検査工程があるように、プログラムにも「これは正しく動いているか」を確認する検査工程があります。人間が毎回手作業で確認する代わりに、確認する処理そのものをプログラムとして書いておくのがテストです。
🧓 ベテランの現場コラム 「テストを書く時間がもったいない」と感じる独学者は多いですが、実際には逆です。テストがないと、機能を追加するたびに「他の場所が壊れていないか」を手作業で全部確認する羽目になり、長期的にはずっと時間がかかります。テストは「未来の自分への時短プレゼント」だと考えると書く気になりやすいです。
Part A: テストの基本概念
なぜテストが必要か・assertステートメント
def is_valid_score(score):
return 1 <= score <= 5
assert is_valid_score(3) == True
assert is_valid_score(0) == False
assertは「この条件が真でなければエラーにする」という文です。関数を作ったその場で、期待通りに動くかを簡単に確認できます。
関数の実装とテストを一緒に考える
def is_valid_score(score):
"""評価スコア(1〜5の整数)として妥当かを判定する"""
return isinstance(score, int) and 1 <= score <= 5
# テスト
assert is_valid_score(5) == True
assert is_valid_score(6) == False
assert is_valid_score("5") == False # 文字列は不正とみなす
「実装してからテストを書く」のではなく、「どんな入力に対してどう動くべきか」を先に洗い出しておくと、実装の抜け漏れに気づきやすくなります。
Part B: Pythonでのテスト(unittest)
テスト関数の作成とunittestフレームワーク
# test_guestbook_utils.py
import unittest
from guestbook_utils import is_valid_score
class TestIsValidScore(unittest.TestCase):
def test_valid_scores(self):
self.assertTrue(is_valid_score(1))
self.assertTrue(is_valid_score(5))
def test_invalid_scores(self):
self.assertFalse(is_valid_score(0))
self.assertFalse(is_valid_score(6))
self.assertFalse(is_valid_score("5"))
if __name__ == "__main__":
unittest.main()
python -m unittest test_guestbook_utils.py
unittestは、複数のテストをまとめて管理し、実行結果(成功・失敗)を一覧で見られるようにするPython標準の仕組みです。
Part C: Djangoでのテスト
テスト用データベースとModelのテスト
# guestbook/tests.py
from django.test import TestCase
from .models import Visitor, Message
class MessageModelTest(TestCase):
def setUp(self):
self.visitor = Visitor.objects.create(name="山田花子", email="hanako@example.com")
def test_message_creation(self):
message = Message.objects.create(visitor=self.visitor, body="参考になりました", score=5)
self.assertEqual(message.visitor.name, "山田花子")
self.assertEqual(Message.objects.count(), 1)
Djangoのテストは、実際のデータベースとは別にテスト専用の使い捨てデータベースを自動で用意し、テスト終了後に破棄してくれます。本番データを汚す心配なく、何度でも実行できます。
Client(クライアント)を使ったビューのテスト
from django.test import TestCase, Client
class GuestbookViewTest(TestCase):
def setUp(self):
self.client = Client()
def test_index_page_status_code(self):
response = self.client.get("/guestbook/")
self.assertEqual(response.status_code, 200)
def test_index_page_contains_heading(self):
response = self.client.get("/guestbook/")
self.assertContains(response, "ゲストブック")
Clientは、実際にブラウザでアクセスするのと同じように「このURLにアクセスしたら、どんなレスポンスが返るか」をテストできる仕組みです。
Part D: テスト駆動開発(TDD)
TDDのサイクル(Red-Green-Refactor)
- Red: まだ実装していない機能に対するテストを先に書く(当然失敗する)
- Green: テストが通る最低限の実装をする
- Refactor: テストが通ったままコードを整理する
# Red: まだ実装していない機能のテストを先に書く
def test_filter_high_score_messages(self):
Message.objects.create(visitor=self.visitor, body="良い", score=5)
Message.objects.create(visitor=self.visitor, body="普通", score=2)
high_score_messages = Message.objects.filter(score__gte=4)
self.assertEqual(high_score_messages.count(), 1) # この時点ではまだ実装がなくてもテスト自体は書ける
バグ修正におけるTDD
バグを見つけたときは、まず「そのバグを再現するテスト」を書きます(Red)。そのテストが通るように修正し(Green)、同じバグが再発しないかを将来にわたって自動チェックできる状態にします。
🧓 ベテランの現場コラム 「バグを直したらまず再現テストを書く」という順番は、現場でも徹底されているルールです。テストを書かずに直しただけでは、半年後に同じバグが再発しても誰も気づけません。
Part E: CI/CD(継続的インテグレーション/デリバリー)
基本概念とGitHub Actions
CI/CDは、「コードをGitHubにpushするたびに、自動でテストを実行する仕組み」です。人間が実行し忘れることがなくなり、チーム開発でも「テストが通ったコードだけがmainブランチに入る」という安心感が生まれます。
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install dependencies
run: pip install -r requirements.txt
- name: Run tests
run: python manage.py test
pushやpull_requestをきっかけに、GitHub上の仮想環境でテストを自動実行してくれます。テストが失敗すれば、GitHub上でひと目で分かるようになります。
トラブルシューティング
- ローカルでは通るのにCI上で失敗する → Pythonのバージョン差異や、環境変数の設定漏れが典型的な原因
- テストが途中で止まる → データベースのマイグレーションをCIの設定に含め忘れていないか確認する
Part F: ベストプラクティス
- テストの原則: 1つのテストで1つのことだけを確認する(あれもこれも1つのテスト関数に詰め込まない)
- 命名規則:
test_該当する状況_期待する結果のように、テスト名を読むだけで内容が分かるようにする - テストデータの管理:
setUpメソッドで共通の準備処理をまとめ、各テストの重複を減らす
実践プロジェクト:ゲストブック機能にテストとCI/CDを整備する
is_valid_scoreのような小さな関数からunittestで書き始めるMessageモデル・guestbookのビューに対してDjangoのテストを追加する- 高評価メッセージだけを抽出する機能をTDDのサイクルで実装する
- GitHub Actionsの設定ファイルを追加し、pushするたびにテストが自動実行される状態にする
🧠 理解度チェック(一部抜粋)
- Q. Djangoのテストが本番のデータベースを汚さないのはなぜ? → テスト実行時に専用の使い捨てデータベースが自動で用意され、終了後に破棄されるため
- Q. TDDの3ステップは? → Red(失敗するテストを書く)→Green(通す実装をする)→Refactor(整理する)
- Q. CI/CDが解決する問題は? → テストの実行忘れを防ぎ、コードの変更のたびに自動で品質を確認できるようにすること
挫折ポイントTOP3と対処法
1. 「何をテストすればいいか分からない」問題
まずは「入力に対して、期待する出力が返ってくるか」という一番シンプルな確認から始めます。完璧な網羅を最初から目指す必要はありません。
2. 「テストが書きにくいコードになっている」問題
1つの関数が多くのことをやりすぎている(画面表示とデータ保存を同時にやっている等)と、テストも書きにくくなります。役割を小さく分けることが、結果的にテストのしやすさにもつながります。
3. 「CI上だけテストが失敗する」問題
ローカルとCI環境の差異(バージョン、環境変数、タイムゾーン設定など)が原因になりがちです。エラーログを1行ずつ読み、「ローカルとの違い」を探す視点を持ちましょう。
🧓 ベテランの現場コラム 「テストを書く時間があるなら機能を1つでも多く実装したい」という気持ちは自然ですが、テストの少ないコードは後から機能を追加するたびに壊れやすくなり、結果的に開発スピードが落ちていきます。短期的なスピードと長期的なスピードは別物だと意識しておくと、テストへの投資判断がしやすくなります。
この先、何を学べばいいか
自動テストとCI/CDが整ったら、最後は 「スケーラビリティとセキュリティ」 です。アクセスが増えたときの対策と、外部からの攻撃に備える対策を学びます。
まとめ
- テストは「出荷前の品質検査」を自動化したもの
assertから始め、unittestやDjangoのTestCaseでまとめて管理する- TDD(Red-Green-Refactor)はバグ修正・新機能追加の両方に使える型
- CI/CD(GitHub Actions等)で、pushのたびに自動でテストが実行される仕組みを作る
- テストへの投資は「未来の自分・チームへの時短」につながる
参考にした学習リソースについて
本記事は、ハーバード大学が提供する人気講座「CS50’s Web Programming with Python and JavaScript」の内容を参考にしつつ、独自の題材・独自の構成・自作のサンプルで書き下ろしたオリジナル解説記事です。CS50そのものに興味を持った方は、以下の公式サイトも覗いてみてください。
- CS50 公式サイト: https://cs50.harvard.edu/x/
🎓 理解度テスト
学んだ内容を確認しましょう。全6問。