Day 88:テスト・修正 ― 「コードに“安心”を宿す仕上げの」
Day 88は、最終プロジェクトを 一段階“安心できる状態”に引き上げる日 です。
今日のキーワードはこの4つ。
- 正常系テスト
- 異常系テスト
- バグ修正
- リファクタリング
ここまでで、売上管理・分析システムは 「動く」状態にはなってきました。
でも、“動く” と “安心して使える” の間には、けっこう大きな差 があります。 その差を埋めてくれるのが、まさに今日のテーマである テストと修正、そしてリファクタリング です。
テストを書く意味 ― 「壊れていないことを、コードで証明する」
なぜテストを書くのか
テストは、
「ちゃんと動いているかどうかを、毎回手で確認する代わりに、コードにやってもらう仕組み」
です。
- 機能を追加したとき
- バグを直したとき
- リファクタリングしたとき
そのたびに、 「前と同じように動いているかな…?」 と不安になるのは、 開発者あるあるです。
テストコードがあると、
- コマンド一発で「壊れていないか」を確認できる
- 直したつもりが別のところを壊していないか分かる
- 自分のコードに対する信頼感が増す
という、とても大きな安心感が得られます。
正常系テスト ― 「期待どおりに動くことを確認する」
ここでは、pytestを使って、 Day 86〜87で作った機能の 正常系テスト を書いていきます。
例:売上データのバリデーション(正常系)
対象関数はこれです(おさらい)。
# services/sales_processing.py
from typing import Any
from models.sale import Sale
def validate_and_convert(rows: list[dict[str, Any]]) -> list[Sale]:
"""
CSVから読み込んだ行データをチェックし、
Saleモデルのリストに変換します。
"""
sales: list[Sale] = []
for row in rows:
if not row.get("date"):
raise ValueError("date が空です")
if not row.get("item"):
raise ValueError("item が空です")
if not row.get("amount"):
raise ValueError("amount が空です")
try:
amount_int = int(row["amount"])
except ValueError:
raise ValueError(f"amount が整数ではありません: {row['amount']}")
sale = Sale(
id=None,
date=row["date"],
item=row["item"],
amount=amount_int,
customer_id=None,
)
sales.append(sale)
return sales
Python正常系テストを書く
# tests/test_sales_processing.py
import pytest
from services.sales_processing import validate_and_convert
from models.sale import Sale
def test_validate_and_convert_normal():
"""
正常系テスト:
正しい形式の行データを渡したとき、
Saleのリストが正しく返ってくることを確認する。
"""
rows = [
{"date": "2024-01-01", "item": "ノート", "amount": "1000"},
{"date": "2024-01-02", "item": "ペン", "amount": "500"},
]
sales = validate_and_convert(rows)
# 戻り値の件数が一致しているか
assert len(sales) == 2
# 型が Sale になっているか
assert isinstance(sales[0], Sale)
# 値が正しく変換されているか
assert sales[0].date == "2024-01-01"
assert sales[0].item == "ノート"
assert sales[0].amount == 1000 # 文字列から整数に変換されている
Pythonポイント:
- 「何を確認したいか」を日本語でコメントに書いてから、assertを書く
- 件数・型・値の3つを確認すると、安心感がぐっと増す
- 正常系テストは「期待どおりに動くか」を確認するもの
異常系テスト ― 「壊れた入力に対して、ちゃんと“怒ってくれる”かを見る」
正常系だけでは、 「壊れたときにどう振る舞うか」 が分かりません。
そこで、異常系テストを追加します。
例:必須項目が空の場合のテスト
def test_validate_and_convert_missing_amount():
"""
異常系テスト:
amount が空の行が含まれている場合、
ValueError が発生することを確認する。
"""
rows = [
{"date": "2024-01-01", "item": "ノート", "amount": ""},
]
with pytest.raises(ValueError) as exc_info:
validate_and_convert(rows)
# エラーメッセージの一部を確認してもよい
assert "amount が空です" in str(exc_info.value)
Python例:整数に変換できない場合のテスト
def test_validate_and_convert_invalid_amount():
"""
異常系テスト:
amount が整数に変換できない場合、
ValueError が発生することを確認する。
"""
rows = [
{"date": "2024-01-01", "item": "ノート", "amount": "abc"},
]
with pytest.raises(ValueError) as exc_info:
validate_and_convert(rows)
assert "amount が整数ではありません" in str(exc_info.value)
Pythonポイント:
pytest.raisesを使うと、「この処理は例外を出すべきだ」をテストできる- 異常系テストは、「壊れた入力に対して、ちゃんと防御できているか」を確認するもの
- セキュリティの観点でも、異常系テストはとても重要
バグ修正 ― 「テストで見つかった“ほころび”を丁寧に縫い直す」
テストを書いて実行すると、 ときどき 「あれ、思っていた動きと違う…」 という瞬間が訪れます。
それが、バグ発見のタイミングです。
例:空リストのときの集計バグ
以前書いた集計関数を思い出してみましょう。
# services/aggregation_service.py
import sqlite3
DB_PATH = "sales.db"
def get_total_sales() -> int:
"""
売上金額の合計を返します。
"""
conn = sqlite3.connect(DB_PATH)
cur = conn.cursor()
cur.execute("SELECT SUM(amount) FROM sales")
row = cur.fetchone()
conn.close()
return row[0] if row[0] is not None else 0
Pythonこれに対して、 「テーブルが空のときもちゃんと0を返すか?」というテストを書いてみます。
# tests/test_aggregation_service.py
import sqlite3
import pytest
from services.aggregation_service import get_total_sales
DB_PATH = "sales.db"
def setup_empty_db():
"""
テスト用に空の sales テーブルを用意する簡易セットアップ。
本番では別のテスト用DBを使うのが理想。
"""
conn = sqlite3.connect(DB_PATH)
cur = conn.cursor()
cur.execute("DELETE FROM sales")
conn.commit()
conn.close()
def test_get_total_sales_empty_table():
"""
sales テーブルが空の場合でも、
get_total_sales が 0 を返すことを確認する。
"""
setup_empty_db()
total = get_total_sales()
assert total == 0
Pythonもしここで、
rowがNoneになってしまうrow[0]でエラーになる
といったバグが見つかったら、 それを修正していきます。
修正例:fetchoneの戻り値を安全に扱う
def get_total_sales() -> int:
"""
売上金額の合計を返します。
"""
conn = sqlite3.connect(DB_PATH)
cur = conn.cursor()
cur.execute("SELECT SUM(amount) FROM sales")
row = cur.fetchone()
conn.close()
# row 自体が None の可能性も考慮する
if row is None or row[0] is None:
return 0
return row[0]
Pythonポイント:
- バグ修正は「テスト → 原因調査 → 修正 → 再テスト」の流れで行う
- 「あり得る最悪のケース」を想像して、コードを安全側に寄せる
- 修正したら、必ずテストをもう一度回す
リファクタリング ― 「動きは変えずに、コードを“読みやすく・育てやすく”する」
リファクタリングは、
「外から見た動きは変えずに、中身のコードをきれいにすること」
です。
テストがあると、
- リファクタリングしても、動きが変わっていないか確認できる
- 「安心してコードを触れる」状態になる
という、とても大きなメリットがあります。
例:重複したエラーメッセージ処理をまとめる
先ほどの validate_and_convert は、 少しだけ重複がありました。
if not row.get("date"):
raise ValueError("date が空です")
if not row.get("item"):
raise ValueError("item が空です")
if not row.get("amount"):
raise ValueError("amount が空です")
Pythonこれを、 「必須項目のチェック関数」 に切り出してみます。
# services/validation_utils.py
def require_field(row: dict[str, object], key: str) -> None:
"""
row の中で、指定した key が空でないことを確認します。
空の場合は ValueError を投げます。
"""
if not row.get(key):
raise ValueError(f"{key} が空です")
Pythonそして、validate_and_convert をリファクタリングします。
# services/sales_processing.py
from services.validation_utils import require_field
def validate_and_convert(rows: list[dict[str, Any]]) -> list[Sale]:
sales: list[Sale] = []
for row in rows:
# 必須項目チェックを共通関数で行う
require_field(row, "date")
require_field(row, "item")
require_field(row, "amount")
try:
amount_int = int(row["amount"])
except ValueError:
raise ValueError(f"amount が整数ではありません: {row['amount']}")
sale = Sale(
id=None,
date=row["date"],
item=row["item"],
amount=amount_int,
customer_id=None,
)
sales.append(sale)
return sales
Pythonポイント:
- リファクタリングは「読みやすさ」「再利用性」「変更のしやすさ」を高めるために行う
- 動きが変わっていないことを、テストで確認する
- 小さなリファクタリングを積み重ねることで、コードベースが“育っていく”
Day 88ミニテンプレート ― テスト・修正・リファクタリングの基本セット
正常系テスト
def test_validate_and_convert_normal():
rows = [
{"date": "2024-01-01", "item": "ノート", "amount": "1000"},
]
sales = validate_and_convert(rows)
assert len(sales) == 1
assert sales[0].amount == 1000
Python異常系テスト
def test_validate_and_convert_invalid_amount():
rows = [
{"date": "2024-01-01", "item": "ノート", "amount": "abc"},
]
with pytest.raises(ValueError):
validate_and_convert(rows)
Pythonバグ修正の流れ
- テストを書く(または既存テストを実行する)
- 失敗したテストを確認する
- 原因をコードから探す
- 修正する
- テストを再実行して、直ったことを確認する
リファクタリングの基本ルール
- 先にテストを書く(または既存テストを整える)
- 小さな変更を少しずつ行う
- 変更のたびにテストを回す
- 「動きが変わっていないこと」を確認しながら進める
Day 88のまとめ ― 「テストは、“自分のコードを信じるための道具”」
今日のテーマは、
- 正常系テスト
- 異常系テスト
- バグ修正
- リファクタリング
でした。
テストを書くことは、
「自分のコードを疑うこと」ではなく、 「自分のコードを信じるための準備をすること」
だと思っています。
- 壊れていないことを、コードで証明する
- 壊れたときに、どこが悪いかすぐ分かる
- 直したあとも、前と同じように動いているか確認できる
その積み重ねが、 最終プロジェクトを 「ただの練習」から「誇れる作品」 に変えていきます。
Day 88を終えた今、 みなさんのプロジェクトには、 もうすでに「安心して触れるための土台」ができ始めています。

