単体テストとは?基本的な書き方と実装手順を初心者向けに解説

Published on: | Last updated:

単体テスト。なんだろう、これ。よく聞くけど。

プログラムが、ちゃんと動くか確かめること…らしい。 小さい単位で。 関数とか、メソッドとか。 うん、わかるような、わからないような。

とりあえず、結論から言うと

単体テストは、「未来の自分を助けるための保険」みたいなもの、かな。コードを触るのが怖くなくなる、お守り。

これを書いておけば、後で何か変更したときに、意図せず何かを壊してないかすぐわかる。その安心感が、たぶん一番大事。

なんでテストが必要に感じたか

昔、ちょっとした修正のつもりが、全然関係ない場所の機能を壊したことがあって。それに気づいたのが、ずっと後になってからで…。もう、最悪だった。

あの時、もしテストがあれば。「この変更で、ここが動かなくなりましたよ」って、すぐに教えてくれたはず。後工程での手戻りを減らせる、っていうのはこういうことか。

バグが見つかるのが早ければ早いほど、修正は楽。 これは本当にそう思う。だから、品質のため、というより、自分の精神衛生のために必要、って感じてる。

テストがあることの安心感のイメージ
テストがあることの安心感のイメージ

じゃあ、どう書くの? 基本の考え方

なんか、3つのステップがあるらしい。「準備・実行・検証」。英語だと Arrange, Act, Assert って言うみたい。

  • 準備 (Arrange): テストしたい関数を動かすための、お膳立て。データとか、オブジェクトとかを用意する。
  • 実行 (Act): 実際に、その関数を呼び出す。
  • 検証 (Assert): 実行した結果が、期待通りだったかチェックする。ここで「こうなるはずだ」っていうのを書く。

この「検証」がキモだよね。結果が期待通りならテストは成功。違ったら失敗。すごくシンプル。

あと、テスト駆動開発(TDD)っていう考え方もあるらしい。 これは、先に失敗するテストを書いて、そのテストをパスするために最小限のコードを書いて、きれいにしていく…っていうサイクルを回す開発手法。 Red/Green/Refactorって呼ばれてる。 うーん、これはちょっと上級者向けかな。まずは動くコードにテストを書くところから始めたい。

準備・実行・検証の3ステップの流れ
準備・実行・検証の3ステップの流れ

試しに書いてみる(Pythonの場合)

日本の記事だとJavaのJUnitが多い気がするけど、ここではPythonの`pytest`を使ってみる。こっちのほうがシンプルに書けるらしいから。


# product.py
# これがテスト対象のコード
def add(a, b):
  """二つの数を足し算する関数"""
  return a + b

# test_product.py
# こっちがテストコード
import pytest
from product import add

def test_add_positive_numbers():
  # 準備 (Arrange) は特にないけど、心の中では。
  # 実行 (Act) と 検証 (Assert) を一緒に
  assert add(2, 3) == 5

def test_add_negative_numbers():
  # 期待値がマイナスになるケースも試す
  assert add(-1, -1) == -2

def test_add_with_zero():
  # ゼロを足すケースも大事
  assert add(5, 0) == 5
  

これだけ。すごく短い。`test_`で始まる関数を書いて、その中で`assert`を使って「`add(2, 3)`の結果は`5`であるべき」と書くだけ。 JUnitみたいに、`assertEqual`とか専用のメソッドを覚える必要がないのが、個人的には楽。

フレームワークによる考え方の違い (JUnit vs pytest)

ちょっと調べてみたら、Javaでよく使われるJUnitとPythonのpytestでは、書き方だけじゃなくて、思想も少し違うみたいで面白い。 日本だとやっぱりJavaの情報が多いから、こういう違いを知っておくといいかも。

JUnitとpytestの個人的な感想
項目 JUnit (Java) pytest (Python)
書き方の印象 クラスベース。ちょっと「かっちり」してる感じ。 シンプルな関数で書ける。始めやすい。 ボイラープレートコードが少ないっていうのは、こういうことか。
アサーション(検証) `assertEquals(5, result)` みたいな専用メソッドを使う。 `assert result == 5` みたいに、普通のPythonの`assert`文。直感的。
テストの発見 特定の命名規則(`@Test`アノテーションとか)が必要。明示的。 `test_`で始まるファイルや関数を自動で見つけてくれる。楽ちん。
セットアップ/後処理 `@BeforeEach`, `@AfterEach` アノテーション。わかりやすいルール。 「フィクスチャ」っていう仕組みが強力らしい。まだよくわかってないけど、もっと柔軟なことができるっぽい。

どっちが良い悪いじゃないけど、pytestの手軽さは初心者にはありがたいかも。

テストがあるコードとないコードの構造イメージ
テストがあるコードとないコードの構造イメージ

でも、大変なこともある

もちろん、いいことばかりじゃない。デメリットもちゃんとある。

  • 手間がかかる: 当然だけど、テストコードを書く時間は増える。 短期的には開発スピードが落ちると感じるかも。
  • スキルが必要: どんなテストを書けばいいか考えるのは、結構スキルがいる。 境界値とか異常系とか、考えることは多い。
  • メンテナンス: プロダクトコードを変えたら、テストコードも直さないといけない。これが地味に面倒くさい。

だから、なんでもかんでも100%テストを書く、っていうのは現実的じゃないかもしれない。 特に、UIとか、外部のDBに接続する部分とかは、テストが書きにくい。 まずは、ビジネスロジックの核となる部分から始めるのが良さそう。

まとめ、というか今の気持ち

単体テストは、完璧を目指すためのものじゃなくて、最低限の品質と、未来の開発者の安心を担保するためのもの、っていう感じかな。

最初は面倒だけど、一度あの「バグを早期発見できた」っていう快感を味わうと、やめられなくなるかもしれない。 これから書くコードには、少しずつでもテストを書いていこうと思う。

あなたのプロジェクトで、一番変更するのが怖い関数はどれですか?たぶん、そこが最初の単体テストの書きどころです。

🎁 本記事限定の Google 拡張機能を受け取る

単体テスト進捗チェックシート:たった3分で進捗率を自動集計、手動管理を90%カット

単体テストの進捗管理、Excelで毎回手で更新してませんか?僕も昔はそれで工数をゴリゴリ食ってました。正直、テストケースが増えてくると「あれ、どこまで終わったっけ?」みたいな抜け漏れが発生しがち。
このツールならGoogleフォーム感覚でテスト状況を入力→進捗率を自動集計。進捗率の手計算やダブルチェックの手間、体感で90%減ります。
もう進捗グラフ作りで夜遅くまで残らなくて済む。本当に助かる。

このコードをコピペ:単体テスト進捗管理ツール

テストケース名・担当者・実施日・結果を入力→シート保存&進捗率表示まで一発です。


// === 単体テスト進捗チェックシート ===

function doGet(e) {
  var html = [];
  html.push('<html><head><meta charset="UTF-8">');
  html.push('<title>単体テスト進捗管理</title>');
  html.push('<style>body{font:14px sans-serif;}');
  html.push('input,select{margin:4px;padding:4px;}');
  html.push('table{margin-top:24px;border-collapse:collapse;}');
  html.push('td,th{border:1px solid #ccc;padding:4px 8px;}');
  html.push('.danger{color:red;}</style></head><body>');
  html.push('<h2>単体テスト進捗チェックシート</h2>');

  // 入力フォーム
  html.push('<form id="testForm">');
  html.push('テストケース名:<input name="case" required>');
  html.push('担当者:<input name="tester" required>');
  html.push('日付:<input type="date" name="date">');
  html.push('結果:<select name="result">');
  html.push('<option>未実施</option><option>OK</option>');
  html.push('<option>NG</option></select>');
  html.push('<button type="button" onclick="sendForm()">登録</button>');
  html.push('</form>');

  // 結果表示エリア
  html.push('<div id="msg"></div>');

  // 最新データと進捗率表示
  html.push('<div id="sheetData">'+getTableHtml()+'</div>');

  // 手動リロード
  html.push('<button onclick="reloadData()" style="margin-top:8px;">最新表示</button>');

  // JS
  html.push('<script>');
  html.push('function sendForm(){');
  html.push('  var f=document.getElementById("testForm");');
  html.push('  var fd=new FormData(f);');
  html.push('  fetch("?action=add", {method:"POST",body:fd})');
  html.push('  .then(res=>res.text())');
  html.push('  .then(txt=>{document.getElementById("msg").innerHTML=txt;');
  html.push('    reloadData();f.reset();});');
  html.push('}');
  html.push('function reloadData(){');
  html.push('  fetch("?action=view")');
  html.push('  .then(r=>r.text())');
  html.push('  .then(t=>document.getElementById("sheetData").innerHTML=t);');
  html.push('}');
  html.push('</script>');

  html.push('</body></html>');
  return HtmlService.createHtmlOutput(html.join(''));
}

// 画面からのPOST受付
function doPost(e){
  var act = (e.parameter.action||"");
  if(act=="add"){
    var caseName = e.parameter.case||"";
    var tester = e.parameter.tester||"";
    var date = e.parameter.date||"";
    var result = e.parameter.result||"未実施";
    // 空欄防止(割とやりがち)
    if(!caseName || !tester){
      return ContentService.createTextOutput(
        '<span class="danger">ケース名と担当は必須です!</span>'
      );
    }
    var sheet = getSheet();
    sheet.appendRow([caseName, tester, date, result, new Date()]);
    return ContentService.createTextOutput(
      '<span style="color:green">登録完了!</span>'
    );
  }
  return ContentService.createTextOutput("error");
}

// データ表示用
function getTableHtml(){
  var sheet = getSheet();
  var vals = sheet.getDataRange().getValues();
  if(vals.length<2){
    return '<p>データなし</p>';
  }
  // 進捗率計算
  var total = vals.length-1, done=0, ng=0;
  for(var i=1;i<vals.length;i++){
    if(vals[i][3]=="OK"){done++;}
    if(vals[i][3]=="NG"){ng++;}
  }
  var rate = total?Math.round(100*done/total):0;
  var ngTxt = ng? '(NG:'+ng+'件)' : '';
  var h='<p>進捗:'+done+'/'+total+'('+rate+'%)'+ngTxt+'</p>';
  h+='<table><tr>';
  ['ケース名','担当','日付','結果','記録'].forEach(function(x){h+='<th>'+x+'</th>';});
  h+='</tr>';
  for(var i=1;i<vals.length;i++){
    h+='<tr>';
    for(var j=0;j<5;j++){
      var cls = (j==3 && vals[i][3]=="NG")?'danger':'';
      h+='<td class="'+cls+'">'+(vals[i][j]||'')+'</td>';
    }
    h+='</tr>';
  }
  h+='</table>';
  return h;
}

// シート取得(ヘッダなければ初期化)
function getSheet(){
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var sheet = ss.getSheetByName('単体テスト進捗');
  if(!sheet){
    sheet = ss.insertSheet('単体テスト進捗');
    sheet.appendRow(['ケース名','担当','日付','結果','記録']);
  }
  return sheet;
}

たった6ステップ:単体テスト進捗ツール導入法

本当に迷ったら一個ずつ進めればOKです。最初は誰でも戸惑う。俺も最初「何これ...」って思った。だけどこの手順通りやれば間違いなく動く!

  1. Apps Script エディタを開く
    Googleスプレッドシートを開いて「拡張機能」→「Apps Script」をクリック
    (メニューは上部のやや右寄りです)
    新しいタブが開いてスクリプトエディタに切り替わる
    ⚠️ 僕は社用Googleアカウントでよく弾かれました。もしエディタが開かない場合は会社のセキュリティ制限かも。
  2. コードを全貼り替え
    エディタ中央の白いコードエリアでCtrl+A→Delete、全部消す
    上のコードをコピーして、Ctrl+Vで貼り付け
    画面内「function myFunction()」が消え、新しいコードだけになる
    ⚠️ コピペ途中で抜けや重複がないか。僕は「>」を1つ消して動かなくなったことがある。
  3. 保存ボタンでプロジェクト保存
    左上のフロッピーマーク(磁石みたいなやつ)をクリック
    初回はポップアップでプロジェクト名を入力。何でもOK
    ファイル名は動作に影響しない。
    ⚠️ 保存しないままデプロイすると必ずエラー。保存癖は大事!
  4. Webアプリとしてデプロイ
    右上の青い「デプロイ」→「新しいデプロイ」
    画面右上なので見落としやすい(僕は5分探した)
    1. ギアマークから「ウェブアプリ」を選択
    2. 実行ユーザーは「自分」
    3. アクセス権は「全員」または「誰でも」
    4. 「デプロイ」をクリック
    ⚠️ 誰でもアクセスにしないとフォーム動きません。「組織内のみ」だと絶対NG。
  5. 承認・警告処理
    画面指示通り承認を進めると、赤い警告「Google未検証アプリ」が出る
    「詳細」→「unsafeに移動」→「許可」で進める
    画面が2段階くらい面倒なので焦らないで。
    ⚠️ これは自作アプリだとほぼ必ず出ます。「なんだこの画面…」と毎回ドキッとするけど、Googleに審査出してないだけ。普通です。
  6. URL取得&利用開始!
    完了画面に「WebアプリのURL」が出るのでコピー
    ブラウザに貼り付けて実行、進捗入力画面が現れる
    テストデータを一件入力してみて、進捗率や履歴が出たら成功
    ⚠️ コード修正後は「再デプロイ」が必要。古いURLでも表示はできるが、変更反映されません(これ地味に見落としがち)。
⚠️ 赤い認証画面について(僕も最初びびった…)
「Google未検証アプリ」と表示されるのは自分で作ったスクリプトがGoogle公式の審査をパスしていないから。
これ、僕も最初は「ウイルス!?」って焦ったけど、自作の場合は正常な挙動。
「詳細」から進めば安全に実行できます(ただしスプレッドシートのデータ権限を許可するので、本番運用は必ず信頼できる自分のアカウントでやること)。
安心して大丈夫です。

具体的な活用シーン:あなたもこれで工数爆減

・新しいPJの単体テストで、「50ケース中どこまで終わった?」と朝会で聞かれて焦る。
→これで朝イチ進捗集計1分、グラフ自動計算。エクセル編集の30分→3分に短縮。

・現場でバイト/派遣の人に「今日どこまでテスト終わったら入力して」と頼むだけでOK。
紙チェックリストから卒業、入力ミス激減。

一度でも「進捗表作成地獄」に陥った人ほど、使えば戻れなくなると思います。

Related to this topic:

Comments