TECH PLAY

Django

Djangoは、Pythonで開発されたオープンソースのWEBアプリケーションフレームワークです。Djangoは高い生産性と堅牢性を提供し、多くのプロジェクトで利用されています。

Djangoの主な特徴は以下の通りです。

MTVアーキテクチャ: Djangoはモデル(データベースの操作)、テンプレート(ユーザーインターフェースの処理)、ビュー(ビジネスロジックの処理)というMTV(モデル・テンプレート・ビュー)アーキテクチャを採用しています。このアーキテクチャにより、コードの再利用性と保守性が向上します。

ORM (Object-Relational Mapping): DjangoのORMはデータベースとのやり取りを簡単に行えるようにするためのツールです。SQLクエリの代わりにPythonのコードを使用してデータベースを操作できます。これにより、データベースに依存しない柔軟なアプリケーション開発が可能です。

豊富な機能セット: Djangoには多くの便利な機能が組み込まれています。ユーザー認証、セッション管理、URLルーティング、フォーム処理、管理者インターフェースなど、一般的なWEBアプリケーション開発に必要な機能を提供しています。

テンプレートエンジン: Djangoのテンプレートエンジンは、HTMLコードとPythonコードを組み合わせた柔軟なテンプレートを作成するためのものです。ビューから渡されたデータを動的に表示することができます。

スケーラビリティ: Djangoはスケーラビリティにも優れています。大規模なトラフィックや高負荷なアプリケーションにも対応できるように設計されており、キャッシング、非同期タスク、負荷分散などの機能を提供しています。

Djangoは豊富なドキュメントと活発なコミュニティがあり、多くの企業や開発者によって活用されています。シンプルな構造と高度な機能を兼ね備えたDjangoは、迅速かつ効率的なWEBアプリケーション開発において強力なツールとなっています。

Django

https://www.djangoproject.com/

イベント

該当するコンテンツが見つかりませんでした

マガジン

該当するコンテンツが見つかりませんでした

技術ブログ

はじめに はじめまして。2025年4月に新卒でニフティに入社した宮村です。 ニフティのエンジニア職には On-the-Job Training(OJT) という制度があり、私が入社した年度では入社後の約1年間で 3つの異なるチーム を経験しました。各期約3ヶ月、実際の業務に携わりながら技術とチームワークを学んでいく仕組みです。 この記事では、私がOJTで経験した3つのチームでの業務内容や、やりがい・苦労した点をエピソードベースでお伝えします。 「ニフティのOJTって実際どんな感じなの?」 という疑問を持つ方に、少しでもリアルな雰囲気が伝われば嬉しいです。 OJTの前に:新人研修(4月〜6月) OJTが始まる前に、約2ヶ月間の 新人研修 があります。研修は大きく3つのフェーズに分かれています。 共通研修(4月) まずはビジネス職と合同の共通研修からスタートします。社会人としての基礎やニフティの事業について学ぶ期間です。職種を問わず同期全員で受けるため、エンジニア以外の同期とも関係を築ける貴重な機会でした。 技術研修(5月) エンジニア職のみの技術研修に移ります。外部の研修会社による講義形式で、Web開発の基礎を体系的に学びます。HTML/CSS/JavaScriptといったフロントエンドの基礎から、Linuxやサーバの基本、バックエンド技術、コンテナ、テスト手法まで幅広くカバーされます。プログラミング経験の差に関わらず、全員が同じスタートラインに立てるよう設計されていました。 エンジニア定例(6月頭) 最後に、 社内のエンジニアが講師を務める研修 (エンジニア定例)を受けます。Git基礎、AWS基礎、生成AIなど、ニフティの現場で実際に使われている技術や手法を先輩エンジニアから直接学べます。外部研修で得た基礎知識を、ニフティの実務にどう活かすかという視点で深掘りしていく内容で、OJTに入る前の総仕上げとなりました。 OJTの基本的な流れ 各期の流れはおおむね共通しています。 目標設定 — 配属後、トレーナーや上長と面談し、その期で達成したい目標を決めます 開発業務 — スクラムなどの開発手法を通じて、実際のプロダクト開発に参加します 振り返り — 期の終わりに目標の達成度を振り返り、次期への引き継ぎを行います トレーナーの方が日々の相談相手としてついてくださるので、困ったときにすぐ聞ける環境が整っています。 一期:SSOチーム(6/12〜9/20) チームについて サービスインフラチームの うさかぴサブチーム に配属されました。名前から想像し辛いかもしれませんが、ニフティ全体で使われている認証・認可システム 「SSO」 やその他ユーザー管理に使用されるシステム を開発・運用するチームです。 SSOは全社共通の基盤であり、止まるとニフティの多くのサービスに影響が出ます。そのため、変更には慎重さが求められる環境でした。 使用技術 OpenID Connect(OIDC) Python(Django) AWS 主な業務内容 SSOをOIDCの標準仕様に近づける改修 @nifty 優待サービスの販路拡大に向けた改修 新規サービスにOIDC導入するための改修 やりがい 最もやりがいを感じたのは、 SSOをOpenID Connect(OIDC)の標準仕様に近づける改修 を行えたことです。 実は大学時代にOIDCを活用した研究をしていたため、この分野には馴染みがありました。スプリントのプランニングで標準化の方針が検討されていた際、開発を進める中で「この修正だけでは足りない箇所がある」ことに気づき、自ら追加の修正を提案・実装しました。 結果として、OIDCのライブラリを導入するだけでSSOを使えるようになり、プロダクトの利便性向上に貢献できました。 学生時代に学んだことが実務で活きた瞬間 は本当に嬉しかったです。 苦労した点 一方で、初めての配属ということもあり、 実務の開発プロセスを一から習得する 必要がありました。スクラム、テストコードの書き方、本番環境を止めずに品質を担保するための工夫など、学生時代には経験のなかったことばかりで最初は苦戦しました。 また、障害対応の場面に立ち会った際に自分の無力さを痛感したこともあります。当時は「一人で解決しなければ」という意識が強く、難しいタスクを抱え込みすぎてしまうこともありました。これは後の期で大きな学びに繋がります。 二期:ポータルチーム(9/21〜12/20) チームについて 第一開発チームの @niftyトップページを担当するサブチームに配属されました。 @niftyトップページ の開発・運用に加え、新規事業の開発も手がけるチームです。 サービスの企画担当者と密にコミュニケーションを取りながら進めるスタイルで、AWSをはじめとしたモダンな技術スタックを使った開発が特徴です。スクラム開発(2週間スプリント)を採用し、デイリースクラムでチーム全体の進捗を共有していました。 使用技術 microCMS Next.js AWS(Terraform) 主な業務内容 「@niftyトップページ」の開発・運用 新規事業のAWSインフラおよびフロントエンドの構築 やりがい・苦労した点 この期で最もやりがいと苦労を感じたのは、 未経験の状態からAWS環境の構築にゼロから挑戦したこと です。 当初はフロントエンド寄りの業務を想定していましたが、チームの状況や自分自身の希望もあり Terraform未経験の状態からインフラ構築 を任されることになりました。正直、最初は不安もありましたが、これが結果的に大きな成長のきっかけになります。 先輩方が過去に構築した環境を参考にしながら、VPC・DNS・CI/CDパイプラインの作成を進めました。ただ、今回の要件と過去の構成には差分があり、初めて触る技術の中でその差分を埋める作業には非常に苦労しました。 一期での反省を活かし、 困ったときは一人で抱え込まず、積極的にチームへ相談する ことを意識しました。先輩方の手厚いフォローのおかげで、最終的には新規事業のフロントエンドインフラをほぼ一人で構築し、動作確認まで完了できました。 また、一期でSSO(認証)の経験があったことが二期で活き、新規事業のSSO導入時に的確な助言ができたことも印象に残っています。 異なるチームでの経験が繋がる瞬間 は、OJTならではだと感じました。 三期:金剛チーム(12/21〜3/20) チームについて 入会システムチームの 金剛サブチーム に配属されました。代理店様が光回線やオプションサービスの入会に使用するシステムの、開発・運用を担当しています。 代理店様との接点が多く、社内外の複数部署との連携が求められる環境です。 使用技術 PHP(CakePHP) AWS(Terraform) 主な業務内容 「@nifty つなぎモバイル」をより多くの方に申し込めるよう、獲得代理店様向けの申込ツールを改修 サインアップシステムにクレカチェック機能を追加 光コラボ入会時のオプションサービス申し込みフォームのデザイン刷新 「ニフティ会員特別でんきプラン」申し込みシステムのAWS移行(Terraform) やりがい・苦労した点 三期では 複数のプロダクトを横断して改修する という、これまでにない経験をしました。 中でもオプションサービス申し込みフォームのデザイン刷新は、サービスの企画や制作を行うチーム、光コラボ申し込みシステムのチーム等と連携しながら進行するプロジェクトで、初めて経験する 多方面とのマルチパスな調整 に難しさを感じました。ただ、一期・二期で培った「相談すること」「周囲を巻き込むこと」の大切さを実践できている手応えがあります。 また、でんきシステムのAWS移行では、一期のAWS経験と二期のTerraform経験がそのまま活きました。OJTを通じて積み上げてきた技術が 三期で統合された ように感じ、自分自身の成長を実感できた瞬間でした。 OJT全体を通して 3チームの共通点 スクラム開発 — 三部署ともスクラムを採用しており、一度身につけた開発の型はどのチームでも活きました AIの活用推進 — 一期ではAI推進チームのメンバーがおり、二期ではKiroを使ったspec開発も実施されるなど、各チームで積極的にAIを取り入れています 人の温かさ — どのチームでも、わからないことがあれば時間を割いて丁寧に教えてくださる先輩方ばかりでした 技術の幅の広がり OJTを通じて触れた技術は、期ごとに大きく異なります。 一期 : Python / Django / OIDC 二期 : Next.js / microCMS / Terraform 三期 : PHP / CakePHP / Terraform バックエンド → フロント+インフラ → レガシー+モダンの混在環境と、 毎期まったく異なる技術スタックに触れられる のはOJTの大きな魅力です。 一番の成長:「相談する力」 振り返ると、OJTを通じた一番の成長は 技術力よりも「相談する力」 かもしれません。 一期では「一人でやろうとしすぎていた」という反省がありました。二期ではそれを意識的に改善し、積極的にチームへ相談するようにしました。そして三期では、トレーナーから「自走できる」と評価されるまでになりました。 一人で抱え込まないこと は、技術を学ぶことと同じくらい大切なスキルだと実感しています。 こうした成長を実現できたのは、 3つのチームを渡り歩きながら、毎回新しい環境で「相談する」経験を積み重ねられるニフティのOJTだからこそ だと強く感じています。チームが変わるたびに関係構築からやり直す大変さはありますが、その繰り返しこそが「相談する力」を本物にしてくれました。 ニフティに興味を持ってくれた方へ 私が入社した年度では、OJTを通じて約1年間で 3つの異なるチーム を経験しました。毎回新しい環境・新しい技術・新しい人間関係にゼロから飛び込むのは正直大変でしたが、だからこそ 圧倒的に視野が広がり、確かな成長を実感できた 1年間でした。 私が伝えたいことは3つです。 学生時代の経験は活きる場合がある — 私の場合はOIDCの研究経験が一期で即戦力になりました。どんな経験も無駄にはなりません 困ったら相談してほしい — ニフティには優しい先輩方がたくさんいます。一人で抱え込む必要はありません 毎期が新しいチャレンジ — 「できない」が「できる」に変わる瞬間を、OJTでは何度も味わえます この記事が、ニフティに興味を持ってくれた方にとって、少しでも働くイメージを持つきっかけになれば幸いです。
PSSLの佐々木です。 E2Eテストは重要だとわかっていても、Playwrightのコードを書くのが面倒で後回しにしていませんか? 本記事では、 Markdownファイルに日本語で操作手順を書くだけで、Playwrightが自動実行してくれるE2Eテストフレームワーク の作り方を、実際のプロダクション事例をもとに解説します。 この記事でわかること Markdownシナリオ駆動のE2Eテストの全体アーキテクチャ 自然言語ステップをPlaywrightアクションに変換する仕組み フォーム入力の多段フォールバック戦略 動画記録・スクリーンショットによるデバッグ支援 pre-commitフックとの連携による開発フロー統合 なぜMarkdownでE2Eテストを書くのか 従来のE2Eテストには3つの問題がありました。 テストコードが仕様と乖離する — Playwrightのコードを読んでも、何のシナリオをテストしているのかひと目でわからない 非エンジニアがレビューできない — PMやデザイナーがテストケースを確認・追加できない メンテナンスコストが高い — セレクタの変更ひとつで大量のテストが壊れる Markdownシナリオ駆動テストなら、こう書けます: ## シナリオ: 管理者ログイン成功 1. <http://localhost:8055/login/> にアクセスする 2. メールアドレスに「admin@example.com」を入力する 3. パスワードに「admin123」を入力する 4. 「ログイン」ボタンをクリックする 5. 「ダッシュボード」というテキストが画面に表示されていることを確認する 6. URLに「/admin/dashboard」が含まれることを確認する 誰が読んでも何をテストしているかわかります。 全体アーキテクチャ フレームワークは4つのコンポーネントで構成されます。 Markdownシナリオファイル (.md) | [Parser] Markdownを構造化データに変換 | [Step Mapper] 自然言語 → Playwrightアクション | [Runner] ブラウザ操作の実行・動画記録・レポート 各コンポーネントのコード量は驚くほど小さく、Parser約90行、Step Mapper約280行、Runner約280行で実現できます。 それぞれ解説していきます。 Step 1: Markdownシナリオのフォーマットを定義する まず、テストシナリオを記述するMarkdownのフォーマットを決めます。 # 認証機能テスト ## 前提条件 - テスト用管理者が存在する(email: admin@example.com, password: admin123) - テスト用代理店ユーザーが存在する(email: user@example.com, password: user123) ## シナリオ: 管理者ログイン成功 → ログアウト 1. <http://localhost:8055/login/> にアクセスする 2. メールアドレスに「admin@example.com」を入力する 3. パスワードに「admin123」を入力する 4. 「ログイン」ボタンをクリックする 5. 「ダッシュボード」というテキストが画面に表示されていることを確認する 6. URLに「/admin/dashboard」が含まれることを確認する 7. ログアウトする ## シナリオ: パスワード間違い 1. <http://localhost:8055/login/> にアクセスする 2. メールアドレスに「admin@example.com」を入力する 3. パスワードに「wrongpassword」を入力する 4. 「ログイン」ボタンをクリックする 5. 「メールアドレスまたはパスワードが正しくありません」というテキストが表示されることを確認する ルールはシンプルです: 要素 記法 例 テストファイルのタイトル # タイトル # 認証機能テスト 前提条件 ## 前提条件 + 箇条書き - テスト用ユーザーが存在する シナリオ ## シナリオ: 名前 + 番号リスト ## シナリオ: ログイン成功 ステップ 1. 操作内容 1. 「ログイン」ボタンをクリックする 1ファイルに複数シナリオを書けます。前提条件セクションはドキュメントとして機能し、シードデータの仕様を明示する役割を果たします。 Step 2: Markdownパーサーを実装する Markdownファイルを解析して構造化データに変換するパーサーを作ります。 # parser.py import re from dataclasses import dataclass, field from pathlib import Path @dataclass class Scenario: name: str steps: list[str] = field(default_factory=list) @dataclass class ScenarioFile: path: Path title: str preconditions: list[str] = field(default_factory=list) scenarios: list[Scenario] = field(default_factory=list) def parse_scenario_file(filepath: Path) -> ScenarioFile: """Markdownシナリオファイルを解析する""" text = filepath.read_text(encoding="utf-8") lines = text.splitlines() title = "" preconditions: list[str] = [] scenarios: list[Scenario] = [] current_section = None current_scenario: Scenario | None = None for raw_line in lines: line = raw_line.strip() # トップレベルタイトル if line.startswith("# ") and not line.startswith("## "): title = line[2:].strip() continue # セクションヘッダー if line.startswith("## "): header = line[3:].strip() if "前提条件" in header: current_section = "preconditions" current_scenario = None continue # シナリオ検出 m = re.match(r"^シナリオ[::]\\s*(.+)", header) if m: current_scenario = Scenario(name=m.group(1).strip()) scenarios.append(current_scenario) current_section = "scenario" continue # 箇条書き(前提条件) m_bullet = re.match(r"^[-*]\\s+(.+)", line) if m_bullet: content = m_bullet.group(1).strip() if current_section == "preconditions": preconditions.append(content) elif current_section == "scenario" and current_scenario: current_scenario.steps.append(content) continue # 番号付きリスト(シナリオステップ) m_num = re.match(r"^\\d+\\.\\s+(.+)", line) if m_num and current_section == "scenario" and current_scenario: current_scenario.steps.append(m_num.group(1).strip()) return ScenarioFile( path=filepath, title=title or filepath.stem, preconditions=preconditions, scenarios=scenarios, ) ポイントは番号付きリストから番号プレフィックスを除去してステップ文字列だけを抽出していることです。 1. 「ログイン」ボタンをクリックする → 「ログイン」ボタンをクリックする Step 3: ステップマッパーを実装する(コア部分) ここがこのフレームワークの心臓部です。自然言語のステップを正規表現でパターンマッチし、対応するPlaywrightアクションを実行します。 基本構造 # step_mapper.py import re from playwright.sync_api import Page, expect def execute_step(page: Page, step: str, base_url: str, timeout: int = 30000): """自然言語ステップを解釈してPlaywrightアクションを実行する""" # --- ナビゲーション --- m = re.search(r"(https?://\\S+)\\s*(?:に|へ)アクセスする", step) if m: page.goto(m.group(1), timeout=timeout) return m = re.search(r"(/.+?)\\s*(?:に|へ)(?:アクセス|遷移|移動)する", step) if m: page.goto(base_url + m.group(1), timeout=timeout) return # ... 他のパターンが続く 対応するステップパターン一覧 フレームワークが認識するステップパターンを紹介します。 ナビゲーション # 絶対URL # 例: "<http://localhost:8055/login/> にアクセスする" m = re.search(r"(https?://\\S+)\\s*(?:に|へ)アクセスする", step) if m: page.goto(m.group(1), timeout=timeout) return # 相対パス # 例: "/agency/dashboard にアクセスする" m = re.search(r"(/.+?)\\s*(?:に|へ)(?:アクセス|遷移|移動)する", step) if m: page.goto(base_url + m.group(1), timeout=timeout) return リンク・ボタンのクリック # リンクをクリック # 例: 「物件管理」リンクをクリックする m = re.search(r"[「「](.+?)[」」](?:リンク|メニュー)をクリックする", step) if m: text = m.group(1) page.get_by_role("link", name=text).first.click(timeout=timeout) page.wait_for_load_state("networkidle") return # ボタンをクリック # 例: 「ログイン」ボタンをクリックする m = re.search(r"[「「](.+?)[」」]ボタンを(?:クリック|押)する", step) if m: text = m.group(1) # submit ボタンを優先的に探す submit_buttons = page.locator("button[type='submit']:visible") if submit_buttons.count() > 0: for i in range(submit_buttons.count()): btn = submit_buttons.nth(i) if text in (btn.inner_text() or ""): btn.click(timeout=timeout) page.wait_for_load_state("networkidle") return # テキスト完全一致がなければ最初のsubmitボタン submit_buttons.first.click(timeout=timeout) else: page.get_by_role("button", name=text).first.click(timeout=timeout) page.wait_for_load_state("networkidle") return # テキスト要素をクリック(テーブル行など) # 例: 「SKR-001」テキストをクリックする m = re.search(r"[「「](.+?)[」」](?:テキスト|文字|項目|行)をクリックする", step) if m: page.get_by_text(m.group(1), exact=False).first.click(timeout=timeout) return フォーム入力 # テキスト入力 # 例: メールアドレスに「admin@example.com」を入力する m = re.search( r"[「「]?(.+?)[」」]?(?:欄|フィールド)?に\\s*[「「](.+?)[」」]\\s*(?:を入力|と入力)する", step ) if m: label, value = m.group(1), m.group(2) _fill_by_label(page, label, value, timeout) return # セレクトボックス # 例: 「ステータス」で「通電中」を選択する m = re.search(r"[「「](.+?)[」」](?:で|から)\\s*[「「](.+?)[」」]\\s*を選択する", step) if m: label, value = m.group(1), m.group(2) page.get_by_label(label).select_option(label=value, timeout=timeout) return # 日付入力 # 例: 希望日に「2026-12-01」を入力する m = re.search( r"[「「](.+?)[」」](?:欄|フィールド)?に\\s*(\\d{4}[-/]\\d{1,2}[-/]\\d{1,2})\\s*を(?:入力|設定)する", step ) if m: label = m.group(1) date_str = m.group(2).replace("/", "-") page.get_by_label(label).fill(date_str, timeout=timeout) return アサーション(検証) # テキストが表示されていることを確認 # 例: 「ダッシュボード」というテキストが画面に表示されていることを確認する m = re.search( r"[「「](.+?)[」」].*(?:表示されている|表示される|見える|確認する|含まれる|ある)", step ) if m: text = m.group(1) locator = page.locator( f":not(option):not(select):visible:has-text('{text}')" ).first expect(locator).to_be_visible(timeout=timeout) return # URLの確認 # 例: URLに「/admin/dashboard」が含まれることを確認する m = re.search(r"URLに\\s*[「「](.+?)[」」]\\s*が含まれる", step) if m: expect(page).to_have_url(re.compile(re.escape(m.group(1))), timeout=timeout) return # テキストが表示されていないことを確認 # 例: 「エラー」というテキストが表示されていないことを確認する m = re.search(r"[「「](.+?)[」」].*(?:表示されていない|表示されない|見えない)", step) if m: expect( page.get_by_text(m.group(1), exact=False).first ).not_to_be_visible(timeout=timeout) return その他 # ログアウト if re.search(r"ログアウトする", step): page.context.clear_cookies() page.goto(base_url + "/login/") page.wait_for_load_state("networkidle") return # 待機 # 例: 3秒待つ m = re.search(r"(\\d+)秒(?:待つ|待機する)", step) if m: page.wait_for_timeout(int(m.group(1)) * 1000) return # マッチしなかった場合 raise ValueError(f"未対応のステップ:{step}") フォーム入力の多段フォールバック戦略 フォーム入力は最もハマりやすいポイントです。実際のHTMLは <label> がない場合、 placeholder で代用している場合、CSSフレームワーク特有のマークアップなど、多様です。 そこで、 5段階のフォールバック戦略 を実装します。 def _fill_by_label(page: Page, label: str, value: str, timeout: int): """多段フォールバックでフォームフィールドを特定して入力する""" # Level 1: aria-label / <label> による特定 try: loc = page.get_by_label(label, exact=False).locator("visible=true") if loc.count() > 0: loc.first.fill(value, timeout=timeout) return except Exception: pass # Level 2: placeholder による特定 try: loc = page.get_by_placeholder(label, exact=False).locator("visible=true") if loc.count() > 0: loc.first.fill(value, timeout=timeout) return except Exception: pass # Level 3: label要素のDOM構造から辿る try: labels = page.locator(f"label:visible:has-text('{label}')") for i in range(labels.count()): label_elem = labels.nth(i) parent = label_elem.locator("..") inp = parent.locator("input:visible, textarea:visible, select:visible") if inp.count() > 0: inp.first.fill(value, timeout=timeout) return except Exception: pass # Level 4: name属性による特定(日本語ラベル → HTMLのname属性マッピング) field_map = { "メールアドレス": "email", "パスワード": "password", "物件名": "name", "郵便番号": "postal_code", "都道府県": "prefecture", "市区町村": "city", "町名番地": "address", "建物名": "building_name", "部屋番号": "room_number", "管理コード": "external_key", "メモ": "memo", # 必要に応じて追加 } name_attr = field_map.get(label) if name_attr: try: loc = page.locator(f"input[name='{name_attr}']:visible, textarea[name='{name_attr}']:visible") if loc.count() > 0: loc.first.fill(value, timeout=timeout) return except Exception: pass # Level 5: type属性による特定 type_map = {"メールアドレス": "email", "パスワード": "password"} type_attr = type_map.get(label) if type_attr: try: loc = page.locator(f"input[type='{type_attr}']:visible") if loc.count() > 0: loc.first.fill(value, timeout=timeout) return except Exception: pass raise ValueError(f"入力フィールドが見つかりません:{label}") この多段フォールバックにより、ほとんどのHTMLフォームに対応できます。 レベル 方法 対応するケース 1 get_by_label 正しく <label> がマークアップされたフォーム 2 get_by_placeholder placeholder 属性で入力ヒントを持つフォーム 3 DOM構造を辿る <label> がinputと同じ親要素内にあるフォーム 4 name 属性マッピング <label> がないがname属性は一貫しているフォーム 5 type 属性 email/passwordなど型で一意に特定できるフィールド Step 4: テストランナーを実装する シナリオファイルの解析とステップ実行を統合するランナーを作ります。 # runner.py import signal import socket import subprocess import sys import time from dataclasses import dataclass from pathlib import Path from playwright.sync_api import sync_playwright from config import APP_DIR, BASE_URL, BROWSER, HEADLESS, SCENARIOS_DIR, TIMEOUT, VIDEO_DIR from parser import parse_scenario_file from step_mapper import execute_step @dataclass class TestResult: scenario_file: str scenario_name: str passed: bool error: str = "" video_path: str = "" screenshot_path: str = "" def start_django_server(port: int = 8055): """Djangoサーバーを起動する(既に起動中ならスキップ)""" sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) try: sock.connect(("localhost", port)) sock.close() print(f"ポート{port} は既に使用中です。既存のサーバーを使用します。") return None except ConnectionRefusedError: sock.close() proc = subprocess.Popen( [sys.executable, str(APP_DIR / "manage.py"), "runserver", str(port), "--noreload"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, ) # サーバーの起動を待機 for _ in range(30): try: s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect(("localhost", port)) s.close() print(f"Django開発サーバーがポート{port} で起動しました") return proc except ConnectionRefusedError: time.sleep(1) raise RuntimeError("Djangoサーバーの起動がタイムアウトしました") def stop_django_server(proc): """Djangoサーバーを停止する""" if proc: proc.send_signal(signal.SIGTERM) try: proc.wait(timeout=5) except subprocess.TimeoutExpired: proc.kill() def ensure_seed_data(): """テスト用シードデータを投入する""" subprocess.run( [sys.executable, str(APP_DIR / "manage.py"), "seed", "--reset"], check=True, capture_output=True, ) print("シードデータを投入しました") def run_scenarios(scenario_files: list[Path], base_url: str, timeout: int) -> list[TestResult]: """シナリオファイルを実行して結果を返す""" results = [] with sync_playwright() as p: browser = getattr(p, BROWSER).launch(headless=HEADLESS) for scenario_path in scenario_files: sf = parse_scenario_file(scenario_path) print(f"\\n{'='*60}") print(f"実行:{sf.title} ({scenario_path.name})") print(f"{'='*60}") # ファイル単位でブラウザコンテキスト(動画記録)を作成 video_dir = VIDEO_DIR / scenario_path.stem video_dir.mkdir(parents=True, exist_ok=True) context = browser.new_context( record_video_dir=str(video_dir), record_video_size={"width": 1280, "height": 720}, viewport={"width": 1280, "height": 720}, ) page = context.new_page() for scenario in sf.scenarios: print(f"\\n シナリオ:{scenario.name}") passed = True error_msg = "" screenshot_path = "" for i, step_text in enumerate(scenario.steps, 1): try: print(f" ステップ{i}:{step_text} ... ", end="", flush=True) execute_step(page, step_text, base_url, timeout) print("OK") except Exception as e: print(f"FAILED:{e}") passed = False error_msg = f"ステップ{i}:{step_text} ->{e}" # 失敗時のスクリーンショット ss_path = video_dir / f"{scenario.name}_fail.png" try: page.screenshot(path=str(ss_path)) screenshot_path = str(ss_path) except Exception: pass break results.append(TestResult( scenario_file=scenario_path.name, scenario_name=scenario.name, passed=passed, error=error_msg, screenshot_path=screenshot_path, )) # コンテキストを閉じて動画を確定 video_path_raw = page.video.path if page.video else None page.close() context.close() # 動画をリネーム if video_path_raw: final_video = video_dir / f"{scenario_path.stem}.webm" try: Path(video_path_raw).rename(final_video) for r in results: if r.scenario_file == scenario_path.name: r.video_path = str(final_video) except Exception: pass browser.close() return results def print_summary(results: list[TestResult]): """テスト結果のサマリーを表示する""" print(f"\\n{'='*60}") print("テスト結果サマリー") print(f"{'='*60}") for r in results: icon = "PASS" if r.passed else "FAIL" print(f" [{icon}]{r.scenario_file} >{r.scenario_name}") if r.video_path: print(f" 動画:{r.video_path}") if r.error: print(f" エラー:{r.error}") if r.screenshot_path: print(f" スクリーンショット:{r.screenshot_path}") total = len(results) passed = sum(1 for r in results if r.passed) failed = total - passed print(f"\\n合計:{total} 成功:{passed} 失敗:{failed}") def main(): import argparse parser = argparse.ArgumentParser(description="Markdown E2Eテストランナー") parser.add_argument("scenarios", nargs="*", help="実行するシナリオファイル") parser.add_argument("--base-url", default=BASE_URL) parser.add_argument("--no-headless", action="store_true") parser.add_argument("--no-server", action="store_true") parser.add_argument("--no-seed", action="store_true") parser.add_argument("--timeout", type=int, default=30) args = parser.parse_args() global HEADLESS if args.no_headless: HEADLESS = False # シナリオファイルを取得 if args.scenarios: files = [Path(s) for s in args.scenarios] else: files = sorted(SCENARIOS_DIR.glob("*.md")) if not files: print("シナリオファイルが見つかりません") sys.exit(1) # テスト実行 if not args.no_seed: ensure_seed_data() server_proc = None if not args.no_server: server_proc = start_django_server() try: results = run_scenarios(files, args.base_url, args.timeout * 1000) print_summary(results) sys.exit(0 if all(r.passed for r in results) else 1) finally: stop_django_server(server_proc) if __name__ == "__main__": main() 動画記録のポイント Playwrightの動画記録は ブラウザコンテキスト単位 で行われます。1つのシナリオファイル内の全シナリオが1本の動画にまとまるため、テスト失敗時のデバッグが容易です。 # ファイルごとにコンテキストを作成 → 1ファイル = 1動画 context = browser.new_context( record_video_dir=str(video_dir), record_video_size={"width": 1280, "height": 720}, ) Step 5: シナリオファイルを書く ここまでのフレームワークを使って、実際のシナリオを書いてみましょう。 ファイル命名規則 e2e/scenarios/ ├── 01_authentication.md # 認証 ├── 02_agency_property.md # 代理店 物件管理 ├── 03_agency_request.md # 代理店 申請フロー ├── 04_admin_dashboard.md # 管理者 ダッシュボード ├── 05_admin_request.md # 管理者 申請処理 └── 06_admin_agency.md # 管理者 代理店管理 番号プレフィックスで実行順序を制御します。認証テストを最初に実行し、前提となる機能を先に検証する構成です。 シナリオの書き方のコツ 1. 1シナリオに詰め込みすぎない <!-- BAD: 長すぎるシナリオ --> ## シナリオ: ログイン → 物件登録 → 申請 → 承認 → ログアウト 1. ... (50ステップ) <!-- GOOD: 論理的なまとまりで分割 --> ## シナリオ: 物件登録 1. ... (15ステップ) ## シナリオ: 通電申請 1. ... (12ステップ) ただし、1ファイル内のシナリオは同じ動画に記録されるため、 関連する操作フロー は同じファイルにまとめると良いでしょう。 2. セレクタではなくユーザーが見えるテキストを使う <!-- BAD: 実装依存 --> 1. #login-btn をクリックする <!-- GOOD: ユーザー視点 --> 1. 「ログイン」ボタンをクリックする 3. アサーションは具体的に <!-- BAD: 曖昧 --> 1. ページが表示されることを確認する <!-- GOOD: 具体的 --> 1. 「ダッシュボード」というテキストが画面に表示されていることを確認する 2. URLに「/admin/dashboard」が含まれることを確認する 対応しているステップ表現のリファレンス カテゴリ ステップ例 ナビゲーション http://... にアクセスする 、 /path にアクセスする リンククリック 「メニュー名」リンクをクリックする ボタンクリック 「送信」ボタンをクリックする テキストクリック 「SKR-001」テキストをクリックする タブ切替 「タブ名」タブをクリックする テキスト入力 項目名に「値」を入力する セレクト 「項目名」で「値」を選択する 日付入力 「項目名」に 2026-01-01 を入力する テキスト表示確認 「テキスト」というテキストが表示されていることを確認する テキスト非表示確認 「テキスト」が表示されていないことを確認する URL確認 URLに「/path」が含まれることを確認する ログアウト ログアウトする 待機 3秒待つ Step 6: pre-commitフックで開発に組み込む 変更したファイルに応じて関連するシナリオだけを自動実行するpre-commitフックを設定できます。 #!/bin/bash # e2e/hooks/pre-commit-e2e.sh CHANGED_FILES=$(git diff --cached --name-only) SCENARIOS_TO_RUN="" for file in $CHANGED_FILES; do case "$file" in *auth* | *login* | *middleware*) SCENARIOS_TO_RUN="$SCENARIOS_TO_RUN e2e/scenarios/01_authentication.md" ;; *property*) SCENARIOS_TO_RUN="$SCENARIOS_TO_RUN e2e/scenarios/02_agency_property.md" ;; *request*) SCENARIOS_TO_RUN="$SCENARIOS_TO_RUN e2e/scenarios/03_agency_request.md" SCENARIOS_TO_RUN="$SCENARIOS_TO_RUN e2e/scenarios/05_admin_request.md" ;; *agency*) SCENARIOS_TO_RUN="$SCENARIOS_TO_RUN e2e/scenarios/06_admin_agency.md" ;; # コアモデル変更時はフルリグレッション *models* | *enums* | *config/*) python e2e/runner.py exit $? ;; esac done if [ -n "$SCENARIOS_TO_RUN" ]; then # 重複除去して実行 UNIQUE=$(echo "$SCENARIOS_TO_RUN" | tr ' ' '\\n' | sort -u | tr '\\n' ' ') python e2e/runner.py $UNIQUE exit $? fi echo "E2Eテスト対象の変更なし、スキップします" exit 0 .git/hooks/pre-commit にシンボリックリンクを貼るか、 pre-commit フレームワークで管理します。 プロジェクト構成まとめ e2e/ ├── config.py # 環境設定(URL、タイムアウト、テストアカウント等) ├── parser.py # Markdownパーサー(~90行) ├── step_mapper.py # ステップマッパー(~280行) ├── runner.py # テストランナー(~280行) ├── requirements.txt # playwright>=1.40.0 ├── hooks/ │ └── pre-commit-e2e.sh ├── scenarios/ │ ├── 01_authentication.md │ ├── 02_agency_property.md │ └── ... └── test-results/ # 動画・スクリーンショット出力先 全体で約650行のPythonコードです。 実行方法 # Playwrightのインストール pip install playwright playwright install chromium # 全シナリオ実行 python e2e/runner.py # 特定シナリオのみ python e2e/runner.py e2e/scenarios/01_authentication.md # ブラウザを表示して実行(デバッグ用) python e2e/runner.py --no-headless # サーバーが既に起動している場合 python e2e/runner.py --no-server --no-seed 実行結果: シードデータを投入しました Django開発サーバーがポート 8055 で起動しました ============================================================ 実行: 認証機能テスト (01_authentication.md) ============================================================ シナリオ: 管理者ログイン成功 → ログアウト → パスワード間違い ステップ 1: <http://localhost:8055/login/> にアクセスする ... OK ステップ 2: メールアドレスに「admin@example.com」を入力する ... OK ステップ 3: パスワードに「admin123」を入力する ... OK ステップ 4: 「ログイン」ボタンをクリックする ... OK ステップ 5: 「ダッシュボード」というテキストが表示されている ... OK ... ============================================================ テスト結果サマリー ============================================================ [PASS] 01_authentication.md > 管理者ログイン成功 動画: test-results/01_authentication/01_authentication.webm 合計: 1 成功: 1 失敗: 0 従来のE2Eテストとの比較 項目 Playwrightコード直書き Markdownシナリオ駆動 可読性 エンジニアのみ 誰でも読める 記述量 多い(セレクタ指定等) 少ない(自然言語) メンテナンス テストごとに修正 Step Mapperの1箇所を修正 柔軟性 無制限 パターン定義内に限定 デバッグ ステップ単位で追跡可能 同左 + 動画記録 学習コスト Playwright APIの理解が必要 日本語テンプレに沿うだけ CI統合 標準的 同左 拡張のアイデア 新しいステップパターンの追加 step_mapper.py に正規表現と実行ロジックを追加するだけです。 # 例: チェックボックスの操作 m = re.search(r"[「「](.+?)[」」]チェックボックスをチェックする", step) if m: page.get_by_label(m.group(1)).check() return # 例: ファイルアップロード m = re.search(r"[「「](.+?)[」」]に\\s*[「「](.+?)[」」]\\s*をアップロードする", step) if m: page.get_by_label(m.group(1)).set_input_files(m.group(2)) return 多言語対応 パターン定義を外部ファイル(YAML等)に切り出せば、英語版も容易に作れます。 # patterns_en.yaml navigation: - pattern: 'navigate to "(.*)"' action: goto link_click: - pattern: 'click "(.*)" link' action: click_link テストデータのパラメータ化 前提条件セクションからテストデータを動的に生成する仕組みを追加することもできます。 まとめ Markdownシナリオ駆動のE2Eテストは、 仕様とテストを一体化 させるアプローチです。 Markdownで書いた操作手順がそのままテストになる 非エンジニアでもテストケースをレビュー・追加できる Step Mapperの修正1箇所で全テストの挙動を変更できる 動画記録により失敗時のデバッグが直感的 全体650行程度のコードで実現可能 Playwrightの柔軟なロケーター戦略( get_by_role , get_by_label , get_by_text )と正規表現ベースのパターンマッチの組み合わせにより、少ないコード量で実用的なフレームワークを構築できます。 テストが仕様書と乖離する問題に悩んでいるなら、ぜひ試してみてください。 ご覧いただきありがとうございます! この投稿はお役に立ちましたか? 役に立った 役に立たなかった 0人がこの投稿は役に立ったと言っています。 The post Markdownで書くE2Eテスト:自然言語シナリオをPlaywrightで自動実行する方法 first appeared on SIOS Tech Lab .
ブログリレー 前回の記事 では、バックエンドのあれこれを齋藤さんが記事にしてくれましたが、本日はフロントエンド編ということで、AWSパラメータシート自動生成ツールのGUIをどのような技術を使用して実現したかをご紹介できればと思います! フロントエンド概要 フロントエンドは下記のような流れでバックエンド(パラメーターシート出力)に処理が引き継がれます。 上記処理のうち、本記事のフロントエンドは「GUI」と「GUIを提供するサーバー」部分を担当しています。 使用した技術 フロントエンド実現のために使用した主な技術とその役割は下記の通りです。 技術 役割 JavaScript クライアント側、サーバーサイド側のロジックを記述するために使用した言語 Node.js  JavaScriptの実行環境であり、アプリケーション全体の実行基盤 Express.js WebサーバーおよびREST APIなどのサーバーサイド側の機能を提供してくれるWebフレームワーク HTML5 + CSS3 画面構造とデザイン フロントエンドは、クライアント側、サーバーサイド側も一貫してJavaScriptを使用して実装しています。 サーバーサイド側の開発として、Ruby + Rails や Python + Django といった組み合わせもある中で、JavaScript + Node.js を使用した理由としては、以下の通りです。 案件で JavaScript を触る機会があり慣れている よく耳にする組み合わせでなんかかっこいい しかし、よくよく調べるとよく耳にする理由がありました。それについては以降で説明していきたいと思います。 Node.js Node.jsとは? そもそもNode.jsとは何者かというと、 JavaScriptをサーバー上で実行するための開発環境 です。 元々、JavaScriptという言語は他の言語と異なり、ブラウザ上で動作する言語であり、ローカル(OS上)環境では動作させることができない言語でした。 よって、ブラウザ上でしか動作しない JavaScript ではローカル環境にあるファイルを読みにいくことができないという問題がありました。 その制限を取っ払ってくれたのが、Node.jsという訳です。 Node.jsの登場のおかげで、 クライアント・サーバーサイドの両者を同一の言語で開発 することができるようになりました。 Node.jsの特徴 Node.jsの特徴としては、主に以下の3つが挙げられます。 1. ノンブロッキングI/O処理 前のタスクが完了していない状態でも次のタスクを開始でき、 非同期での処理を実現 している。 そのため、大量のアクセスがあっても対応が可能である。 2. シングルスレッドによる処理 プログラムを実行する際に、1つずつ処理を行う 。 一般的にシングルスレッドだと大量のアクセスがあった場合に制御することが難しくなるが、ノンブロッキングI/O処理によって、多くのアクセスがあってもリアルタイムでレスポンスが可能になる。 3. 豊富なライブラリ サーバーサイドアプリケーション、デスクトップアプリケーション、コマンドラインツールなどの幅広い用途で使用されるため、非常に多くのライブラリがある。 npmというパッケージ管理ツールを使用することにより、膨大なライブラリを簡単にインストール・管理することができる。 これらの特徴から、Node.js + JavaScript は 大量の同時処理をさばけ、なおかつ様々な目的で使用できる汎用的な技術基盤 として人気があるという訳です。 Express.js ここまでで、Node.jsを使用した理由や用途は理解できたかと思いますが、あくまでもNode.jsはサーバーサイド側でJavaScriptを実行できるようにした実行環境であり、ただの基盤です。 フロントエンドを作成する上では、基盤の上に実際の機能を作成してあげる必要があります。 その実際の機能は一から作る必要はなく、既に用意されたひな形から作成することができます。 それにあたるのが、Express.jsです。 Express.jsとは? Express.jsとは、 Node.jsのための軽量で柔軟なWebアプリケーションフレームワーク です。 フレームワークとはひな形とも言い換えることができ、様々なひな形が用意されています。 例えば、機能開発が簡単に実装できる以下のようなひな形が用意されています。 ルーティング機能 ユーザーが特定のURLにアクセスした際に、 どのようなレスポンス(応答)を返すかを定義 することができる。 例えば、ユーザが / にアクセスしたら、トップページを表示する、/download/file.xlsx にアクセスしたら、ファイルをダウンロードするなどがある。 REST API機能 フロントエンド(ブラウザ)からのリクエストを受け取り、 Web上でデータをやりとりするための操作を定義 することができる。その操作には以下のものがあり、これらを定義することができる。 HTTPメソッド 操作 GET リソースを取得する POST リソースを作成する PUT 指定したIDのリソースを更新する DELETE 指定したIDのリソースを削除する 静的ファイルの配信 静的ファイル(HTML、CSS、JavaScript、画像など)をユーザーに配信することができる。 また、HTTPヘッダーによるキャッシュ制御も可能で、同じファイルへの再リクエスト時のレスポンス時間を短縮できる。 どこに使用したの? では、フロントエンド実現にあたってどこに活用したのかというと下記の通りです。 機能 使用箇所 ルーティング機能 トップページの表示 ファイルのダウンロード REST API機能 AWSリソース一覧取得 Excel生成リクエスト(バックエンドへのトリガー) 静的ファイルの配信 HTML/CSS/JavaScriptファイルの配信 苦労した点 リソースごとに異なるJSONファイル フロントエンドでは、AWSのリソースの詳細情報を表示するために、AWS CLIコマンドを実行し、リソースの一覧を取得してきています。その一覧情報の構造が、リソースによって異なり、リソースごとに取得する情報を定義してあげることが苦労しました。 例: EC2インスタンスの場合: {   “Reservations”: [     {       “Instances”: [         { “InstanceId”: “i-xxxxx”, “State”: {…} }       ]     }   ] } S3バケットの場合: {   “Buckets”: [     { “Name”: “my-bucket”, “CreationDate”: “…” }   ] } このように、同じ「リソース一覧を取得する」という処理でも、項目名やJSONファイルのネスト構造の深さが異なります。そのため、すべてのリソースに対して、どのキーからデータを取り出すか、IDとして何を使うかを個別に定義する必要がありました。 一方で、共通化できる部分は共通化し、できるだけ同様のコードで詳細情報を取得できるようにしてあげました。やはりコードの最適化には、AIを使用してあげるのが効率よかったです。 特殊なリソースの取得フロー 一般的なリソースの場合、取得フローは下記のようになります。 サービス選択 ⇒ リソース一覧から出力したいリソースを選択 ⇒ プレビュー画面表示 ⇒ Excel 出力 しかし、サービスの中には一発で一覧を取得できないサービスがあります。 例えば、ELBのターゲットグループの場合、ELBに紐づくターゲットグループを取得する必要があるため、まず最初にELBの一覧を取得し、その後にターゲットグループを取得する必要があります。よって、一覧の取得フローとしては下記のようになります。 サービス選択(ELBターゲットグループ)⇒ リソース一覧から出力したいリソースを選択(ELB)⇒ リソース一覧から出力したいリソースを選択(ターゲットグループ)⇒ プレビュー画面表示 ⇒ Excel 出力 このようにサービスによっては、中間ステップが存在するものもあるため、コードを共通化することができず、苦労しました。 まとめ 本記事では、AWSパラメータシート自動生成ツールのGUIであるフロントエンドで使用した技術について簡単に紹介しました。 実は、この取り組みはフロントエンドのアプリケーションを作成するつもりはなく、バックエンドのJSONファイルをパラメータシートに変換するアプリケーションのみを構築予定でした。しかし、使用するユーザー目線で考えると、やはりGUIがほしいということになり、開発に取り組みました。 普段はインフラ構築を担当する部署のため、初めて触れる技術が多く、開発には3~4か月ほどかかってしまいましたが、この取り組みを経てアプリケーションの知識をつけることができ、フルスタックエンジニアへと一歩近づいた気がします。 皆さんもぜひ、息抜きもかねて普段とは異なる分野の技術に触れてみるのはいかがでしょうか? というわけで、私の投稿は以上です! 次回は、AWSパラメータシート自動生成ツールを使ってみたということで、藪内さんにバトンタッチしますので、ぜひそちらも閲覧いただければと思います。

動画

書籍