
プログラミング
イベント
マガジン
技術ブログ
はじめに こんにちは、プラットフォーム部の 勝間田 です! 今回は書籍紹介記事の第2弾です! 昨年投稿した第一弾の記事は👇にあります。 tech.stmn.co.jp 今回はそれぞれ職種の異なる4人が各々GWで読んでよかった書籍について、紹介させていただきます! この記事で何か学びになったり、書籍を読むきっかけになったら嬉しいです! SREの知識地図——基礎知識から現場での実践まで 勝間田が紹介する本は、「SREの知識地図」という書籍です! gihyo.jp 私自身、今年からSRE業務に携わることになったため、SRE関連の書籍を探していたところこの本に出会いました! 読んでみて、SLIやSLO、The Four Golden SignalsといったSRE業務でよく使われる用語の意味をしっかりと学習することができました。用語の意味を理解したことで、普段何気なく活用していたDatadogなどの監視ツールも、より使いやすくなった気がします。 また、この書籍を読んでからシステムの「攻めと守り」や、エラーバジェットについて意識するようになりました。弊社ではまだ明確にSLO等を定義できているわけではないので今後の課題ですが、意識づけができたのはよかったかなと思っています! さらに、普段チームで行っているSRE業務が正しく行えているかの「答え合わせ」ができたのもよかったです。例えば、障害発生時に作成しているポストモーテムについては、本に記載されている通り「再発防止」や「被害の最小化」に向けた具体的なネクストアクションまで落とし込めていることが確認できたので、これは今後も自信を持って継続していきたいです。 弊社ではAIを活用すべく、開発チームの形も大きく変わりました。 本書ではチームトポロジーについても触れられており、4つのチームタイプと3つの主要なインタラクションモードが図解でわかりやすく解説されていました。色々なパターンのSREについて知ることができたので、自組織にあった動きができるよう精進していきたいです! SRE業務を始めることになり、SLIやSLOなどの基礎用語からしっかり理解したい方におすすめです! エンジニアリング組織論への招待 こんにちは、名古屋でEMをしているあさしん( @asashin227 )です。 私がお勧めするのは、「エンジニアリング組織論への招待」です。 gihyo.jp 日々エンジニアリングの現場で直面する様々な不合理に対して『エンジニアリング組織論への招待』は、ビジネスや組織、コミュニケーションの構造的な課題として捉え直し、どのように向き合うべきかを説明しています。 この本を読んだことで「不確実性をいかに最小化するか」、「不確実性を受け入れたまま、いかに前に進めるか」という視点を知ることができました。 プロジェクトを進める中で、「ここが分からないので進めません」「経験がないから難しいです」といったメンバーからの相談を受けることがあります。 本書を読んだことで、未知の領域に対しても「次に進むための構造的な視点」を持って向き合えるようになりました。 現代はAIの進化により、実装方法(How)に頭を悩ませるシーンが劇的に減りました。今私たちが集中すべきなのは、「どのような課題を解決し、どのような価値を創るのか」という本質的な問いです。 このような時代だからこそ、「未知を既知に変えていくプロセス」そのものの重要性が増しています。自分の知らない領域に飛び込むことを恐れず、仮説を持って挑戦し続ける。このマインドセットこそが、技術力以上に求められる現代の重要なソフトスキルではないでしょうか。 本書は、組織論の解説書ではなく、不確実なこのAI時代でエンジニアとして、もしくはリーダーとして「いかに思考し、行動するか」のマインドセットの下地を与えてくれる一冊です。 現状に閉塞感を感じている方や、新しい挑戦に踏み出す勇気が欲しい方に、ぜひ手に取っていただきたいです。 デザインの伝え方 はじめまして、プロダクトデザイナーの hikky です。 最近はコードを書く機会も増えてきたので、エンジニアによる書籍紹介に混ぜてもらいました🐢 私が紹介するのは、オライリー「 デザインの伝え方 」です。 www.oreilly.co.jp AIエージェントの発展でデザイン領域に踏み込むエンジニアの方も増えてきましたが、デザインに直接関わらない場合でも、自分が書いたコードに承認をもらう場面は誰にでもあると思います。そこで切っても切り離せないのが「コミュニケーション」です。本書はその根っこにある考え方を学べる一冊となっています。 本書が教えてくれることはシンプルで、コミュニケーションにおいて「 聞く・伝える・信頼を築く 」がいかに大事か、ということです。文字にすれば当たり前なのですが、その当たり前が一番難しい。 たとえば「聞く」一つとっても、相手に「ちゃんと自分の話を聞いてくれている」と感じてもらえているか、スムーズに本音を引き出せているか、といった観点があります。「伝える」についても、専門用語は同じ知識を持つ人同士では効率の良い言葉ですが、そうでない相手にはノイズになり得ます。立場の違う相手にどう届けるかという視点が必要なのです。 本書を通じて、エンジニアやビジネスサイドのメンバーは自分と違う立場で物事を見ており、それを前提に意図を汲み取ろうというマインドが強くなりました。違うからこそ、お互いを尊重して歩み寄ることがコミュニケーションには欠かせません。AIがどれだけ進化しても、人と人との対話は変わらず残り続けます。 他職種のメンバーともっとうまく連携して、良いものを届けていきたいと感じている方は、ぜひ手に取ってみてください! 書くスキルも設計スキルも飛躍的に上がる! プログラムを読む技術 GW中は首を痛めて、左をほとんど向けなかった、とんとんぼです。 私は最近、「 書くスキルも設計スキルも飛躍的に上がる! プログラムを読む技術 」という書籍を読んでいました。 bookplus.nikkei.com この本は2024年に発売され、当時も一度手に取ったのですが、最近のAIの普及などを受け、改めて読み返してみることにしました。 多くのプログラミング書は「いかにコードを書くか(例:〇〇の実装方法、XX実践入門など)」に焦点を当てたものがほとんどで、「コードの読み方」に特化した本は稀に思えます。しかし、実際の業務ではコードを書くよりも読む時間のほうが圧倒的に長く、比重も大きいのが現実です。さらに、近年では AI がコードを生成してくれるようになったため、提示されたコードの正誤や意図を正しく理解する力はこれまで以上に重要なスキルになっていると感じています。 この本の優れた点は、「理論」と「実践」が明確に分けられていることです。 前半では、コードを読む際の視点や意識すべきポイントについて理論と少しのサンプルコードから学び、後半では、そこで得た知識を活かして実際にさまざまなコードを読み解いていく構成になっています。 サンプルコードには、Python が採用されているため、読みやすく、実際の仕事でのコードでも実践しやすいのも魅力です。 最後に 最後までお付き合いいただきありがとうございました! それぞれ異なる職種のメンバーによる選書はいかがでしたでしょうか? 記事をまとめていて、自分自身も手を伸ばしてみたくなる書籍がありました。 AIで簡単に情報が手に入る時代ですが、本ならではの説得力や納得感を今回改めて感じました。 もし気になる書籍がありましたらぜひ読んでみてください! herp.careers
Goのtime.Nowとは? 〜synctestを添えて〜 はじめに エブリーでエンジニアをやっております、 赤川 です。食事管理アプリ ヘルシカ の開発を通じてGoを嗜んでいます。 ダイエット・食事管理・体重管理・カロリー計算 - ヘルシカ every, Inc. ヘルスケア/フィットネス 無料 ふと、以下のコードを見て、「Goにおける現在時刻ってなんなんだ…?」となりました。 now := time.Now() OSから取って来ているのは既知とした上で、Goのコードでそれをどのような形で扱っているのか、synctestの仮想時刻を返す挙動がどのように実装されているのかなど、いろいろ気になったのでコードを追っていこうと思います。 本記事で話すこと Goの時刻の扱い方 Goの現在時刻の取得の実装(ある程度高レイヤーの部分のみ) 本記事で話さないこと 他プログラミング言語の現在時刻の取得方法との違い プラットフォーム別実装など低レイヤーの詳細 time.Now の戻り値の作られ方 ウォールクロックとモノトニッククロック まず、OSが提供する時刻ソースには、大きく2種類あります。 種類 内容 特徴 ウォールクロック 現実で扱われている時刻。OSはUNIXエポック(1970-01-01 UTC)からの経過秒数として返すことが多い NTP補正・サマータイム・手動変更で 過去に巻き戻ることがある モノトニッククロック マシン起動などを起点とした、単調増加するカウンタ 必ず単調増加。日付としての意味は持たない これらの扱いについて、 time パッケージの公式ドキュメント に方針が書かれています。 Operating systems provide both a “wall clock,” which is subject to changes for clock synchronization, and a “monotonic clock,” which is not. The general rule is that the wall clock is for telling time and the monotonic clock is for measuring time. Rather than split the API, in this package the Time returned by time.Now contains both a wall clock reading and a monotonic clock reading; later time-telling operations use the wall clock reading, but later time-measuring operations, specifically comparisons and subtractions, use the monotonic clock reading. OSは「ウォールクロック」と「モノトニッククロック」の2つを提供している。ウォールクロックはクロック同期のために変更されうるが、モノトニッククロックは変更されない。一般的なルールとして、ウォールクロックは時刻を知るため(telling time)に、モノトニッククロックは時間を測るため(measuring time)に使う。本パッケージではAPIを分けるのではなく、 time.Now が返す Time にウォールクロックとモノトニッククロックの両方の読み取り値を含めることにしている。以降の「時刻を知る」操作はウォールクロックの値を、「時間を測る」操作(具体的には比較と差分)はモノトニッククロックの値を使う。 time.Timeの構造 「ウォール/モノトニックの両方を1つの Time で扱う」という方針を踏まえて、 src/time/time.go#L140-L161 で定義されている time.Time の構造を見てみましょう。 type Time struct { // wall and ext encode the wall time seconds, wall time nanoseconds, // and optional monotonic clock reading in nanoseconds. // // From high to low bit position, wall encodes a 1-bit flag (hasMonotonic), // a 33-bit seconds field, and a 30-bit wall time nanoseconds field. // The nanoseconds field is in the range [0, 999999999]. // If the hasMonotonic bit is 0, then the 33-bit field must be zero // and the full signed 64-bit wall seconds since Jan 1 year 1 is stored in ext. // If the hasMonotonic bit is 1, then the 33-bit field holds a 33-bit // unsigned wall seconds since Jan 1 year 1885, and ext holds a // signed 64-bit monotonic clock reading, nanoseconds since process start. wall uint64 ext int64 // loc specifies the Location that should be used to // determine the minute, hour, month, day, and year // that correspond to this Time. // The nil location means UTC. // All UTC times are represented with loc==nil, never loc==&utcLoc. loc *Location } 中身は wall 内に持っている hasMonotonic フラグによって2パターンに切り替わります。 hasMonotonic = 1 (ウォール+モノトニック) hasMonotonic = 0 (ウォールのみ) wall フラグ(1) + ウォール秒(33bit, 1885年起点) + ウォールナノ秒(30bit) フラグ(0) + 33bit秒は未使用(0) + ウォールナノ秒(30bit) ext モノトニッククロック値 (プロセス起動からのナノ秒) ウォール秒 (西暦1年起点の符号付き64bit) loc タイムゾーン タイムゾーン ext が hasMonotonic で意味を切り替えるようになっているのは、 wall の33bit秒(1885年起点)だと 1885〜2157年の約272年分 しか表現できないからです。 time.Date(1500, ...) のような範囲外の時刻を扱う hasMonotonic = 0 のケースでは、ウォール秒を wall の33bitから ext (int64, 西暦1年起点) に移してより広い範囲をカバーします。これは Time のサイズを増やさずに「モノトニック付き」と「広い時刻範囲」を両立させるための工夫です。次節の time.Now 実装の中にも、以下のように33bit上限に言及するコメントが出てきます。 // This will be true after March 16, 2157. time.Now 本体の実装 ここまで把握した上で、 src/time/time.go#L1347-L1361 にある time.Now の実装を見ていきます(Go 1.26.3 現在)。 // Now returns the current local time. func Now() Time { sec, nsec, mono := runtimeNow() if mono == 0 { return Time{ uint64 (nsec), sec + unixToInternal, Local} } mono -= startNano sec += unixToInternal - minWall if uint64 (sec)>> 33 != 0 { // Seconds field overflowed the 33 bits available when // storing a monotonic time. This will be true after // March 16, 2157. return Time{ uint64 (nsec), sec + minWall, Local} } return Time{hasMonotonic | uint64 (sec)<<nsecShift | uint64 (nsec), mono, Local} } コードに出てくる定数は同じく src/time/time.go の L163-L169 ( hasMonotonic など)と L535-L568 ( unixToInternal など)、および L1341 ( startNano )に以下のように定義されています。(簡単のため一部省略して記述しています) const ( secondsPerDay = 24 * 60 * 60 // 西暦1年1月1日 〜 UNIXエポック(1970-01-01) の秒数 unixToInternal int64 = ( 1969 * 365 + 1969 / 4 - 1969 / 100 + 1969 / 400 ) * secondsPerDay // 西暦1年1月1日 〜 1885年1月1日 の秒数 wallToInternal int64 = ( 1884 * 365 + 1884 / 4 - 1884 / 100 + 1884 / 400 ) * secondsPerDay ) const ( hasMonotonic = 1 << 63 // wall の最上位bitに立てるフラグ minWall = wallToInternal // wall の33bit秒の起点(= 1885年) nsecShift = 30 // wall に秒を詰めるときのシフト量 ) // プロセス起動時点のモノトニック値(runtime 初期化時にセットされる) var startNano int64 (1969*365 + 1969/4 - 1969/100 + 1969/400) の式は閏年を考慮してその年までの日数を計算しています。これに secondsPerDay を掛けることで「西暦1年1月1日からその年までの 秒数 」になります。 unixToInternal は1970年、 wallToInternal は1885年までのそれにあたります。 役割を整理すると以下のとおりです。 定数 役割 unixToInternal OSが返すUNIX秒を 「西暦1年起点」 に変換するオフセット minWall (= wallToInternal ) 西暦1年起点を 「1885年起点」 にずらすオフセット( wall の33bit秒の基準合わせ) nsecShift wall に秒を詰めるとき左に30bitシフトしてナノ秒の場所を空ける hasMonotonic モノトニックが入っているかのフラグ( wall の最上位bit) startNano プロセス起動時点のモノトニック値。これを引くことで ext を 「プロセス起動からの経過ns」 に正規化する Local loc に入れるデフォルトのタイムゾーン( *Location ) これらを踏まえてもう一度 time.Now を読み直すと、3パターンで Time を組み立てていることがわかります。 func Now() Time { // OSから現在のウォール秒・ナノ秒・モノトニック値を取得 sec, nsec, mono := runtimeNow() // 【パターン1】モノトニッククロックが取れなかった環境 // → ウォールクロックだけを ext に入れて返す(hasMonotonic = 0 のレイアウト) if mono == 0 { // sec(UNIX秒) + unixToInternal で「西暦1年起点の秒」に変換し、ext に詰める return Time{ uint64 (nsec), sec + unixToInternal, Local} } // 以降は mono あり mono -= startNano // モノトニックを「プロセス起動からの経過ns」に正規化 sec += unixToInternal - minWall // sec を「1885年起点の秒」に変換(33bit領域に詰める準備) // 【パターン2】33bitに収まらない(= 2157年3月16日以降) // → モノトニックを諦めて、ウォール秒は ext の方に置く(hasMonotonic = 0 のレイアウト) if uint64 (sec)>> 33 != 0 { // sec はいま1885年起点。minWall を足し戻して「西暦1年起点」に戻してから ext へ return Time{ uint64 (nsec), sec + minWall, Local} } // 【パターン3】通常パス // → hasMonotonic フラグを立て、ウォール・モノトニック両方を wall / ext に詰める // - sec は1885年起点のまま wall の33bit領域へ(<< nsecShift でナノ秒の場所を空けて | で合成) // - mono は正規化済みの値をそのまま ext へ return Time{hasMonotonic | uint64 (sec)<<nsecShift | uint64 (nsec), mono, Local} } runtimeNow() の中身 runtimeNow() は time パッケージ側ではシグネチャだけ書かれており、実体は src/runtime/time.go#L16-L31 にあります。 //go:linkname time_runtimeNow time.runtimeNow func time_runtimeNow() (sec int64 , nsec int32 , mono int64 ) { if bubble := getg().bubble; bubble != nil { sec = bubble.now / ( 1000 * 1000 * 1000 ) nsec = int32 (bubble.now % ( 1000 * 1000 * 1000 )) // Don't return a monotonic time inside a synctest bubble. // If we return a monotonic time based on the fake clock, // arithmetic on times created inside/outside bubbles is confusing. // If we return a monotonic time based on the real monotonic clock, // arithmetic on times created in the same bubble is confusing. // Simplest is to omit the monotonic time within a bubble. return sec, nsec, 0 } return time_now() } 分岐は2つあります。 synctest bubble の分岐 : 実行中のゴルーチンが synctest のバブル内にいる場合、バブルの仮想時刻 bubble.now を ウォール秒・ナノ秒として返し 、モノトニックは 0 を返します。 通常の分岐 : time_now() を呼び出します。これの実体はプラットフォーム別に実装されており、最終的にはどれもOSが提供する時刻取得APIを叩いています。 time_now() の実装は低レイヤーに近い話になるので今回は触れません。 bubble.now は、 src/runtime/synctest.go#L186-L187 の synctestRun で初期化されます。 const synctestBaseTime = 946684800000000000 // midnight UTC 2000-01-01 bubble.now = synctestBaseTime モノトニックを使わない理由については、コメントに書かれています。以下、和訳です。 synctestバブル内ではモノトニック時刻を返さないようにする。 フェイククロックに基づいたモノトニック時刻を返してしまうと、バブル内で作った時刻とバブル外で作った時刻のあいだでの計算結果が紛らわしくなる。 一方で実モノトニッククロックに基づいたモノトニック時刻を返しても、同じバブル内で作った時刻同士の計算が紛らわしくなる。 もっともシンプルな解は、バブル内ではモノトニック時刻を省略することだ。 モノトニッククロックはマシン起動を起点とするものなので、ウォールクロックだけ仮想時刻を進めても両者の値が食い違ってしまいます。一方で synctestBaseTime を起点にしたモノトニック値を別途用意するという選択肢もありますが、その場合もバブル外で取得した Time との差分計算で実時間とバブル内仮想時間が混在してしまいます。これらを避けるために、バブル内ではウォールクロックのみを扱う実装になっている、ということですね。 まとめ time.Now() の経路は以下のようになっていることがわかりました。 time.Now() │ │ ① (sec, nsec, mono) を取得 └── time.runtimeNow() ── linkname ──→ runtime.time_runtimeNow() │ ├── synctest bubble 内 │ sec = bubble.now / 1e9 (バブル仮想時刻) │ nsec = bubble.now % 1e9 │ mono = 0 (バブル内ではmonoを返さない) │ └── 通常経路 runtime.time_now() (プラットフォーム別実装) └─ OSが提供する時刻取得APIを呼ぶ │ │ ② 受け取った値を Time{wall, ext, loc} に組み立てて返す │ ├── mono == 0 → hasMonotonic=0, ext に「西暦1年起点ウォール秒」 ├── 通常 (33bit以内) → hasMonotonic=1, wall=フラグ|33bit秒(1885年起点)|30bitナノ秒, ext=mono(プロセス起動からのns) └── 33bit溢れ (2157年以降) → hasMonotonic=0, ext に「西暦1年起点ウォール秒」 Goが扱う時刻の構造とその設計理由、synctestの分岐実装、そしてウォールとモノトニックを巧みに組み合わせる工夫を知ることができ、とても満足しています。プラットフォームごとの実装は、まずは自分が使っているarm64から見てみたいと思います。 最後までお読みいただきありがとうございました! 参考 Package time - pkg.go.dev Package testing/synctest - pkg.go.dev Go: src/time/time.go Go: src/runtime/time.go Go: src/runtime/synctest.go Go 1.9 Release Notes - Monotonic Clocks Proposal: Monotonic Elapsed Time Measurements in Go
はじめに こんにちは、カート決済部カート決済サービスAブロックの 道場 です。ZOZOTOWN内のカート機能や決済機能の開発、保守運用を担当しています。 現在、ZOZOTOWNのカート決済画面はリプレイスが進行中です。既存システムとリプレイス後のシステムが並行して開発される中、既存システムへのさまざまな機能改修を、リプレイス側にも取り込む必要があります。その際、条件の組み合わせが膨大になるテストを手動で網羅的に実施することが現実的でなく、特に注文金額の計算結果の正確性を人間が1件ずつ確認するには大きなコストがかかっていました。 本記事では、Claude CodeとPlaywright CLIを組み合わせて、自然言語によるE2Eテストを自動化した仕組みをご紹介します。Confluence(Atlassian社が提供するナレッジ共有ツール)に自然言語でテスト手順を記述することでAIが自律的にブラウザを操作し、計算検証も含めてE2Eテストを完結させています。コードを書かずにテストを作成・実行できるため、テスト自動化の属人化解消にもつながりました。 目次 はじめに 目次 背景・課題 リプレイスに伴う二重開発とテストの課題 なぜ従来のE2E自動化では足りなかったのか AIエージェント駆動のE2Eテストシステム 全体アーキテクチャ Playwright CLIによるブラウザ操作 Agent Skillsによる操作手順の定義 テストケースの設計と期待値の保証 Confluenceベースのテストケース管理 計算が必要なテストの期待値保証 テスト実行の6つのStep テスト支援ツールの構築 atlassian-cli:Confluence操作のCLI zozo-sql-server-cli:SQL Serverクエリ実行CLI AIエージェントが必要なツールを自ら作る 従来のテスト自動化との比較 実践から得られた知見 テストケースの実績 実践を通じた気づき まとめ 背景・課題 リプレイスに伴う二重開発とテストの課題 冒頭の通り、ZOZOTOWNのカート決済画面ではリプレイスが進行中です。既存システムとリプレイス後のシステムが並行して動作する期間中、既存システムに対するさまざまな機能改修をリプレイス側へ取り込む必要があります。 これらの改修をすべて取り込み、条件の組み合わせが爆発的に増加するテストケースを検証する工数が大きな課題となりました。 たとえば、ある案件の機能を取り込む場合、以下のような因子が絡み合います。 ユーザーの属性(性別・年齢 等) 購入商品の種類・金額 割引・クーポンの有無 ポイント利用の有無 キャンペーン期間の内外 これらを組み合わせると、1つの案件だけで 100件以上のテストケース が発生することもありました。さらに、各テストケースでは 注文フローの複数画面 (配送・支払い選択、注文の確認 等)で表示値の確認が必要です。そして、 PC用の画面とスマートフォン(以下、SPと表記します)用の画面がそれぞれ存在 するため、検証量は実質的にさらに倍になります。 カート決済画面では、注文金額の計算ロジックにさまざまな要素が関わっており、前述の通り案件ごとに条件の組み合わせが大きくなりがちでした。さらに、期待値は複雑な計算式で決まるため、人間が1件ずつ手計算したうえで画面の表示と照合するには多くの時間がかかっていました。 なぜ従来のE2E自動化では足りなかったのか ZOZOTOWNでは、手動テストに加えて品質管理部によるコードベースのE2E自動テストも活用しています。しかし、そのような従来のコード記述型の自動テストを使ったアプローチでは以下の課題がありました。 プログラミングスキルへの依存 :CSSセレクタやロールを使った要素特定のコードを書く必要があるため、開発者でなければ作成・保守が難しい UI変更への追従コスト :UIの変更に応じて、要素特定の方法やテスト内容のメンテナンスが必要になる テストコードの属人化 :記述・保守できる人が限られるため、特定の開発者への依存が生じる 実現したかったのは、 テスト手順を自然言語で書くだけで、AIが要素を自動で見つけて操作し、計算検証まで完結する仕組み です。そのためのアプローチとして、Claude CodeのAgent SkillsとPlaywright CLIを組み合わせた自動化システムを構築しました。 AIエージェント駆動のE2Eテストシステム 全体アーキテクチャ 構築したシステムの全体像は以下の通りです。 各コンポーネントの役割は次の通りです。 コンポーネント 役割 Confluenceページ テストデータ・手順・期待値を自然言語で記載したテストケース管理の場 エージェント ( zozotown-qa-tester ) テストの実行フローを定義するClaude Codeエージェント Agent Skills ZOZOTOWNの操作手順やCLIの使い方をMarkdownで定義した再利用可能なリファレンス 計算サービス(TypeScript) 期待値を算出するための計算ロジック実装 Playwright CLI コマンドでブラウザを操作するCLIツール atlassian-cli Confluenceの読み取りと、エビデンスを含めた結果の記載を行う自作CLI zozo-sql-server-cli SQL Serverへのクエリ実行と結果の画像化を行う自作CLI Claude CodeのエージェントがConfluenceからテストケースを読み取ります。Agent Skillsを参照しながらPlaywright CLIでブラウザを操作し、結果をConfluenceに書き戻します。 Playwright CLIによるブラウザ操作 Playwright CLI は、ブラウザ操作をコマンドで実行できるCLIツールです。テストコードを書く代わりに、コマンド1つでブラウザを操作できます。Playwright MCPもありますが、CLIの方がトークン使用量を節約できるため選択しています。 特徴的なのは スナップショット機能 です。ページを開くと、Playwright CLIはページの構造をYAML形式で取得します。このとき各要素には ref 番号が付与されています。AIはこのスナップショットを読んで要素を特定し、 ref 番号を使って操作します。 # ref番号を使って要素をクリック playwright-cli click e42 --session = pc # テキストを入力 playwright-cli fill e15 " test@example.com " --session = pc # スクリーンショットを取得 playwright-cli screenshot --output screenshots/cart-top.png --session = pc CSSセレクタやロールを明示的に指定しなくても、AIがスナップショットを解釈して要素を特定できます。そのため、セレクタベースの実装に比べると、軽微なUI変更には追従しやすくなります。 PCとSPの切り替えは設定ファイルで行います。 // playwright-cli.json(PC用) { " browser ": { " launchOptions ": { " headless ": false } , " isolated ": false , " contextOptions ": { " viewport ": { " width ": 1400 , " height ": 1080 } } } } // playwright-cli-sp.json(SP用) { " browser ": { " launchOptions ": { " headless ": false } , " isolated ": false , " contextOptions ": { " viewport ": { " width ": 430 , " height ": 932 } , " userAgent ": " Mozilla/5.0 (iPhone; ...) Safari/604.1 ", " isMobile ": true , " hasTouch ": true } } } PCテストとSPテストは 別セッションで同時に実行できる ため、テスト時間の短縮にも貢献します。 Agent Skillsによる操作手順の定義 Agent Skillsでは、Claude CodeのSkill機能を活用してZOZOTOWN固有の操作手順を定義しています。コードベースのPlaywrightにおけるPage Object Modelに相当する役割を、Markdownによる自然言語の手順書で担うイメージです。 操作手順は次のように自然言語で記述します。 # ログイン手順リファレンス ## 手順 1. 以下のページを開く - PC: ` /_member/login.html ` - SP: ` /sp/_member/login.html ` 2. ` メールアドレス ` 入力欄にメールアドレスを入力する。 3. ` パスワード ` 入力欄にパスワードを入力する。 4. ` ログイン ` ボタンをクリックする。 テストケースに「テストユーザーAのアカウントでログインする」と書けば、エージェントがこのリファレンスを参照して手順を実行します。操作をリファレンスとして標準化しておくことで、 誰が書いたテストケースでも同じ操作が再現できます 。 今回定義した主要なリファレンスは次の通りです。 login-flow.md :ログイン手順(PC / SP対応) add-to-cart-flow.md :商品をカートへ投入する手順 order-flow.md :注文フロー(カートTOP → 配送・支払い選択 → 注文確認 → 注文完了) sql-execution-flow.md :SQL Serverへのクエリ実行手順 テストケースの設計と期待値の保証 Confluenceベースのテストケース管理 テストケースはConfluenceページで管理しています。ページの構成は次の通りです。 セクション 内容 要件 テスト対象の機能仕様 因子と水準 テストに関わる条件の洗い出し(ホワイトボックス観点) デシジョンテーブル 条件の組み合わせパターン テストデータ 環境URL、ユーザー情報、商品情報 テストケース 手順、パラメータ、期待値、実行結果、エビデンス テスト実行後は、Claude Codeがこのページに結果(OK / NG)とスクリーンショットを自動で書き込みます。 実際に実施したテストケースの例を紹介します。 注文金額に関わる計算ロジックの検証テスト :注文の確認画面に表示される金額が、計算サービスの算出結果と一致することを検証します。前述の因子を組み合わせた数十件のパターンを定義しています。 テストの手順は、Confluenceページに次のように自然言語で記述されています。 1. カートを空にする 2. パラメータ(商品)に記載されている商品をカートに入れる 3. 注文へ進み、パラメータ(支払い方法)の支払い方法を選択して注文確認画面を表示する 4. 表示されている計算結果の値が OrderAmountCalculationService.getの値と 一致していることを確認する 5. viewportのスクリーンショットを取得する 6. パラメータ(ポイント利用)に記載のポイントを利用する 7. 表示されている計算結果の値が上記計算サービスの値と一致していることを確認する ... この手順をClaude Codeが読み取り、Agent Skillsを参照しながらブラウザを操作します。 計算が必要なテストの期待値保証 計算結果の検証は、今回の取り組みで最も重要なポイントです。 課題 :注文金額に関わる複雑な計算結果を、人間が手計算して期待値と照合するには大きな工数が必要です。特に、割引・クーポン・ポイント利用・税率が絡み合う計算は、ミスが発生しやすく時間もかかっていました。 解決策 :Playwrightテスト用リポジトリにTypeScriptで計算サービスを実装し、あらかじめ期待値を算出しておきます。Claude Codeはテスト計画の作成時に計算サービスを呼び出し、期待値をプランに出力してから、ブラウザの表示値と照合します。 // ZOZOCARD還元ポイントを計算するクラス export class ZozocardRewardPointCalculationService { private static readonly POINT_RETURN_RATE = 0.05 ; public get ( goodsPriceWithoutTax : number , quantity : number , taxRate : number ): number { // ZOZOCARD 還元ポイントの計算処理... } } この計算処理は、システムと同じ仕様をもとにClaude Codeで生成した 独立した実装 になっています。システム側の実装コードをそのまま流用すると、同じバグを共有してしまいます。仕様を別実装することで、 システム側とテスト側の独立性 を保っています。これにより、期待値とシステムの表示値を照合したときに、単なる一貫性チェックではなく、システム側の実装が仕様どおりかを検証できます。実際に、このテストを通じてシステム側の実装が仕様を正しく考慮できていないケースを検知できた事例もありました。 期待値の検証フローは次の通りです。 Claude Codeはテスト計画を作成する段階で計算サービスを実行し、全テストケースの期待値を事前に算出します。テスト実行時には、ブラウザで取得した表示値と事前に算出した期待値を照合します。 テスト実行の6つのStep エージェント定義ファイル( zozotown-qa-tester.md )では、テスト実行を次の6つのStepで定義しています。 --- name: zozotown-qa-tester description: ZOZOTOWN の QA テストを実行するエージェント skills: - playwright-cli - zozotown-operations - confluence-page-operations - atlassian-cli - zozo-sql-server-cli --- ## テスト実行フロー ### 1. テストケースの確認 Confluenceページからテストケースを取得し、 対象の開発環境・前提条件・手順・期待結果を読み取る。 ### 2. テストケースプランの作成 テストデータ・期待値(計算サービスの実行結果)・実行手順を整理し、 ` test-plans/ ` ディレクトリにMarkdownファイルとして出力する。 **ユーザーの承認を得てからテスト実行に進む。** ### 3. テスト準備 ブラウザを起動し、ログインや初期データのセットアップを行う。 ### 4. テスト実行 各ステップを ` zozotown-operations ` のリファレンスに従って実行する。 手順が定義されていない操作は、実際にブラウザで確認して新しいリファレンスを作成する。 ### 5. 結果の記録 実行結果(OK / NG)を判定し、スクリーンショットを撮影して Confluenceページに結果とエビデンスを書き込む。 ### 6. 結果の報告 ユーザーに実行結果のサマリを報告する。 特に重要なのは Step 2のテストケースプランの作成とユーザー承認 です。AIは非決定的に動作するため、テストケースの解釈が意図と異なる可能性があります。実行前に計画を提示してユーザーに確認することで、 解釈のズレを事前に検出 できます。 また、Step 4の「リファレンスに手順がない操作は自ら作成する」という仕組みにより、エージェントが新しい操作手順を発見するたびにリファレンスファイルが自動的に追加されていきます。使うほどにリファレンスが充実し、テスト作成が楽になっていく仕組みです。 実際のテスト実行では、テスト計画の確認とPC / SPセッションの並列実行をターミナル上で確認できます。 テスト支援ツールの構築 atlassian-cli:Confluence操作のCLI Confluenceのテストケースページを詳細に処理するため、atlassian-cliを作成しました。Atlassian MCPもありますが、スクリーンショットを添付できないため、REST APIをラップしたCLIです。 テスト実行フローでの使用例を示します。 # Confluence のテストケースページを取得 atlassian-cli confluence get-page 348678105 --body-format atlas_doc_format # テスト結果のスクリーンショットをアップロード atlassian-cli confluence upload-attachment 348678105 \ --file ./screenshots/confirm-pc.png # テスト結果をページに追記 atlassian-cli confluence update-page 348678105 \ --body-file ./test-results/result.json \ --page-version 41 zozo-sql-server-cli:SQL Serverクエリ実行CLI 注文完了後のDBデータを検証するため、zozo-sql-server-cliも作成しました。注文データが正しく保存されているかをSQLで確認し、 結果をHTMLテーブルとして描画してPuppeteerでスクリーンショット化 する機能が特徴です。 # SQL クエリを実行してテーブル形式で表示 zozo-sql-server-cli \ " SELECT total_amount, discount_amount FROM orders WHERE order_id = 12345 " # クエリ結果をスクリーンショット(HTMLテーブルとして描画)として保存 zozo-sql-server-cli \ " SELECT total_amount, discount_amount FROM orders WHERE order_id = 12345 " \ --screenshot ./screenshots/order-db.png このスクリーンショットをそのままConfluenceのエビデンスとして添付することで、DB検証の証跡も自動的に記録できます。 AIエージェントが必要なツールを自ら作る atlassian-cliとzozo-sql-server-cliは、いずれもClaude Codeを活用して作成しました。 テスト自動化を進める中で「Confluenceにスクリーンショットを添付したい」「DBの検証結果を画像として保存したい」といったニーズが生まれました。これらをCLIとしてClaude Codeに実装してもらい、短期間で必要な機能を揃えることができました。 AIエージェントに必要なツールをAI自身が作れる という点は、自動化のエコシステムを大幅に加速させます。 従来のテスト自動化との比較 従来のコードベースのE2E自動テストと、今回構築したClaude Code + Playwright CLIのアプローチを比較します。 観点 コードベースのPlaywright Claude Code + Playwright CLI テストケースの形式 TypeScript / JavaScriptコード Confluenceページ(自然言語) 要素の特定方法 CSSセレクタ / ロール スナップショットのref番号(AIが自動特定) 期待値の検証 ハードコードされたアサーション 計算サービス + AIによる照合 UI変更への耐性 低い(セレクタ・ロールの変更対応が必要) 高い(スナップショットベースで柔軟に対応) 作成に必要なスキル プログラミング ドメイン知識 + 自然言語 最も大きな違いは、 テストコードの記述・保守スキルがなくてもE2Eテストを作成・実行できる 点です。 Confluenceでテスト手順を書く際には「テストユーザーAでログインする」「XXXの商品をカートに入れる」といった日常的な言葉で記述できます。Agent Skillsのリファレンスにログインやカート投入の手順が定義されているため、この自然言語の指示だけでAIが正確に操作を再現します。 また、計算検証の自動化により、人手では高コストだった期待値照合をAIが実行できるようになりました。開発者は別の案件の開発を進めながら、Claude Codeにテストを並行して実行させることができます。 実践から得られた知見 テストケースの実績 実際に実施したテストの実績は次の通りです。 テスト対象 テストケース数 対象画面 プラットフォーム 案件A(計算ロジックの検証) およそ20件 注文フローの各画面 PC / SP 案件B(条件の組み合わせ検証) およそ50件 注文フローの各画面 PC / SP 手動でのフローと、今回構築したAIエージェント活用後のフローを比較すると次のようになります。 人が行うのは、テスト計画のレビューのみです。数十件のテストケース × 複数ページ × PC / SPの全テストをClaude Codeに任せられました。案件Aでは詳細な計算結果を、案件Bでは肥大化する条件の組み合わせを検証でき、人手による手計算や確認にかかる工数を大きく減らせました。 実践を通じた気づき Agent Skillsの粒度設計 :ログインやカート投入、注文フローというような1つの手順として指示する粒度がちょうどよく、再利用しやすいです。細かすぎるとリファレンスが増えすぎて管理が難しくなり、粗すぎると他のテストケースで使いにくくなります。 テスト計画承認フローの効果 :「2. テストケースプランの作成」でAIが作成した計画をレビューすることで、テストケースの解釈ミスを事前に検出できた事例がありました。コーディング時もそうですが、私はClaude Codeのプランモードをよく利用します。何をするかを綿密に考えさせたものを自分が確認することで、あとはそれを実行するだけになり、質が高くなると感じています。 自己改善するリファレンス :未定義の操作に遭遇した際、エージェントが実際にブラウザで操作して手順を確認し、新しいリファレンスファイルを自動作成する仕組みは実用的でした。テストを重ねるほどリファレンスが充実し、環境を育てていくことで後のテスト作成が楽になっていきます。 まとめ 本記事では、Claude CodeとPlaywright CLIを組み合わせた自然言語E2Eテストの構築と実践をご紹介しました。Confluenceに自然言語でテスト手順を記述するだけでAIが自律的にブラウザを操作し、計算検証も含めてE2Eテストを完結させることができました。 膨大な組み合わせテストの自動化・計算検証の正確性担保・テスト自動化の属人化解消という課題を同時に解決し、開発者が別の案件を進めながらテストを並行完了できる体制が実現しました。今後は他の画面への展開や、定期的な実行によるリグレッションの検知などを検討していきたいと考えています。 ZOZOでは、一緒にサービスを作り上げてくださる方を募集中です。ご興味のある方は、ぜひ採用ページをご覧ください。 corp.zozo.com


























