100本ノック / システム開発 / システム開発100本ノック
製造業システムのテスト設計入門|単体・API・E2E・ログを実務で学ぶ10本ノック
工場業務システムの停止と品質流出を防ぐ — テスト・品質管理実践10本ノック
製造実行・品質管理システムを題材に、テストの種類、バックエンド・API・画面・E2Eテスト、型チェック、リンター、フォーマッター、ログ設計、例外調査を一続きで整理します。目的はテスト件数を増やすことではなく、品質・納期・設備稼働に影響する不具合を早く見つけ、発生時には短時間で原因へ到達できる仕組みを作ることです。
外部データは使わず、Pythonで生成した架空のテスト実行・静的解析・障害ログを用いて、検出率、実行時間、変更失敗率、平均復旧時間(MTTR)などを表とグラフで確認します。
[!NOTE] 本資料は、数理工房 (もしくは代表である和山個人) が過去に企業研修において使用した notebook を企業様の許可を得て再構成・編集のうえ公開しています。
掲載データはすべて架空のものであり、実在する企業・工場・数値とは一切関係ありません。
はじめに:この記事で扱う製造業の実務課題
工場の業務システムは、作業指示、実績収集、品質判定、在庫引当、設備連携などを支えます。不具合は「画面が崩れる」だけでなく、誤った指示、検査漏れ、二重登録、トレーサビリティ欠落へ波及します。一方、すべてを同じ深さで試験するとリリースが遅れ、現場改善の速度を落とします。
今回の課題は、次の3点です。
- 変更の影響と業務リスクに合わせてテストを配分する
- 型・規約・書式の確認を自動化し、人は業務仕様のレビューへ集中する
- 本番例外を、製造指示や設備を特定できるログから迅速に調査する
現場でよくある状況
- 正常系の手動確認に偏り、境界値や権限違いを見落とす
- API単体では成功するが、画面からの一連操作で二重登録が起きる
- テストが遅く不安定で、変更時に実行されなくなる
- コードレビューが空白や命名の指摘に費やされ、業務ロジックの議論が薄くなる
- 障害ログに「エラー」の一行しかなく、対象工場・指示・処理を追えない
これらは個人の注意力だけでは解消できません。検出を開発工程へ組み込み、失敗時に診断可能な情報を残す必要があります。
なぜこの問題は判断が難しいのか
品質施策にはトレードオフがあります。E2Eテストは利用者操作を広く確認できますが、遅く壊れやすい傾向があります。詳細ログは調査を助けますが、個人情報・機密情報や保存費用の問題を生みます。カバレッジを上げても、重要な業務ルールを検証していなければ安全とは言えません。
| 判断対象 | 技術指標 | 製造業務への翻訳 |
|---|---|---|
| テスト構成 | 検出率、実行時間、不安定率 | 品質流出リスク、リリース待ち時間 |
| 静的解析 | 指摘件数、修正時間 | 手戻り、レビュー負荷 |
| ログ | 相関ID付与率、欠損率 | 影響範囲特定時間、MTTR |
| 例外対応 | 検知時間、復旧時間 | 生産停止時間、代替運用時間 |
したがって「テスト数」だけでなく、重要障害の流出率、変更失敗率、復旧時間まで追います。
今回扱うノックの全体像
| No. | テーマ | 実務上の確認点 |
|---|---|---|
| 061 | テストの種類 | リスクに応じたテスト配分 |
| 062 | バックエンド単体テスト | 品質判定ロジックの境界値 |
| 063 | APIテスト | 契約・認証・重複登録 |
| 064 | コンポーネントテスト | 画面状態と操作結果 |
| 065 | E2Eテスト | 重要業務フローの完遂 |
| 066 | 型チェック | データ構造の不整合検知 |
| 067 | リンター | 不具合につながる記述の検知 |
| 068 | フォーマッター | 書式統一とレビュー集中 |
| 069 | ログ設計 | 相関IDと構造化ログ |
| 070 | 例外調査 | 検知から復旧・再発防止まで |
Python環境の準備
numpyとpandasで架空データを生成・集計し、matplotlibで可視化します。日本語表示にはjapanize_matplotlibを使います。乱数seedを固定するため、再実行しても同じ結果になります。
%matplotlib inline
%config InlineBackend.figure_format = 'svg'
import sys
import numpy as np
import pandas as pd
import matplotlib
import matplotlib.pyplot as plt
import japanize_matplotlib
from IPython.display import display
SEED = 20260712
rng = np.random.default_rng(SEED)
pd.options.display.max_columns = 20
print(f"Python : {sys.version.split()[0]}")
print(f"numpy : {np.__version__}")
print(f"pandas : {pd.__version__}")
print(f"matplotlib : {matplotlib.__version__}")
print(f"random seed: {SEED}")
Python : 3.11.9
numpy : 1.26.4
pandas : 2.2.2
matplotlib : 3.9.2
random seed: 20260712
架空データの作成
24週間のリリース、1,200件の潜在不具合、900回のAPI呼び出し、500ファイルの静的解析、180件の本番例外を生成します。対象は架空の製造実行・品質管理システムです。重大度が高い不具合ほど業務影響が大きく、テスト層ごとに検出確率と実行時間が異なるよう設定します。
ここでの数値は教育用シミュレーションであり、一般的な効果を保証するものではありません。実務では自社の障害票、CI履歴、監視データで確率と費用を更新します。
n_defects = 1200
defects = pd.DataFrame({
"defect_id": [f"D-{i:04d}" for i in range(1, n_defects + 1)],
"severity": rng.choice(["重大", "高", "中", "低"], n_defects, p=[0.08, 0.22, 0.45, 0.25]),
"area": rng.choice(["品質判定", "作業指示", "実績登録", "在庫連携", "設備連携"], n_defects),
})
test_layers = pd.DataFrame({
"layer": ["単体", "API", "コンポーネント", "E2E"],
"detection_prob": [0.56, 0.48, 0.34, 0.27],
"minutes_per_run": [4, 11, 16, 38],
"flaky_rate_pct": [0.2, 0.5, 1.3, 3.8],
})
n_api = 900
api_calls = pd.DataFrame({
"endpoint": rng.choice(["GET /orders", "POST /results", "POST /quality-decisions", "GET /inventory"], n_api),
"status": rng.choice([200, 201, 400, 401, 409, 500], n_api, p=[.52, .25, .08, .04, .06, .05]),
"latency_ms": np.clip(rng.lognormal(np.log(310), .65, n_api), 40, 5000).round(),
})
n_files = 500
static_files = pd.DataFrame({
"module": rng.choice(["quality", "orders", "results", "inventory", "equipment"], n_files),
"loc": rng.integers(60, 900, n_files),
"type_errors": rng.poisson(0.42, n_files),
"lint_errors": rng.poisson(0.66, n_files),
"format_changes": rng.poisson(1.1, n_files),
})
n_incidents = 180
incidents = pd.DataFrame({
"incident_id": [f"INC-{i:04d}" for i in range(1, n_incidents + 1)],
"severity": rng.choice(["重大", "高", "中"], n_incidents, p=[.16, .39, .45]),
"correlation_id": rng.random(n_incidents) < .78,
"order_id_logged": rng.random(n_incidents) < .70,
"stacktrace_logged": rng.random(n_incidents) < .83,
})
base = rng.lognormal(np.log(95), .55, n_incidents)
completeness = incidents[["correlation_id", "order_id_logged", "stacktrace_logged"]].sum(axis=1)
incidents["mttr_min"] = np.clip(base * (1.55 - .20 * completeness), 12, 420).round()
print(f"潜在不具合: {len(defects):,}件 / API呼び出し: {len(api_calls):,}件")
print(f"解析対象: {len(static_files):,}ファイル / 本番例外: {len(incidents):,}件")
display(test_layers)
潜在不具合: 1,200件 / API呼び出し: 900件
解析対象: 500ファイル / 本番例外: 180件
| layer | detection_prob | minutes_per_run | flaky_rate_pct | |
|---|---|---|---|---|
| 0 | 単体 | 0.56 | 4 | 0.2 |
| 1 | API | 0.48 | 11 | 0.5 |
| 2 | コンポーネント | 0.34 | 16 | 1.3 |
| 3 | E2E | 0.27 | 38 | 3.8 |
No.061:テストの種類を整理する
実務での意味
単体、API、コンポーネント、E2Eは競合する選択肢ではなく、異なる不具合を異なる速さで検出する層です。品質判定式は単体テスト、入出力契約はAPIテスト、警告表示はコンポーネントテスト、検査登録から承認までの重要フローはE2Eで確認します。
分析・モデル化の考え方
各層を独立に実行したと仮定し、検出確率 から累積検出率 を試算します。実行時間と不安定率も併記し、速い下層を厚く、E2Eは停止・品質流出に直結する経路へ絞ります。カバレッジ率ではなく、業務リスクに対する検出力を評価します。
Pythonで確認する
layers = test_layers.copy()
layers["expected_detected"] = (n_defects * layers["detection_prob"]).round().astype(int)
layers["detected_per_minute"] = (layers["expected_detected"] / layers["minutes_per_run"]).round(1)
layers["cumulative_detection_pct"] = (1 - (1 - layers["detection_prob"]).cumprod()) * 100
display(layers)
fig, ax = plt.subplots(figsize=(8, 4))
ax.bar(layers["layer"], layers["cumulative_detection_pct"], color="#4472C4")
ax.set_title("テスト層を積み上げた場合の累積検出率(試算)")
ax.set_xlabel("追加したテスト層")
ax.set_ylabel("累積検出率(%)")
ax.grid(axis="y", alpha=.3)
plt.tight_layout()
plt.show()
| layer | detection_prob | minutes_per_run | flaky_rate_pct | expected_detected | detected_per_minute | cumulative_detection_pct | |
|---|---|---|---|---|---|---|---|
| 0 | 単体 | 0.56 | 4 | 0.2 | 672 | 168.0 | 56.000000 |
| 1 | API | 0.48 | 11 | 0.5 | 576 | 52.4 | 77.120000 |
| 2 | コンポーネント | 0.34 | 16 | 1.3 | 408 | 25.5 | 84.899200 |
| 3 | E2E | 0.27 | 38 | 3.8 | 324 | 8.5 | 88.976416 |
結果の読み取り
下層テストは短時間で多くの不具合を検出でき、層を重ねるほど取りこぼしを減らせます。ただし独立性を置いた仮定値なので、割合そのものより、検出対象の重複と抜けをレビューする材料にします。重大障害ごとに『どの層で防ぐか』を対応づけることが実務上の次の一歩です。
No.062:バックエンドの単体テストを書く
実務での意味
品質判定、数量整合、在庫引当などの純粋な業務ロジックは、DBやネットワークから切り離すと高速に多数の境界値を確認できます。特に規格下限・上限ちょうどの扱いは、品質流出や過剰廃棄へ直結します。
分析・モデル化の考え方
測定値 を規格下限 、上限 と比較し、 を合格とする関数を検証します。同値クラスだけでなく、境界の直前・一致・直後を用意します。入力欠損や単位違いも別のテストとして扱います。
def judge_quality(value, lsl, usl):
if value is None: raise ValueError("測定値が必要です")
return "合格" if lsl <= value <= usl else "不合格"
Pythonで確認する
def judge_quality(value, lsl=9.5, usl=10.5):
if value is None:
raise ValueError("測定値が必要です")
return "合格" if lsl <= value <= usl else "不合格"
cases = pd.DataFrame({
"case": ["下限直前", "下限一致", "中心", "上限一致", "上限直後"],
"value": [9.49, 9.50, 10.00, 10.50, 10.51],
"expected": ["不合格", "合格", "合格", "合格", "不合格"],
})
cases["actual"] = cases["value"].map(judge_quality)
cases["passed"] = cases["expected"].eq(cases["actual"])
display(cases)
print(f"成功: {cases['passed'].sum()}/{len(cases)}件")
| case | value | expected | actual | passed | |
|---|---|---|---|---|---|
| 0 | 下限直前 | 9.49 | 不合格 | 不合格 | True |
| 1 | 下限一致 | 9.50 | 合格 | 合格 | True |
| 2 | 中心 | 10.00 | 合格 | 合格 | True |
| 3 | 上限一致 | 10.50 | 合格 | 合格 | True |
| 4 | 上限直後 | 10.51 | 不合格 | 不合格 | True |
成功: 5/5件
結果の読み取り
5つの境界ケースが期待どおりなら、少なくとも規格端の包含関係は固定できます。実務では規格値をコードへ埋め込まず、品番・工程・適用開始日と結びつけ、規格改訂前後もテストします。合格件数だけでなく、過去に流出した不具合を回帰テストへ加える運用が重要です。
No.063:APIテストを書く
実務での意味
APIテストは、画面や外部システムとの約束であるステータスコード、レスポンス型、認証、入力検証、重複防止を確認します。実績登録APIのタイムアウト後に再送して二重計上される問題は、生産数・在庫・原価を同時に狂わせます。
分析・モデル化の考え方
エンドポイント別に成功率、クライアントエラー、サーバーエラー、応答時間の95パーセンタイルを集計します。登録系では同じ冪等性キーの再送が同じ結果を返すことを検証します。 は全呼び出しの95%が収まる待ち時間です。
def test_duplicate_request(client):
headers = {"Idempotency-Key": "result-001"}
first = client.post("/results", json=payload, headers=headers)
second = client.post("/results", json=payload, headers=headers)
assert first.json()["id"] == second.json()["id"]
Pythonで確認する
api_kpi = api_calls.groupby("endpoint").agg(
calls=("status", "size"),
success_rate_pct=("status", lambda s: s.between(200, 299).mean() * 100),
conflict_409=("status", lambda s: s.eq(409).sum()),
server_error_5xx=("status", lambda s: s.ge(500).sum()),
p95_latency_ms=("latency_ms", lambda s: s.quantile(.95)),
).round(1)
display(api_kpi)
fig, ax = plt.subplots(figsize=(9, 4))
api_kpi["p95_latency_ms"].sort_values().plot.barh(ax=ax, color="#70AD47")
ax.set_title("API別の95パーセンタイル応答時間")
ax.set_xlabel("応答時間(ms)")
ax.set_ylabel("エンドポイント")
ax.grid(axis="x", alpha=.3)
plt.tight_layout()
plt.show()
| calls | success_rate_pct | conflict_409 | server_error_5xx | p95_latency_ms | |
|---|---|---|---|---|---|
| endpoint | |||||
| GET /inventory | 228 | 75.0 | 11 | 11 | 930.9 |
| GET /orders | 209 | 78.0 | 13 | 13 | 864.8 |
| POST /quality-decisions | 208 | 75.5 | 6 | 15 | 858.7 |
| POST /results | 255 | 76.5 | 18 | 8 | 852.3 |
結果の読み取り
エンドポイントごとに成功率と裾の遅延を分けると、平均値では見えない利用者の待ちを発見できます。409は仕様どおりの重複防止か、単なる競合障害かをレスポンスコードだけで判断せず、エラーコードと監査ログまで確認します。テスト環境ではDB更新件数も検証します。
No.064:フロントエンドのコンポーネントテストを書く
実務での意味
製造画面では、異常値を受け取ったときに警告が表示され、承認ボタンが無効化されるかが重要です。DOM構造の細部ではなく、利用者が見える文言・役割と操作結果を検証すると、内部実装を変えても価値のあるテストが残ります。
分析・モデル化の考え方
画面状態を正常・注意・異常・通信中・通信失敗に分け、期待する表示と操作可否を表にします。状態遷移表は、条件の組合せ漏れを可視化する簡潔なモデルです。スナップショットだけに頼らず、クリック後のAPI呼び出しやメッセージまで確認します。
render(<QualityCard value={10.8} status="loaded" />);
expect(screen.getByRole("alert")).toHaveTextContent("規格外");
expect(screen.getByRole("button", {name: "承認"})).toBeDisabled();
Pythonで確認する
ui_states = pd.DataFrame({
"state": ["正常", "注意", "異常", "通信中", "通信失敗"],
"alert_visible": [False, True, True, False, True],
"approve_enabled": [True, True, False, False, False],
"retry_visible": [False, False, False, False, True],
"test_cases": [4, 6, 8, 3, 5],
})
display(ui_states)
fig, ax = plt.subplots(figsize=(8, 4))
ax.bar(ui_states["state"], ui_states["test_cases"], color="#ED7D31")
ax.set_title("画面状態別のコンポーネントテスト件数")
ax.set_xlabel("画面状態")
ax.set_ylabel("テストケース数")
ax.grid(axis="y", alpha=.3)
plt.tight_layout()
plt.show()
| state | alert_visible | approve_enabled | retry_visible | test_cases | |
|---|---|---|---|---|---|
| 0 | 正常 | False | True | False | 4 |
| 1 | 注意 | True | True | False | 6 |
| 2 | 異常 | True | False | False | 8 |
| 3 | 通信中 | False | False | False | 3 |
| 4 | 通信失敗 | True | False | True | 5 |
結果の読み取り
異常状態は表示と操作制限の組合せが多いため、正常系より厚く確認します。件数の多さが目的ではなく、『異常なのに承認できる』『失敗時に再試行できない』といった業務上危険な状態を防ぐことが目的です。文言変更に弱いテストは、アクセシブルなroleや業務上安定したラベルを使います。
No.065:E2Eテストの考え方を理解する
実務での意味
E2Eテストは、ログインから作業指示選択、検査値入力、品質承認、履歴確認までを利用者と同じ経路で通します。システム境界をまたぐ不整合を検出できますが、実行時間と保守費用が大きいため、停止・品質・法令に関わる重要フローへ集中させます。
分析・モデル化の考え方
各シナリオの事業影響を、発生確率 、影響度 、検出困難度 の積 で相対評価します。厳密な損失額ではなく、E2E自動化の優先順位を合意するための指標です。
Pythonで確認する
e2e = pd.DataFrame({
"scenario": ["検査登録→品質承認", "作業指示→実績確定", "設備停止→保全通知", "在庫照会", "帳票ダウンロード", "表示設定変更"],
"probability": [4, 5, 3, 4, 3, 2],
"impact": [5, 5, 5, 4, 3, 1],
"detectability": [4, 3, 5, 2, 2, 1],
"runtime_min": [9, 8, 11, 5, 7, 3],
})
e2e["risk_score"] = e2e["probability"] * e2e["impact"] * e2e["detectability"]
e2e["priority_per_min"] = (e2e["risk_score"] / e2e["runtime_min"]).round(1)
e2e = e2e.sort_values("risk_score", ascending=False)
display(e2e)
fig, ax = plt.subplots(figsize=(9, 4))
ax.barh(e2e["scenario"], e2e["risk_score"], color="#A5A5A5")
ax.invert_yaxis()
ax.set_title("E2Eシナリオの相対リスク優先度")
ax.set_xlabel("リスクスコア")
ax.set_ylabel("業務シナリオ")
ax.grid(axis="x", alpha=.3)
plt.tight_layout()
plt.show()
| scenario | probability | impact | detectability | runtime_min | risk_score | priority_per_min | |
|---|---|---|---|---|---|---|---|
| 0 | 検査登録→品質承認 | 4 | 5 | 4 | 9 | 80 | 8.9 |
| 1 | 作業指示→実績確定 | 5 | 5 | 3 | 8 | 75 | 9.4 |
| 2 | 設備停止→保全通知 | 3 | 5 | 5 | 11 | 75 | 6.8 |
| 3 | 在庫照会 | 4 | 4 | 2 | 5 | 32 | 6.4 |
| 4 | 帳票ダウンロード | 3 | 3 | 2 | 7 | 18 | 2.6 |
| 5 | 表示設定変更 | 2 | 1 | 1 | 3 | 2 | 0.7 |
結果の読み取り
品質承認や実績確定など、失敗時の影響が大きい一連操作が上位になります。毎回すべてを実行せず、Pull Requestでは短いスモークテスト、夜間に広い回帰テストという分割も有効です。テストデータの独立性、時刻依存、外部設備の代替環境を整え、不安定な失敗を放置しません。
No.066:型チェックを導入する
実務での意味
品番を数量へ渡す、欠損し得る測定値を数値として計算する、APIの項目名変更に画面が追随しない、といった不整合は型チェックで実行前に発見できます。Pythonならmypyやpyright、TypeScriptならtscが候補です。
分析・モデル化の考え方
モジュール別に型エラー密度(1,000行当たり件数)を計算します。件数だけでは大きいモジュールが不利になるため、コード行数で正規化します。ただし型エラー0は業務ロジックの正しさを保証せず、単体・APIテストと組み合わせます。
def defect_rate(defects: int, produced: int) -> float:
return defects / produced
# mypy --strict src/
Pythonで確認する
type_kpi = static_files.groupby("module").agg(
files=("module", "size"), loc=("loc", "sum"), type_errors=("type_errors", "sum")
)
type_kpi["errors_per_kloc"] = (type_kpi["type_errors"] / type_kpi["loc"] * 1000).round(2)
type_kpi = type_kpi.sort_values("errors_per_kloc", ascending=False)
display(type_kpi)
fig, ax = plt.subplots(figsize=(8, 4))
type_kpi["errors_per_kloc"].plot.bar(ax=ax, color="#5B9BD5")
ax.set_title("モジュール別の型エラー密度")
ax.set_xlabel("モジュール")
ax.set_ylabel("型エラー数 / 1,000行")
ax.grid(axis="y", alpha=.3)
plt.xticks(rotation=0)
plt.tight_layout()
plt.show()
| files | loc | type_errors | errors_per_kloc | |
|---|---|---|---|---|
| module | ||||
| inventory | 102 | 47159 | 38 | 0.81 |
| results | 112 | 53228 | 42 | 0.79 |
| orders | 90 | 43239 | 33 | 0.76 |
| equipment | 94 | 45690 | 30 | 0.66 |
| quality | 102 | 48213 | 32 | 0.66 |
結果の読み取り
密度の高いモジュールから型を厳格化すると、既存システムへ段階導入しやすくなります。最初から全件エラーを必須修正にせず、新規・変更コードはエラー0、既存分は基準値を悪化させない運用が現実的です。APIスキーマから型を生成すると、バックエンドと画面の契約ずれも減らせます。
No.067:リンターを導入する
実務での意味
リンターは未使用変数、到達不能コード、危険な例外処理、複雑すぎる関数などを自動検出します。製造システムではexcept: passが設備通信エラーを隠すなど、書き方の問題が監視漏れへつながることがあります。
分析・モデル化の考え方
モジュール別の指摘密度と種類を確認し、バグにつながる規則を先に必須化します。全規則を一度に有効化すると大量の書式指摘に埋もれるため、重大度、誤検知、修正費用で段階を分けます。PythonではRuffなどをCIとエディタで同じ設定にします。
ruff check src tests
ruff check src tests --output-format=github
Pythonで確認する
lint_kpi = static_files.groupby("module").agg(
loc=("loc", "sum"), lint_errors=("lint_errors", "sum")
)
lint_kpi["errors_per_kloc"] = (lint_kpi["lint_errors"] / lint_kpi["loc"] * 1000).round(2)
lint_kpi = lint_kpi.sort_values("errors_per_kloc", ascending=False)
display(lint_kpi)
rules = pd.DataFrame({
"rule_group": ["例外握りつぶし", "未定義・未使用", "複雑度", "可読性"],
"findings": [38, 91, 54, 147],
"gate": ["即時エラー", "即時エラー", "警告→段階導入", "自動修正"],
})
display(rules)
| loc | lint_errors | errors_per_kloc | |
|---|---|---|---|
| module | |||
| orders | 43239 | 65 | 1.50 |
| inventory | 47159 | 65 | 1.38 |
| equipment | 45690 | 60 | 1.31 |
| results | 53228 | 69 | 1.30 |
| quality | 48213 | 56 | 1.16 |
| rule_group | findings | gate | |
|---|---|---|---|
| 0 | 例外握りつぶし | 38 | 即時エラー |
| 1 | 未定義・未使用 | 91 | 即時エラー |
| 2 | 複雑度 | 54 | 警告→段階導入 |
| 3 | 可読性 | 147 | 自動修正 |
結果の読み取り
モジュールの規模で正規化すると重点領域を比較できます。導入時は例外握りつぶしや未定義参照をCIの失敗条件とし、複雑度は警告から始めるなど、規則ごとに扱いを変えます。抑制コメントには理由を残し、恒久的な盲点にならないよう定期レビューします。
No.068:フォーマッターを導入する
実務での意味
フォーマッターは空白、改行、引用符などを統一し、レビューを仕様・安全性・保守性へ集中させます。人ごとの好みをなくすことが目的ではなく、意味のない差分を減らして重要な変更を見やすくする仕組みです。
分析・モデル化の考え方
整形前後でレビューコメントの内訳を比較するシミュレーションを行います。書式コメントが自動化されることで、同じレビュー時間内に業務ロジックへ使える比率がどう変わるかを見ます。PythonならRuff formatやBlack、フロントエンドならPrettierが候補です。
ruff format src tests
ruff format --check src tests
Pythonで確認する
review = pd.DataFrame({
"category": ["書式", "命名・可読性", "業務ロジック", "テスト不足", "セキュリティ"],
"導入前": [34, 22, 27, 12, 5],
"導入後": [3, 23, 42, 23, 9],
}).set_index("category")
display(review)
fig, ax = plt.subplots(figsize=(9, 4))
review.plot.bar(ax=ax, color=["#BFBFBF", "#4472C4"])
ax.set_title("フォーマッター導入前後のレビューコメント構成(試算)")
ax.set_xlabel("コメント分類")
ax.set_ylabel("構成比(%)")
ax.grid(axis="y", alpha=.3)
plt.xticks(rotation=20, ha="right")
plt.tight_layout()
plt.show()
| 導入前 | 導入後 | |
|---|---|---|
| category | ||
| 書式 | 34 | 3 |
| 命名・可読性 | 22 | 23 |
| 業務ロジック | 27 | 42 |
| テスト不足 | 12 | 23 |
| セキュリティ | 5 | 9 |
結果の読み取り
仮定上、書式コメントが減り、業務ロジックやテスト不足へレビューを振り向けられます。導入時に全ファイルを一括整形すると履歴が追いにくくなるため、機能変更と整形だけのコミットを分離し、移行時点をチームで共有します。CIでは整形済みかだけを確認します。
No.069:ログ出力を設計する
実務での意味
ログは障害時の証拠であり、単なるデバッグ文ではありません。時刻、レベル、サービス、イベント名、相関ID、工場・ライン、作業指示ID、結果を構造化して残すと、画面からAPI、バッチまで一連の処理を追えます。一方、パスワード、トークン、不要な個人情報は記録しません。
分析・モデル化の考え方
ログ充足度を必須3項目の記録数で0〜3に分類し、MTTRとの関係を比較します。これは因果効果の証明ではありませんが、診断可能性の改善候補を示します。相関ID付与率や必須項目欠損率を運用KPIにします。
logger.info("quality_decision_completed", extra={
"correlation_id": cid, "order_id": order_id,
"plant_id": plant_id, "result": "rejected"
})
Pythonで確認する
incidents["log_completeness"] = incidents[["correlation_id", "order_id_logged", "stacktrace_logged"]].sum(axis=1)
log_kpi = incidents.groupby("log_completeness").agg(
incidents=("incident_id", "size"), median_mttr_min=("mttr_min", "median"),
p90_mttr_min=("mttr_min", lambda s: s.quantile(.9))
).round(1)
display(log_kpi)
fig, ax = plt.subplots(figsize=(8, 4))
ax.plot(log_kpi.index, log_kpi["median_mttr_min"], marker="o", linewidth=2)
ax.set_title("ログ必須項目の充足数とMTTR中央値")
ax.set_xlabel("記録された必須項目数(0〜3)")
ax.set_ylabel("MTTR中央値(分)")
ax.set_xticks([0, 1, 2, 3])
ax.grid(alpha=.3)
plt.tight_layout()
plt.show()
| incidents | median_mttr_min | p90_mttr_min | |
|---|---|---|---|
| log_completeness | |||
| 1 | 19 | 122.0 | 269.0 |
| 2 | 77 | 103.0 | 253.0 |
| 3 | 84 | 97.0 | 173.1 |
結果の読み取り
この架空データでは必須項目が揃うほどMTTR中央値が短い傾向です。ただし重大度などの交絡があるため、ログだけの効果とは断定しません。実務では相関IDを入口で発行して全サービスへ伝播し、イベント名と項目定義をログ仕様として管理します。保持期間、閲覧権限、マスキングも同時に設計します。
No.070:例外発生時の調査方法を整理する
実務での意味
障害対応では、すぐコードを直す前に、影響範囲を限定し、安全な代替運用やロールバックを判断します。品質判定の誤りなら出荷保留、実績登録障害なら紙・端末内保存など、技術復旧と業務継続を並行させます。
分析・モデル化の考え方
対応を検知、一次切り分け、影響限定、原因特定、復旧、再発防止に分解します。平均復旧時間は ですが、重大障害は中央値だけでなく90パーセンタイルも確認します。ログ充足度別のMTTRを使い、調査手順改善の効果を試算します。
Pythonで確認する
severity_order = ["重大", "高", "中"]
incident_kpi = incidents.groupby("severity").agg(
incidents=("incident_id", "size"),
mean_mttr_min=("mttr_min", "mean"),
median_mttr_min=("mttr_min", "median"),
p90_mttr_min=("mttr_min", lambda s: s.quantile(.9)),
).reindex(severity_order).round(1)
display(incident_kpi)
runbook = pd.DataFrame({
"phase": ["検知", "一次切り分け", "影響限定", "原因特定", "復旧", "再発防止"],
"owner": ["監視担当", "当番開発者", "工場責任者", "開発・基盤", "変更責任者", "開発・業務部門"],
"evidence": ["アラート・KPI", "相関ID・直前変更", "対象工場・指示", "ログ・トレース・再現", "復旧確認・データ整合", "原因・対策・期限"],
})
display(runbook)
fig, ax = plt.subplots(figsize=(8, 4))
incident_kpi[["median_mttr_min", "p90_mttr_min"]].plot.bar(ax=ax, color=["#70AD47", "#C00000"])
ax.set_title("重大度別の復旧時間")
ax.set_xlabel("重大度")
ax.set_ylabel("復旧時間(分)")
ax.grid(axis="y", alpha=.3)
plt.xticks(rotation=0)
plt.tight_layout()
plt.show()
| incidents | mean_mttr_min | median_mttr_min | p90_mttr_min | |
|---|---|---|---|---|
| severity | ||||
| 重大 | 27 | 106.7 | 98.0 | 201.6 |
| 高 | 71 | 122.5 | 98.0 | 253.0 |
| 中 | 82 | 113.4 | 105.5 | 194.8 |
| phase | owner | evidence | |
|---|---|---|---|
| 0 | 検知 | 監視担当 | アラート・KPI |
| 1 | 一次切り分け | 当番開発者 | 相関ID・直前変更 |
| 2 | 影響限定 | 工場責任者 | 対象工場・指示 |
| 3 | 原因特定 | 開発・基盤 | ログ・トレース・再現 |
| 4 | 復旧 | 変更責任者 | 復旧確認・データ整合 |
| 5 | 再発防止 | 開発・業務部門 | 原因・対策・期限 |
結果の読み取り
p90が中央値より大きい場合、少数の長期化障害を平均値だけで隠さないことが重要です。初動では直前リリース、対象範囲、再現条件、相関IDを揃え、証拠を残してから安全な復旧を行います。事後レビューは個人の責任追及ではなく、検出・防御・診断のどこが不足したかを対策と期限へ落とします。
対象ノックを通して見える実務上の示唆
-
テストは業務リスクから逆算する
品質判定、実績確定、設備停止通知など、失敗時の損失が大きい経路から防御層を決めます。 -
速いテストを土台にし、E2Eを重要フローへ絞る
単体・APIテストで短いフィードバックを作り、E2Eは境界をまたぐ代表シナリオに集中させます。 -
静的解析はレビュー時間の再配分である
型・規約・書式を自動化し、人は業務仕様、例外時の安全性、テスト不足を確認します。 -
ログ品質は復旧能力の一部である
相関IDと業務識別子を構造化して残し、監視から対象指示、処理履歴まで追跡可能にします。 -
品質を複数KPIで管理する
テスト件数やカバレッジだけでなく、変更失敗率、重大障害流出率、不安定テスト率、MTTRを追います。
実務導入する場合に必要なこと
1. リスクと受入条件の合意
品質保証、生産管理、現場、情報システムで、止めてはいけない業務、許容停止時間、品質・法令上の必須条件を整理します。要件を正常系だけでなく境界値、権限、通信失敗、再送まで受入条件にします。
2. CI品質ゲートの段階導入
変更コードの単体・APIテスト、型チェック、リンター、フォーマット確認を自動化します。既存負債は基準値を記録し、新規差分で悪化させないところから始めます。不安定テストには担当と修正期限を付けます。
3. 本番相当のテストデータと環境
個人情報・実績値をそのまま複製せず、匿名化または合成データを使います。設備・外部基幹との連携は契約テストやシミュレーターを用意し、時刻、タイムゾーン、再送、部分障害を再現します。
4. 観測可能性とインシデント運用
構造化ログ、メトリクス、トレース、相関IDを揃え、アラートから手順書へ到達できるようにします。工場側の業務継続手順、連絡網、ロールバック権限、データ復旧確認を定期的に訓練します。
5. 継続改善
本番障害を回帰テストへ追加し、長期化した調査はログ項目や監視を改善します。KPIを個人評価へ直結させず、システムとプロセスを改善するために使います。
まとめ
No.061〜No.070では、テストの層、バックエンド・API・画面・E2E、型・リンター・フォーマッター、ログ、例外調査を、製造実行・品質管理システムの品質保証プロセスとして確認しました。
重要なのは、ツールを導入すること自体ではありません。製造業務のリスクをテストへ翻訳し、機械的に確認できることを自動化し、本番で残る不確実性には診断可能なログと復旧手順を用意することです。架空データで確認した検出率やMTTRは出発点であり、実運用のCI履歴・障害票・監視データで継続的に更新します。
法人向けのご相談
数理工房では、製造業の業務システム、データ分析・AIシステムについて、品質戦略、テスト自動化、CI品質ゲート、ログ・監視、障害対応手順まで一体でご支援しています。
- 品質・生産システムのテスト範囲をリスクベースで見直したい
- APIや画面の回帰テストを自動化したい
- 型チェック・リンター・CIを既存システムへ段階導入したい
- 障害調査に時間がかかり、ログ・監視・運用を改善したい
といった課題について、現状評価、設計、プロトタイプ、実装、運用定着までご相談いただけます。
📩 お問い合わせ: surikobo.co.jp/contact
まずはお気軽にご相談ください。