今回の到達点
Day 1からDay 7までで、Flaskによる画面配信、Mermaid.jsによるリアルタイム描画、構文エラー処理、.mmd保存・読込、SVG出力、コードコピー、テンプレート選択、README整備を進めました。最終回のDay 8では、個々の機能が動くことと、一連の操作として矛盾なく動くことを区別して検証します。
MVP(Minimum Viable Product)は、必要最小限の機能で利用価値を確かめられる製品です。「機能を実装した」だけで完成とはせず、合格基準を決めてテスト結果を残します。
flowchart TD
A[ソースと依存関係] --> B[Flaskの配信テスト]
B --> C[ブラウザの操作テスト]
C --> D{全項目合格?}
D -->|Yes| E[MVP受入完了]
D -->|No| F[原因の記録と修正]
F --> B今日の成果物は、新規のtests/test_mvp.py、READMEへのテスト手順追記、受入チェック表です。Day 6までのeditor.jsやtemplates.jsを別実装に置き換えません。
総合テストで何を確認するか
単体テストと総合テストの違い
単体テストは関数や小さな処理の振る舞いを調べます。総合テストは、入力から描画、保存、再読込までの機能間の接続を確認します。今回は自動化できるサーバー側の確認と、ブラウザでしか確かめられない確認を分けます。
| 対象 | 検証方法 | 合格条件 |
|---|---|---|
| Flaskトップページ | 自動テスト | HTTP 200を返す |
| 画面要素 | 自動テスト | 必須IDとmoduleスクリプトがある |
| CSS・JavaScript配信 | 自動テスト | 3ファイルを取得できる |
| 入力・描画・エラー復帰 | ブラウザ | 最後の入力が図へ反映される |
.mmd保存・読込 | ブラウザ | 元のテキストへ復元できる |
| SVG出力・コピー | ブラウザ | 図とソースを正しく持ち出せる |
| テンプレート選択 | ブラウザ | 適用・キャンセルが仕様どおり |
Flaskのテストクライアントは、開発サーバーを起動せずHTTPリクエストを模擬できます。ただし、JavaScriptの実行やMermaidの描画までは検証しません。この境界を明確にすることが、テスト結果を誤解しないための第一歩です。
既存構成にテストファイルを追加する
Day 7の構成へtestsフォルダだけを追加します。
mermaid-editor/
├── app.py
├── requirements.txt
├── README.md
├── .gitignore
├── tests/
│ └── test_mvp.py ← 今回追加
├── templates/
│ └── index.html
└── static/
├── css/style.css
└── js/
├── editor.js
└── templates.js
テストにはPython標準ライブラリのunittestを使います。追加のテスト専用パッケージをインストールせず、Day 7のFlask環境で実行できる構成です。app.pyにFlaskインスタンスappが定義されていることを前提にします。
tests/test_mvp.pyの完成コード
import unittest
from html.parser import HTMLParser
from app import app
class PageElements(HTMLParser):
def __init__(self):
super().__init__()
self.ids = set()
self.scripts = []
def handle_starttag(self, tag, attrs):
attributes = dict(attrs)
element_id = attributes.get("id")
if element_id:
self.ids.add(element_id)
if tag == "script":
self.scripts.append(attributes)
class MvpSmokeTests(unittest.TestCase):
@classmethod
def setUpClass(cls):
app.config["TESTING"] = True
cls.client = app.test_client()
def get_page(self):
response = self.client.get("/")
self.assertEqual(response.status_code, 200)
return response.get_data(as_text=True)
def test_homepage(self):
self.assertIn("<html", self.get_page().lower())
def test_required_elements(self):
parser = PageElements()
parser.feed(self.get_page())
required = {
"mermaid-input", "mermaid-preview", "error-message",
"action-status", "open-button", "save-button",
"svg-button", "copy-button", "file-input",
"template-select", "template-description",
}
self.assertFalse(required - parser.ids,
f"Missing IDs: {required - parser.ids}")
def test_module_entry_point(self):
parser = PageElements()
parser.feed(self.get_page())
self.assertTrue(any(
script.get("type") == "module"
and script.get("src", "").endswith("/static/js/editor.js")
for script in parser.scripts
))
def test_static_assets(self):
for path in (
"/static/css/style.css",
"/static/js/editor.js",
"/static/js/templates.js",
):
with self.subTest(path=path):
response = self.client.get(path)
self.assertEqual(response.status_code, 200)
self.assertTrue(response.data)
def test_client_side_contract(self):
editor = self.client.get(
"/static/js/editor.js"
).get_data(as_text=True)
templates = self.client.get(
"/static/js/templates.js"
).get_data(as_text=True)
for name in (
"scheduleRender", "renderSequence", "renderedSource",
"downloadBlob", "copyMermaidSource", "getTemplateById",
):
with self.subTest(name=name):
self.assertIn(name, editor)
for template_id in (
"flowchart-basic", "sequence-basic", "state-basic",
):
with self.subTest(template_id=template_id):
self.assertIn(template_id, templates)
if __name__ == "__main__":
unittest.main()
HTMLParserはHTMLを読み取り、必要なIDとスクリプトの属性を収集します。subTest()は、複数の静的ファイルや識別子を個別に報告するための仕組みです。
最後のテストは名前の存在を調べる構造検査であり、JavaScript関数の実行結果を保証しません。Day 5・6の記事で示した関数名とIDを契約として扱っています。独自に改名している場合は、テスト側の期待値も更新してください。
自動テストを実行する
プロジェクト直下で、Day 7で作成した仮想環境のPythonを使います。
Windows PowerShell
.\.venv\Scripts\python.exe -m unittest discover -s tests -v
macOS・Linux
./.venv/bin/python -m unittest discover -s tests -v
Ran 5 testsとOKが表示されれば、上記5つのテストメソッドが合格です。これは期待する出力例であり、この記事の制作環境でFlaskを実行した結果ではありません。失敗した場合は、FAIL(期待値不一致)とERROR(例外)を区別して原因を調べます。
Node.jsがインストールされている場合は、JavaScriptの構文だけを追加確認できます。
node --check static/js/editor.js
node --check static/js/templates.js
--checkは構文検査であり、CDNからMermaid.jsを取得したり、ブラウザAPIを実行したりしません。Node.jsはアプリの通常起動には不要です。
ブラウザで行う総合受入テスト
Flaskを起動し、ブラウザでhttp://127.0.0.1:5000/を開きます。以下は実施するための試験票です。合格欄を埋めるまでは、実機テスト完了とは扱いません。
| ID | 操作 | 期待結果 |
|---|---|---|
| B01 | 正常な図を入力 | プレビューにSVGが表示される |
| B02 | 短時間で連続編集 | 最後の入力だけが反映される |
| B03 | 構文を壊してから修正 | エラー表示後、正常描画に復帰する |
| B04 | 入力を空にする | プレビューが消える |
| B05 | .mmd保存→編集→読込 | 保存時のコードへ戻る |
| B06 | 正常描画後にSVG出力 | diagram.svgが開ける |
| B07 | 編集直後にSVG出力 | 古い図の出力が拒否される |
| B08 | Copy Code→テキストへ貼付 | 改行を含め元のコードと一致する |
| B09 | 3種類のテンプレートを選択 | 各ひな形が描画される |
| B10 | テンプレート上書きをキャンセル | 編集中のコードが残る |
| B11 | 構文エラー中にCopy Code | ソースだけはコピーできる |
| B12 | ページを再読み込み | 自動保存されない仕様を確認する |
テスト用の正常な入力
次のコードを左側の入力欄へ貼り付けます。これはテストデータなので、記事上では図に変換せずコードとして示します。
flowchart LR
A[Start] --> B{Check}
B -->|OK| C[Finish]
B -->|NG| D[Retry]
D --> B
B03では末尾をA[のような不完全な記述に変更し、エラー表示を確認してから元に戻します。B05では保存後に別の文字列へ編集し、保存した.mmdを読み直して入力文字列が一致することを確認します。B06はファイル名だけでなく、保存したSVGを別タブで開けるか確認してください。
非同期描画の競合を重点確認する
Mermaid描画は非同期です。入力Aの描画中に入力Bへ変更すると、Aの結果が後から到着する可能性があります。Day 3のrenderSequenceは古い結果の画面反映を防ぎ、Day 5のrenderedSourceは現在の入力と一致しないSVGの出力を防ぎます。
B02とB07はこの設計を確認する試験です。ブラウザの開発者ツールでCPUを低速化すると競合を観察しやすくなりますが、必ず再現するとは限りません。一度再現しなかったことは、競合が存在しない証明ではありません。
不合格時の切り分け方
Flaskのテストが失敗する
ModuleNotFoundError: No module named 'flask'なら、テストに使うPythonとインストール先が一致しているか確認します。HTTP 404なら静的ファイル名やディレクトリ構成を確認します。Missing IDsなら、Day 6で追加したHTML要素が残っているか確認してください。
描画やテンプレートだけが失敗する
ブラウザのConsoleとNetworkを確認します。MermaidのCDN取得失敗、type="module"の指定漏れ、templates.jsの相対パス誤りを切り分けます。FlaskのHTTP 200は、JavaScriptが正常に実行された証拠にはなりません。
保存・コピーだけが失敗する
.mmdとSVGはブラウザ側で生成・ダウンロードします。ダウンロード制限や保存先を確認します。Clipboard APIのwriteText()はセキュアコンテキストを要求し、ブラウザの権限やユーザー操作条件でも拒否される場合があります。localhostで開いて操作してください。
SVGの見た目が変わる
SVGはベクター形式ですが、外部フォントやCSSへの依存があると別環境で見た目が変わることがあります。ファイルが開けることと、すべての環境で同じ見た目になることは別の合格基準です。
READMEにテスト手順を追記する
Day 7のREADME.mdへ次の節を追加します。コードや設定値を重複管理せず、利用者が同じ試験を再実行できるようにします。
## テスト
Windows PowerShell:
```powershell
.\.venv\Scripts\python.exe -m unittest discover -s tests -v
```
macOS / Linux:
```bash
./.venv/bin/python -m unittest discover -s tests -v
```
自動テスト合格後、ブラウザで入力・描画・構文エラー復帰・
.mmd保存読込・SVG出力・コピー・テンプレート適用を確認します。
自動テストだけではブラウザ機能の正常動作は保証されません。
注意: 上のREADME例はMarkdown内にコードフェンスを含むため、実際に記事からコピーするときは外側の表示枠を含めず、## テストから保存してください。
MVP完成の判定基準
完成を宣言する条件は次のとおりです。
unittestの5テストがすべて合格する。- ブラウザ受入テストB01〜B12の結果を記録し、重大な不具合が残っていない。
- READMEどおりに新しい仮想環境から起動できる。
- 制約を明示する。自動保存・認証・クラウド共有・本番公開は対象外とする。
テスト記録は、たとえばdocs/test-results.mdへ日付、Python・ブラウザ・Mermaidのバージョン、各項目の合否、再現手順を残します。これは後日の改修で同じ確認を繰り返すための回帰テストの基準になります。
今回の設計と試験票がそろった時点で、MVPの機能範囲と受入条件は完成です。ただし、実機で未実施の項目を合格と見なしてはいけません。すべての受入項目を確認した段階で、運用上の「MVP完成」と判定します。
練習問題
練習1:失敗箇所を分類する
トップページは表示されるのに、テンプレート選択だけが動きません。Flask側とブラウザ側のどちらから調べるべきでしょうか。
解答例: まずブラウザのConsoleとNetworkを確認します。templates.jsの読み込み、要素ID、changeイベント登録を調べます。HTTP 200だけではJavaScriptの動作を保証できません。
練習2:テストを1件増やす
/static/js/templates.jsが取得できるだけでなく、3種類のテンプレートIDが存在することを確認してください。
確認観点: 掲載したtest_client_side_contract()がすでにこの条件を確認しています。次の課題は、IDの重複がないことをJavaScript側で検証するテストへ拡張することです。
まとめ|8回で何ができるようになったか
Day 1のFlask最小構成から始め、Day 2で2ペインUI、Day 3でリアルタイム描画、Day 4で.mmd保存・読込、Day 5でSVG出力・コピー、Day 6でデータ駆動テンプレート、Day 7でREADMEを整えました。最終回では動作を検証する手順と合否基準を追加しました。
小さなWebツールでも、入力・描画・保存・出力・テンプレート・テストの責務を分ければ、あとから機能を増やしやすくなります。設計図、業務フロー、設備の状態遷移などを扱うツールへ発展させる場合にも、この分離は再利用できます。
全8回のカリキュラムはここで完結です。 新しい機能を追加する前に、まず受入テストを実行し、結果を記録してMVPを確定させましょう。
参考資料
- Flask公式ドキュメント「Testing Flask Applications」:テストクライアントの使い方。
- Python公式ドキュメント「unittest」:標準テストフレームワークとテスト検出。
- Mermaid公式ドキュメント「Usage」「Mermaid API」:構文検証と描画API。
- MDN「Clipboard.writeText()」:クリップボード操作とセキュアコンテキスト。
