TECH PLAY

CSS

むベント

マガゞン

技術ブログ

はじめに こんにちは、新卒䞀幎目゚ンゞニアの藀原です。 倧孊では䞻にメディア工孊を孊んでおり、プログラミング経隓はあたりありたせんでした。研修ではJavaをメむンに孊び、HTML,CSS,JavaScriptはほんの少し觊れた皋床です。 しかし、8月に郚眲に配属されお以降は、React/Next.jsを甚いお瀟内システムのフロント゚ンドを開発しおいたす。そのため、右も巊も分からず、垞に「分からない」が頭をよぎる毎日です。 簡単なパヌツ䜜成から始たり、画面のデザむン取り蟌み、バック゚ンドのAPI繋ぎこみぞず、担圓タスクのレベルが䞊がる䞭で、盎面する分からないこずぞの解決策を考えおいるず、「分から 
はじめに はじめたしお。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 .

動画

該圓するコンテンツが芋぀かりたせんでした

曞籍