Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                

Tabelog Tech Blog

食べログの開発者による技術ブログです

テスト自動化への心理的障壁がエベレストなSET1年生が記述レベルMAXな自動テストスクリプトを書けるわけがない

はじめに

 課金戦士は恐怖した。必ず、テスト自動化の実装をできるようにならなければならぬと決意した。課金戦士にはコードがわからぬ。課金戦士は、QAエンジニアである。テストケースを作成し、テスト環境に弄ばれて暮して来た。けれどもテスト自動化という未知に対しては、人一倍に敏感であった。


 テスト自動化への心理的障壁がエベレストなあなたも、日和山なあなたも、はじめまして。
 食べログの品質管理室、SETチームに生息している課金戦士と申します(課金機能を担当している戦士ではなく、アプリゲーム課金を趣味として赤字と戦い続ける戦士です)。

 ちなみにSETとは、「Software Engineer in Test」のことを指します。つまりSETチームとは、ソフトウェアの品質保証とテスト自動化に特化したエンジニアのチームです。
 課金戦士は、SETチームの一員として、約1年半テスト自動化に携わってきました。

 今回の物語は、テスト自動化への心理的障壁がエベレストな課金戦士が、「記述レベル」という概念を知るところから始まります。

 この記事では、以下について、システムテスト自動化 標準ガイドを参考に、課金戦士による個性溢れる説明を記載します(Fewster & Graham, 2014)1
1. 記述レベル
2. 記述レベルを使いこなすための鍵であるテスト条件

 ※この記事には主にガチャや課金等のゲームをテーマにしたコードが登場しますが、課金戦士の個人的な趣味が反映された結果生まれたものです。食べログではガチャ機能は提供していません。

目次

SET1年生、入学時のステータス

 手動QAとしての経験だけがある課金戦士。
 なんと実務としてコードを見たり触ったりしたことがないままテスト自動化に携わることになって──!?

 上記のようなあらすじから、食べログでの課金戦士物語が始まりました。
 当時の絶望的なステータスを表現してみたため、興味がある方は当時の課金戦士と一緒に頭を抱えていただければと思います。

SET1年生の入学時のステータス

課金戦士でも分かる!記述レベルの概要

 ここでは、「記述レベル」の概要について簡単に説明します。
 「記述レベル」とは、「自動テストの保守性の高さを示すレベル」です。個人的には、「自動テストのコードを変更した際の影響範囲が最小限になるように、構造が整理されているレベル」であると解釈しています。

 上段の表では、記述レベルについて、チームリーダーが真面目なカンファレンス向けに作成した表を参考に表現しています。
 下段の表では、課金戦士向けに記述レベルをとても簡単に表現してみました。

 これより先では、各記述レベルについて、以下の4点を紹介します。

  • 概要
  • 特徴
  • 各記述レベルに該当する自動テストスクリプト

記述レベルを正確に表した表

 ※上の表の参考元となった表は以下のリンク先の資料にあります。
 ▶︎インプロセスQAとテスト自動化の両輪で進める食べログの開発生産性と品質改善の3年間

記述レベルを簡単に表した表

記述レベル、完全に理解した〜課金戦士産の自動テストスクリプトを添えて〜

【Lv.1】線形スクリプト【保守性の高さ: ☆☆☆☆】

記述レベルを正確に表した表_レベル1

▶︎概要
 線形スクリプトは、構造化されていないスクリプトです。具体的には、実際の操作手順が一操作ずつ順に記述されるスクリプトを指します。

▶︎特徴
 ✅記述する処理がシンプルなため実装しやすい反面、保守性が低い

▶︎例
 例として、ガチャを300回実行したい場合でも、1回実行する処理を300行記述して実装することが挙げられます。
 実行している操作はシンプルで分かりやすいのですが、効率はとても悪いです......。

▶︎記述レベル1の自動テストスクリプト
 以下のコードは、上記の例として挙げた処理をそのまま記述したものです。

async function main(): Promise<void> {
 // ブラウザを起動〜ログイン
 // ...省略...

 // ガチャ画面に遷移する
 await driver.get(`https://game/gacha/`);

 // ガチャを1回実行する処理を300行記述
 await driver.findElement(By.xpath('//button[@type="button"][text()="1回"]')).click();
 await driver.findElement(By.xpath('//button[@type="button"][text()="1回"]')).click();
 await driver.findElement(By.xpath('//button[@type="button"][text()="1回"]')).click();
 // ... 省略 ...
 await driver.findElement(By.xpath('//button[@type="button"][text()="1回"]')).click();

 // ブラウザを終了する
 await driver.quit();
}

main();

▶︎記述レベル1のまとめ

記述レベル1のまとめ


【Lv.2】構造化スクリプト【保守性の高さ: ★☆☆☆】

記述レベルを正確に表した表_レベル2

▶︎概要
 構造化スクリプトは、制御フローが構造化されたスクリプトです。具体的には、「〜の場合は〜する」といったテスト実施者の判断が、if文やswitch文、for文やwhile文などの「制御構造」によって記述されるスクリプトのことを指します。

▶︎特徴
 ✅制御フローの構造化によって、条件分岐や繰り返し処理を表現できるようになる
 ✅複雑な制御が増えると理解が難しくなるという注意点があるものの、保守性はやや向上

▶︎例
 例として、条件(SSRを当てる)を満たすまで実施される繰り返し処理(ガチャ実行)が挙げられます。
 課金戦士の行動パターンから具体例を挙げるなら、「SSRが当たったら、幸福を享受してガチャから撤退する」「300回ガチャを実行したら、確定でSSRが当たる保証がされているのでガチャから撤退する」といった分岐処理を交えた繰り返し処理です。
 記述レベル1ではガチャ1回の実行処理を300行書く必要がありましたが、記述レベル2では数行で済みます。

▶︎記述レベル2の自動テストスクリプト
 以下のコードでは、課金戦士の行動パターンを記述してみました。

async function main(): Promise<void> {
// ブラウザを起動〜ログイン
 // ...省略...

 // ガチャ画面に遷移する
 await driver.get(`https://game/gacha/`);

 // ガチャを1回実行する処理を300回まで繰り返す
 for (let count = 1; count <= 300; count++) {
   // ガチャを実行する
   await driver.findElement(By.xpath('//button[@type="button"][text()="1回"]')).click();

   // SSRが当たったら、ガチャの繰り返し実行を終了する
   if (await driver.findElement(By.css(".SSR")).isDisplayed()) {
     console.log(`【速報】${count}回目でSSRが出ました。幸福を享受してガチャの繰り返し実行を終了しましょう。`);
     break;
   }
 }

 // ブラウザを終了する
 await driver.quit();
}

main();

▶︎記述レベル2のまとめ

記述レベル2のまとめ


【Lv.3】共有スクリプト【保守性の高さ: ★★☆☆】

記述レベルを正確に表した表_レベル3

▶︎概要
 共有スクリプトは、各テストケースの重複処理が共通化されたスクリプトです。
 処理を共通化することで、複数のテストケースで同じ手順を再利用できます。

▶︎特徴
 ✅重複処理の共通化によって、再利用性が向上する
 ✅冗長な重複処理の減少により、スクリプトの保守性が向上する

▶︎例
 例として、以下のような、ガチャ実行前の準備を共通化することが挙げられます。
 この場合、新たなガチャが追加される度に共通化した処理を再利用できるという点で、効率が良くなります。また、手順誤りや更新漏れから生じるようなミスも減ります。

  • ユーザーアカウントでログインする
  • 現在のガチャ石の数を確認する
  • 10連分のガチャ石がなければショップ画面に遷移して、ガチャ石を購入する
  • ガチャ画面に遷移する

▶︎記述レベル3の自動テストスクリプト
 以下のコードは、ユーザーアカウントでログインする処理を記述しています。
 レベル1とは異なり、ログインが必要な複数のシナリオそれぞれで同じ処理を記述せず、ログイン時にこの処理を再利用します。

async function login() {
  const loginPage = loginPage;
  const id = "kakin";
  const password = "senshi";
  await loginPage.inputAuthInformation(id, password);
  await loginPage.submitLogin();
}

▶︎記述レベル3のまとめ

記述レベル3のまとめ


【Lv.4】データ駆動スクリプト【保守性の高さ: ★★★☆】

記述レベルを正確に表した表_レベル4

▶︎概要
 データ駆動スクリプトは、テスト手順(シナリオのStep)とテストデータを分離​した​スクリプトです。​
 これまでの記述レベルにおけるスクリプトでは、テストデータ(例: ログインID・パスワード)が直書きされているという問題がありました。しかし、データ駆動スクリプトでは、共通化されたシナリオから外出しする形でテストデータを管理できます。外出しされた複数のテストデータを用いることで、1つのシナリオで複数のテストケースの実行が可能です。

▶︎特徴
 ✅保守性が更に向上し、テストケースの拡張も容易
 ❗️シナリオの共通化により、操作は同じだがテストデータが異なる複数のテストケースを一括で管理できる
 ❗️テストデータの構造化により、純粋にテストデータのみを管理できる

▶︎例
 例として、排出対象のキャラクターを指定したガチャを回すことが挙げられます。
 シナリオ(指定したキャラクターが当たるまでガチャを回す)はそのままに、テストデータ(レアリティに応じた獲得保証回数、キャラクター)のみを変更して異なるテストケース(ガチャ)を実行可能です。

▶︎記述レベル4の自動テストスクリプト
 以下は、テストケースを自然言語で記述したfeatureファイルのシナリオです。
 ここでは、featureファイルとは、主に下表の2点(StepとExamples)からシナリオを記述するものであるという簡素な説明に留めます。

featureファイルについての表
@gacha @guaranteed-acquisition
Scenario Outline: 指定したレアリティのキャラクターが獲得保証回数以内に獲得できることを確認する
  Given ログイン画面で"チュートリアル完了済みユーザー"でログインして"ガチャ画面"に移動する
  When ガチャ画面で<獲得保証対象>を指定する
  And ガチャ結果画面のガチャ実行回数が"0"以内であることを確認できる
  And ガチャ画面で<獲得保証対象>を獲得するまでガチャを実行する
  Then ガチャ結果画面のガチャ実行回数が<獲得保証回数>以下であることを確認できる

Examples:
  | 獲得保証対象 | 獲得保証回数 |
  | '{ "レアリティ": "SR", "キャラクターID": "0033" }' | 60 |
  | '{ "レアリティ": "SSR", "キャラクターID": "0199" }' | 80 |

 テストデータを外出しすることで、条件を変えてテストしたい場合でも、Examplesに行を増やすか行の中身を書き換えるだけで済みます。つまり、実施したいシナリオはそのままで、変更箇所を最小限に抑えることが可能です。

▶︎記述レベル4のまとめ

記述レベル4のまとめ


【Lv.5】キーワード駆動スクリプト【保守性の高さ: ★★★★】

記述レベルを正確に表した表

▶︎概要
 キーワード駆動スクリプトは、テストケースの記述とテスト実行の操作が構造化されており、プログラムコードからそれ以外の部分が分離されたスクリプトです。
 具体的には、「キーワード(例: featureファイルのStep)が記述されたシナリオに対して、キーワードに紐づいたプログラムコードが動作する」という形で記述されるスクリプトのことを指します。

▶︎特徴
 ✅テストケース記述と実行操作の分離によって、可読性と保守性が大幅に向上する
 ✅高度な構造化によって、複雑なシナリオの管理が容易になる

▶︎例
 例として、「指定したキャラクターを獲得するまでガチャを実行する」ようなキーワードを指定して実行すると、キーワードに紐づいた操作(例: 指定したキャラクターを獲得するまでガチャ実行の処理をループする)が実行されるようなテストが挙げられます。

▶︎記述レベル5の自動テストスクリプト
 以下のコードは、記述レベル4で紹介したfeatureファイルから一部抜粋したものです。
 ここでは、「ガチャ結果画面のガチャ実行回数が<獲得保証回数>以下であることを確認できる」というキーワードが、シナリオとして記述されています。

@gacha @guaranteed-acquisition
Scenario Outline: 指定したレアリティのキャラクターが獲得保証回数以内に獲得できることを確認する
  # ...省略...
  Then ガチャ結果画面のガチャ実行回数が<獲得保証回数>以下であることを確認できる

Examples:
  | 獲得保証対象 | 獲得保証回数 |
  | '{ "レアリティ": "SR", "キャラクターID": "0033" }' | 60 |
  | '{ "レアリティ": "SSR", "キャラクターID": "0199" }' | 80 |

 続いて、以下のコードは、先程のようなキーワードに紐づく処理が記述されているstepファイルから一部抜粋したものです。
 こちらはStep関数(Given / When / Then)を用いており、大きくは以下の2点が記述されています。

  • 処理に紐づくキーワード(Stepとしてfeatureファイルに記述した文言)
  • キーワードの内容通りに実行操作するための処理
Then<CustomWorld>(
  "ガチャ結果画面のガチャ実行回数が{string}以下であることを確認できる",
  async function (pureDefinedGuaranteeCount: string) {
    // シナリオから渡された引数の型をバリデーション(実際の操作手順とは関係ない)
    const definedGuaranteeCountSchema = 
      獲得保証回数: z.union([
        z.literal(60),
        z.literal(80),
      ]);

    const definedGuaranteeCount = definedGuaranteeCountSchema.parse(pureDefinedGuaranteeCount);

    // ページオブジェクトを宣言
    const gachaResultPage = await this.scenarioContext.getPage('ガチャ結果画面');

    // ガチャ実行回数を取得するメソッドをページオブジェクトから呼び出し
    const actualExecutionCount = await gachaResultPage.getActualExecutionCount();

    // actualExecutionCountが獲得保証回数以下であることを確認
    expect(actualExecutionCount).toBeLessThanOrEqual(definedGuaranteeCount);
  }
);

 このように、プログラムコード以外(シナリオ+テストデータ)とプログラムコードに分離することで、コードを変更した際の影響範囲が小さくなります。

▶︎記述レベル5のまとめ

記述レベル5のまとめ


テスト条件は記述レベルを使いこなすための鍵

▶︎概要
 ここまで記述レベルについて説明しましたが、単に記述レベルに則った記述をするだけでは、適切な構造化はできません。
 それは、どんなに強力な伝説の武器でも、使い方次第で与える威力が変わるのと同じです。ただなんとなく振り回して使うのと、急所を見極めて使うのとでは雲泥の差があります。

 以下では、記述レベルを使いこなすために理解しておくと良い「テスト条件」について説明します。

▶︎テスト条件について
 「テスト条件」とは、「環境」、「事前条件」、「入力」、「出力」の4つの要素で構成されています。
 テスト条件を明確にすることで、結果に影響を与える要素与えない要素を区別して考えることができます。この考え方は、テストを適切に構造化するための指針となります。

 今回、1つのシナリオのStepとExamplesについて、3つの異なる記述方法で考えました。以下は、単一のテスト条件とテスト目的(シナリオ)です。
 これらを通じて、テスト条件の理解度が課金戦士にどのような違いをもたらすかを見ていきましょう。

  • テスト目的(シナリオ): 課金した際に2倍ボーナスフラグの有無によって購入後ガチャ石数が2倍になることを確認する
  • 環境: Android、iOS
  • 事前条件: ユーザーアカウント、操作対象の画面、購入前ガチャ石数、購入対象のガチャ石数
  • 入力: 2倍ボーナスフラグ
  • 出力: 購入後ガチャ石数

①記述レベルもテスト条件も知らない場合のシナリオ

@in-app-purchase @double-bonus-flag
Scenario Outline: 課金した際に2倍ボーナスフラグの有無によって購入後ガチャ石数が2倍になることを確認する
  Given アプリを起動する
  And ログイン画面で<ログイン情報>でログインする
  And <画面>に移動する
  And ショップ画面で保有ガチャ石数が<購入前ガチャ石数>であることを確認する
  When ショップ画面で<購入対象>の購入を開始する
  And ショップ画面で<支払い方法>で購入確定する
  Then ショップ画面で保有ガチャ石数が<購入後ガチャ石数>であることを確認する

Examples:
  | ログイン情報| 画面 | 購入前ガチャ石数 | 購入対象 | 支払い方法 | 購入後ガチャ石数 |
  | '{"ユーザー": "2倍ボーナスフラグテスト用ユーザー", "ID": "abcd", "パスワード": "12345"}' | "ショップ画面" | 0 | '{"2倍ボーナスフラグ": true, "ガチャ石数": 10000, "金額": 10000 }' | "クレジットカード" | 20000 |
  | '{"ユーザー": "2倍ボーナスフラグテスト用ユーザー", "ID": "abcd", "パスワード": "12345"}' | "ショップ画面" | 0 | '{"2倍ボーナスフラグ": false, "ガチャ石数": 10000, "金額": 10000 }' | "クレジットカード" | 10000 |

 ①では、実際の操作手順を主軸に、変数にできそうなものを全て変数に入れたような乱暴なシナリオが記述されています。Scenario Outlineにテスト目的が書かれているだけで、テスト条件やテスト手順が混在しており何をしたいテストなのかがわかりません。
 課金戦士がSET1年生になったばかりの頃は、①のようなシナリオばかり量産していました。レビュアーが頭を抱える理由は①に凝縮されていますね。

②記述レベルだけ知っている場合のシナリオ

@in-app-purchase @double-bonus-flag
Scenario Outline: 課金した際に2倍ボーナスフラグの有無によって購入後ガチャ石数が2倍になることを確認する
  Given ログイン画面で<ユーザー>でログインして<画面>に移動する
  And ショップ画面で保有ガチャ石数が<購入前ガチャ石数>であることを確認する
  When ショップ画面で<購入対象>を<支払い方法>で購入する
  Then ショップ画面で保有ガチャ石数が<購入後ガチャ石数>であることを確認する

Examples:
  | ユーザー | 画面 | 購入前ガチャ石数 | 購入対象 | 支払い方法 | 購入後ガチャ石数 |
  | 2倍ボーナスフラグテスト用ユーザー | "ショップ画面" | 0 | '{"2倍ボーナスフラグ": true, "ガチャ石数": 10000}' | "クレジットカード" | 20000 |
  | 2倍ボーナスフラグテスト用ユーザー | "ショップ画面" | 0 | '{"2倍ボーナスフラグ": false, "ガチャ石数": 10000}' | "クレジットカード" | 10000 |

 ②では、以下の点で①よりも改善されています。1つ目は、Stepが実際の操作手順のような細かさにならないようにまとめている点です。2つ目は、シナリオ上省略可能な値(例: ユーザーに紐づくログインIDとパスワード)をExamplesから除外している点です。しかし、テスト条件を意識していないため、Examplesでは2行ともほぼ同様の値が使用されており、テスト目的に対して適切なテストデータを使用できているのかが分かりづらいです。
 課金戦士は約1年目にして、記述レベルを習得したことで②のようなシナリオ作成が可能になりました。その反面、Stepが細かすぎてはいけないという漠然とした理解にとどまっており、Stepの単位やExamplesの項目数を決める基準がなかったという問題も抱えていました。

③記述レベルとテスト条件を知っている場合のシナリオ

@in-app-purchase @double-bonus-flag
Scenario Outline: 課金した際に2倍ボーナスフラグの有無によって購入後ガチャ石数が2倍になることを確認する
  Given ログイン画面で"2倍ボーナスフラグテスト用ユーザー"でログインして"ショップ画面"に移動する
  And ショップ画面で保有ガチャ石数が"0"であることを確認する
  When ショップ画面で'{"2倍ボーナスフラグ": <2倍ボーナスフラグ>}'のガチャ石を"10000"個購入する
  Then ショップ画面で保有ガチャ石数が<購入後ガチャ石数>であることを確認する

Examples:
  | 2倍ボーナスフラグ |  購入後ガチャ石数 |
  | true | 20000 |
  | false | 10000 |

 ③では、テスト条件のうち、結果に影響を与える「入力」と、それによる結果である「出力」のみがExamplesに表現されています。一方で、結果に影響を与えない「環境」(ユーザーと画面)や「事前条件」(保有ガチャ石数が0個である)はStepに直書きされています。また、テスト目的に関係のない「支払い方法」がシナリオから除外されています。
 この構造によって、③ではExamplesを確認するだけで、テスト目的に対して実施する入力と期待結果がすぐに分かるようになっています。
 ここで重要となるテスト条件は、今年に入ってようやく少し理解できました。課金戦士は、ようやく一人前に近づいて来たのかもしれません。

▶︎まとめ
 具体例から、特に②③で表現しているテスト目的は同じですが、テスト条件を踏まえて記述するか否かで可読性や保守性が大きく向上していることが分かります。そもそも、テスト条件を理解していれば、①のようなテスト目的が不明瞭なシナリオを記述をすることはありません。
 以上のことから、テスト条件の理解が記述レベルの知識をより適切に活かすための鍵であると強く感じました(個人の感想です)。

さいごに

 記述レベルやテスト条件について、誰もが身近に感じられるガチャや課金から説明してみたのですが、いかがでしたか?
 多少なりとも楽しく学べたことがあれば幸いです。

 私個人としても、記述レベルについてアウトプットをするということは、大変得難い経験でした。
 はじめは「何も分からないから怖い」テスト自動化でしたが、記述レベルを学んでアウトプットする過程で理解を深めた今では、「分かった気がする」「意外と怖くない」に気持ちが傾いています。課金と同じで、最初は何百円、何千円の課金額に対しても抱いていた恐れが、いつしか万単位になっても何も感じなくなる(当たり前となる)ような日が来るのでしょう。
 今後は、テスト条件をしっかり理解した上で「約束された勝利の記述」として記述レベルを使いこなし、チームに貢献する所存です。

 最後に、食べログでは一緒に働く仲間たちを募集しています!
 食べログで働いてみたいと感じてくださった方は、以下の採用情報のリンクから是非応募してみてください!!


参考文献


  1. Fewster, M., & Graham, D. (2014). システムテスト自動化 標準ガイド (テスト自動化研究会, 訳). 翔泳社. (Original work published 1999)