Skip to content

テストを書く意味から設計の本質へ 〜AI時代に残る“人間の役割”とは〜

Published:

はじめに:テストがつらいが、プログラミングが自動化されたあとにも残る

テストは、正直とても面倒です。

コードを書くだけなら生成AIがやってくれる時代が来るかもしれませんが、 「そのコードは本当に安全なのか?」「本番で問題を起こさないか?」という問いに答えるのは、やはり人間の責任として残り続けるでしょう。

むしろ、自動化されたコードに対して信頼を担保するテストや設計こそ、今後ますます重要になるはずです。

単体テストであれば「こんな単純な関数にテストいる?」「どうせ壊れないし…」と考えてしまいがちです。 テストを書くことに慣れてくると、ふとこんな疑問が湧いてきます。

「このコード、なぜこんなにテストしづらいんだろう…?」

その答えは、実は設計にあるのかもしれません。

「テストを書けるように設計する」こと自体が、 保守性の高いソフトウェアを作る第一歩であり、 チームにも未来の自分にも優しい開発だということです。

本記事では、

本記事では、ChatGPTとのブレストを通じて見えてきた以下のポイントを整理しながら、 **「テストしやすい設計とは何か?」**について考察していきます。


単体テストの目的:成熟度について

テストについて学んだ本『はじめて学ぶソフトウェアのテスト技法』では、テストの成熟度を5段階で説明しています。

それらを引用します。

引用元:『はじめて学ぶソフトウェアのテスト技法』リー コープランド (著)

この5段階を見ていくと、テストの目的は単なる「動作確認」にとどまらず、ソフトウェアの設計品質や保守性を高めるための“思考の道具”であることが見えてきます。

現在の生成AIは、一定の「動くコード」を生成する能力においては、すでに高い精度を誇ります。

シンタックスエラーや明らかなロジックバグといったデバッグレベルの品質は、ある程度担保されていると言ってよいでしょう。

異常値を指示することで、動かないケースも摘出可能だと思います。

しかし、いくらプロンプトを工夫しても、プロンプトに書かれていない“業務要件”や“背景知識”をくみ取ってコードを書くことは、まだ難しいのが現実です。

さらに、プロンプトにない背景を汲み取って、保守性の高いソースコードもまた、難しいと思います。

※すぐにこのあたりの思考は追いつくかもしれませんが。

だからこそ、「人間によるテストの設計と実施」こそが、AI時代において残るクリティカルな役割になります。

テストは、ただの確認作業ではなく、設計の質そのものを問い直し、ソフトウェア全体の未来を支える柱になるのです。


品質の高さとは

バグがないこと=品質ではありません。JIS規格(JIS X 25010:2013)には次のようにシステム/ソフトウェア製品品質が表現されています。

特に、インターネット経由で提供されるソフトウェアは、リリースがゴールではありません。

リリース後も改修・機能追加が続くことが前提となっています。

むしろ、機能追加のアップデートができることが品質の高いソフトウェアの要件になっています。

このように考えると、「将来的な変更に耐えられる:保守性」や、 「異なる環境でも安定して動かせる:移植性」といった特性も、品質の重要な側面になります。

これらの品質特性を支えるためには、 テストの

が鍵となります。


テストに設計の意味を込めるとは

テストが書きづらいと感じたら、それは設計が凝り固まっているサインかもしれません。

テストを通じて設計を見直すことは、実は最も自然な改善アプローチなのです。

具体的には、次のような設計を考えます。(一例であり、SOLID原則の一部)

特にDB接続やAPI連携などソフトウェアの外部との連携に注目して具体例を考えます。

1. テスト可能な設計=疎結合な設計

依存性を外から渡す(DI)、部品を入れ替えられるようにする、責務を明確に分ける。

これらはすべて「テスト可能にする」ための設計手法であり、結果的に拡張性・保守性の高いアーキテクチャにつながります。

たとえば、API呼び出しを直接行うのではなく、次のように依存性注入を行います。

import requests

def fetch_user_data(user_id):
    response = requests.get(f"https://api.example.com/users/{user_id}")
    return response.json()
def fetch_user_data(user_id, http_get=requests.get):
    response = http_get(f"https://api.example.com/users/{user_id}")
    return response.json()
from unittest.mock import MagicMock

def test_fetch_user_data():
    fake_response = MagicMock()
    fake_response.json.return_value = {"id": 1, "name": "Alice"}
    fake_http_get = MagicMock(return_value=fake_response)

    result = fetch_user_data(1, http_get=fake_http_get)

    assert result["name"] == "Alice"

2. リポジトリパターンで責務を分離する

DBアクセスを「リポジトリ」に集約することで、ビジネスロジック側はDBを意識せず、モックを使って安全にテストが書けるようになります。

具体的には、次のようなコードとなるように設計します。

def get_user_name(session, user_id):
    user = session.query(UserModel).filter_by(id=user_id).first()
    return user.name
class UserRepository:
    def __init__(self, session):
        self.session = session

    def find_by_id(self, user_id):
        return self.session.query(UserModel).filter_by(id=user_id).first()

def get_user_name(repo: UserRepository, user_id):
    user = repo.find_by_id(user_id)
    return user.name
def test_get_user_name():
    fake_repo = MagicMock()
    fake_repo.find_by_id.return_value = MagicMock(name="Alice")

    result = get_user_name(fake_repo, 1)

    assert result == "Alice"

モックによるテストと依存性注入

SQLAlchemyやRequestsなどのライブラリは、すでにテストされ尽くした信頼できる存在です。

それをモックで置き換えて、自分のロジックだけを検証する。

これは「自分の責任範囲だけをテストする」ための合理的な考え方です。

また、モックを使うことで「正しく依存先に命令を送ったか」(たとえばDB保存を呼び出したか)など、 **振る舞い(振る舞い検証)**をチェックすることも可能になります。


TDDという設計の武器

テストを書くのがつらいなら、最初に書いてしまえばいい。

TDD(テスト駆動開発)は、「仕様」と「設計」をテストという形で先に表現し、あとは通すだけ。

Red→Green→Refactor の流れは、自然と設計を改善する力を持っています。

たとえば、「この関数は整数を2倍にするべき」という仕様があれば、まず「2を渡したら4になる」というテストを書き、失敗を確認し、実装して通す。

こうした小さなサイクルを積み重ねることで、設計と実装の精度が自然に高まります。


(オプション)統合テストとの役割分担

単体テストだけでは不安?

だからこそ統合テストが存在します。依存先との整合性は、実際の接続で確かめればいい。

単体テストは「部品単体の動きを保証」し、統合テストは「部品同士の接続が正しくできているか」を確かめます。

単体はロジック、統合は接続性。この分業が開発効率を高めます。


おわりに:テストは面倒、でも未来の自分を守ってくれる

書かないよりは書いたほうがいい。

でも、どうせなら「書きやすい設計」にした方がずっと楽になる。

そう気づいた時、テストは義務ではなく、開発を快適にするツールになります。