「FlaskでMermaid簡易エディタを作る|総合テストでMVPを完成させる」の内容を表す技術イラスト
UNIT 00LAB NOTES FIG.00 · EYECATCH

FlaskでMermaid簡易エディタを作る|総合テストでMVPを完成させる

FlaskとMermaid.jsで作った簡易エディタを総合テストし、MVP完成の判定基準を整えます。Python標準unittestによるFlaskの配信検査、ブラウザでのリアルタイム描画・構文エラー復帰・.mmd保存読込・SVG出力・コピー・テンプレート選択の受入試験、READMEへの記録方法まで解説します。

DOC NO.EKP-00-1061
DATE
REVB
READ約 14 分
目次

今回の到達点

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出力古い図の出力が拒否される
B08Copy Code→テキストへ貼付改行を含め元のコードと一致する
B093種類のテンプレートを選択各ひな形が描画される
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完成の判定基準

完成を宣言する条件は次のとおりです。

  1. unittestの5テストがすべて合格する。
  2. ブラウザ受入テストB01〜B12の結果を記録し、重大な不具合が残っていない。
  3. READMEどおりに新しい仮想環境から起動できる。
  4. 制約を明示する。自動保存・認証・クラウド共有・本番公開は対象外とする。

テスト記録は、たとえば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()」:クリップボード操作とセキュアコンテキスト。
参考になったらシェアしてください
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

機械設計・油圧・CAD・Python・AIなど、ものづくりに関わる技術を扱っています。工学知識を整理・構造化し、設計や自動化に再利用できる形へ変えていくことを目指しています。

目次