Selenium練習サイトを使って、CodeceptJS vs Cucumberしてみた

# 概要
[Selenium練習サイト](http://example.selenium.jp/reserveApp_Renewal/)を使って、CodeceptJSを使ったe2eテストを試してみましたので、テストコード（CucumberではJavaを使っています。CodeceptJSではJavaScriptを使いました）の記述しやすさ、テストケース（Cucumberの場合はGherkinで記述するfeartureファイル。CodeceptJSの場合は*_tesj.jsファイル）の扱いやすさなど述べたいと思います。
尚、CodeceptJS導入にあたっては、Qiitaの記事を参考にしています。大変助かりました。ありがとうございます。
画面操作の結果を、画面に表示されたもので検証することが基本でも、場合によってはビジネスロジックが稼働して、画面表示に影響を与えるような時、対応するテストロジックをテスト側で実装することもあろうかと思います。今回比較したCucumberとCodeceptJSとでは、テストケース仕様の違いから、作成したテストスイートは、テストコードの記述言語（JavaとJavaScript）の違い以上の相違点ができました。
どっちがいいのかなぁ　と読み進めて頂ければと思います。
# 何が嬉しいのか
Webアプリのe2eテストをSeleniumで行っている現場をよく見るようになりました。
WebDriverを使って要素の指定を行うことで、画面操作と操作後に表示されるビジネスロジックの結果表示を期待値と比較検証することで、テストを実装しています。
CodeceptJSの場合では、WebDriverも使えるのですが、今回、実行環境にplayWrightを用いてみました。導入から動かせるようになるまで、IDEに開発環境を入れて、pom.xmlに諸々の記述を追加して・・・　といったことはplayWrightでは不要な点、よく出来ていると感じました。そのplayWrightをCodeceptJSで使えるというのは、とても心強く思いました。また、試してはいませんが、WebDriverやPuppeteer、TestCafe等も利用できるようです。
自動テストを実装する上で、テストコードとテストケースを分離することは、仕様変更やテスト条件の変更に対してある程度の耐性を持たせることができる点、嬉しいかと思われます。
また、データ駆動型でテストケースを実装する場合、Pictmasterのような組み合わせ自動生成で生成したテーブルをテストシナリオで扱うテストデータ（Gherkinなら、シナリオアウトラインで使うデータ）に整形して利用・連携することで、テストケース維持に係る手間を減らすこともできます。
# テストケースの比較
用意したテストは、宿泊初日の曜日と連泊数によっては、週末料金が適用される仕組みを有している点に着目し、追加プランや宿泊人数によって、最終的な合計料金が正しく表示されるかどうかを検証しています。
ここで表示される合計料金が、実際に精算したときの料金と違っていたら・・・
なんか嫌ですよね。
##CodeceptJSで利用するテストケース内のテストデータ部分
 こんな感じ（データ部分のみ抜粋）

```reserve_test.js
let reserveTestTable = new DataTable(['宿泊初日', '連泊数', '宿泊人数', '朝食', '昼からチェックインプラン', 'お得な観光プラン', '氏名', '合計料金'])
reserveTestTable.add(['Monday', '1', '1', 'off', 'off'                   ,'off'           ,'武田晴信','7000'])
reserveTestTable.add(['Monday'   ,'2'   ,'8'     ,'on' ,'on'                    ,'on'            ,'真田昌虎','144000'])
reserveTestTable.add(['Monday'   ,'3'   ,'9'     ,'off','off'                   ,'on'            ,'山本寛太','198000'])
reserveTestTable.add(['Monday'   ,'5'   ,'2'     ,'on' ,'on'                    ,'off'           ,'上杉景虎','82000'])
reserveTestTable.add(['Monday'   ,'9'   ,'2'     ,'on' ,'on'                    ,'off'           ,'直江愛子','153000'])
reserveTestTable.add(['Tuesday'  ,'1'   ,'9'     ,'on' ,'on'                    ,'on'            ,'武田晴信','90000'])
reserveTestTable.add(['Tuesday'  ,'2'   ,'2'     ,'off','on'                    ,'on'            ,'真田昌虎','32000'])
reserveTestTable.add(['Tuesday'  ,'3'   ,'1'     ,'on' ,'off'                   ,'off'           ,'山本寛太','24000'])
reserveTestTable.add(['Tuesday'  ,'5'   ,'9'     ,'off','off'                   ,'off'           ,'上杉景虎','330750'])
reserveTestTable.add(['Tuesday'  ,'9'   ,'8'     ,'off','off'                   ,'on'            ,'直江愛子','540000'])
```
拡張子が js になっていることからも判るように、もはやこれはテストケースと言うよりは、テストコードの一部だと言えます。DaraTableを宣言して、データを追加していますから。
ただし、feartureファイルやspecファイルが、テストコードでは無いと言ってもDSL（Gherkin）あるいはMarkdown（Gauge）による記述にする点、導入にあたっての障壁は低いのかもしれませんが、ある程度の習熟は必要かと考えます。

## Cucumberで利用するテストケース内のテストデータ部分
 こんな感じ（抜粋）

```reserve.feature
		例:
		|宿泊初日   |連泊数|宿泊人数|朝食 |昼からチェックインプラン|お得な観光プラン|氏名      |合計金額|
		|"Monday"   |"1"   |"1"     |"off"|"off"                   |"off"           |"武田晴信"|"7000"|
		|"Monday"   |"2"   |"8"     |"on" |"on"                    |"on"            |"真田昌虎"|"144000"|
		|"Monday"   |"3"   |"9"     |"off"|"off"                   |"on"            |"山本寛太"|"198000"|
		|"Monday"   |"5"   |"2"     |"on" |"on"                    |"off"           |"上杉景虎"|"82000"|
		|"Monday"   |"9"   |"2"     |"on" |"on"                    |"off"           |"直江愛子"|"153000"|
		|"Tuesday"  |"1"   |"9"     |"on" |"on"                    |"on"            |"武田晴信"|"90000"|
		|"Tuesday"  |"2"   |"2"     |"off"|"on"                    |"on"            |"真田昌虎"|"32000"|
		|"Tuesday"  |"3"   |"1"     |"on" |"off"                   |"off"           |"山本寛太"|"24000"|
		|"Tuesday"  |"5"   |"9"     |"off"|"off"                   |"off"           |"上杉景虎"|"330750"|
		|"Tuesday"  |"9"   |"8"     |"off"|"off"                   |"on"            |"直江愛子"|"540000"|

```
CodeceptJSに比べると、スッキリしていて、テストコードと分離した感は高いかもしれません。
反面、テストシナリオ内にはプログラム制御を記述できませんので、例えば朝食の選択有無による画面上のチェックボックスOnOff操作については、StepDefinition側で記述することになります。
# テストシナリオの比較
CodeceptJSとCucumberとでは以上のようなテストケースの仕様に違いがあり、テスト目的を果たすためには、テストシナリオに記述できる内容にも影響が出ます。
## CodeceptJSで作成するテストシナリオ（抜粋）
テストシナリオに、制御（ここでは例としてif文）を記述しています。JavaScriptの言語仕様で扱える範囲の制御をテストシナリオにも記述できる点で、Gherkinより柔軟に対応できるとも言え、逆に、テストコードとの分離が曖昧とも言えます。分離が曖昧な場合に考えられるリスクとしては、開発言語が変わったとき、テストケースとテストコードとの両方を作り直さなければならないことになると思います。ただ、どれくらい、開発言語の乗り換えがあるのか？
前述のテストデータ部分は、これより前の方に記述します。

```reserve_test.js
中略
reserveTestTable.add(['Sunday'   ,'5'   ,'8'     ,'on' ,'off'                   ,'on'            ,'上杉景虎','342000']);
reserveTestTable.add(['Sunday'   ,'9'   ,'2'     ,'off','off'                   ,'off'           ,'直江愛子','136500'])

Feature('宿泊予約機能');

Data(reserveTestTable).Scenario('Reserve Tests', async({I , current}) => {
    I.ページを移動する('http://example.selenium.jp/reserveApp_Renewal/');
    I.fromDay(current.宿泊初日);
    I.click('#guestname');
    I.selectOption('reserve_t', current.連泊数);
    I.selectOption('hc', current.宿泊人数);
    if(current.朝食 == 'on'){
        I.checkOption('#breakfast_on');
    }else{
        I.checkOption('#breakfast_off');
    }
    if(current.昼からチェックインプラン == 'on'){
        I.checkOption('plan_a');
    }
    if(current.お得な観光プラン == 'on') {
        I.checkOption('plan_b');
    }
    I.fillField('gname', current.氏名);

    let term = await I.grabValueFrom('select[name="reserve_t"]');
    let headcount = await I.grabValueFrom('select[name="hc"]');

    I.click('利用規約に同意して次へ');

    I.see(current.合計料金);
    I.seeTerm(term);
    I.seeHeadCount(headcount);
    if(current.朝食 == 'on'){
        I.see('朝食: あり');
    }else{
        I.see('朝食: なし');
    }
    if(current.昼からチェックインプラン == 'on'){
        I.see('昼からチェックインプラン');
    }
    if(current.お得な観光プラン == 'on') {
        I.see('お得な観光プラン');
    }
    I.see(current.氏名);
    I.click('確定');
    I.see('ご来館、心よりお待ちしております。');
});
```
CodeceptJS + playWright では、エレメントをクリックしたり、画面からエレメントを取得したりするメソッドは、バンドルされており使うだけなので、CucumberのようにStepDefinitionに画面上のエレメントを扱うためのテストコードを記述する必要がありません。CodeceptJSではバンドルされているステップしか扱えないかというとそうではなく、カスタムステップにも対応しており、以下の例では宿泊期間の検証（seeTerm）と宿泊人数の検証（seeHeadCount）を作成しています。
こんな感じです。（抜粋）

```step_file.js
       seeTerm: function(term){
            var termText = term + "泊";
            this.see(termText);
            this.see(firstDay);
            lastDay = connector.termSet(term);
            this.see(lastDay);
        },

        seeHeadCount: function(headcount){
            var headText = headcount + "名様";
            this.see(headText);
        },

```
連泊数に「泊」、宿泊人数に「名様」を付けてCodeceptJSにバンドルされているsee を使って検証するカスタムステップとしました。

## Cucumberで作成するテストシナリオ
Gherkinで記述するfeatureファイル内のテストコシナリオはこんな感じ。

```reserve.feature
シナリオアウトライン: 宿泊料金検証テストケース
    もし  宿泊初日の曜日を<宿泊初日>として
    ならば "1"秒待つ
    もし  連泊数選択をクリックして
    もし  連泊数を<連泊数>にして
    もし  宿泊人数を<宿泊人数>にして
    もし  朝食バイキング有無を<朝食>にして
    もし  昼からチェックインプランを<昼からチェックインプラン>にして
    もし  お得な観光プランを<お得な観光プラン>にして
    もし  氏名を<氏名>として
    もし  予約内容を記録して
    もし  利用規約に同意して次へボタンをクリックする
    ならば  合計金額は<合計金額>となり
    ならば  宿泊期間の表示が正しく
    ならば  宿泊人数の表示が<宿泊人数>名様となり
    ならば  追加プランが<朝食>または<昼からチェックインプラン>または<お得な観光プラン>で正しく
    ならば  氏名の表示が<氏名>様となり
    もし    確定ボタンをクリックする
    ならば  ポップアップ表示に"ご来館、心よりお待ちしております。"が表示され
```
Cucumberにはバンドル済みのステップというのは無いので、feature記述したステップに対応した実行についての記述をStepDefinitionにて、自前でプログラムする必要があります。その分、いかようにもステップを記述できるとも言えます。
「ならば  追加プランが<朝食>または<昼からチェックインプラン>または<お得な観光プラン>で正しく」
に対応したStepDefinitionは以下のような感じです。（抜粋）

```StepDefinitios.java
    @ならば("追加プランが\"([^\"]*)\"または\"([^\"]*)\"または\"([^\"]*)\"で正しく$")
    public void testOption1(String plan1, String plan2, String plan3) throws InterruptedException {
    	String planText;

    	String selector1 = "//*[@id=\"bf_order\"]";
    	String selector2 = "//*[@id=\"plan_a_order\"]";
    	String selector3 = "//*[@id=\"plan_b_order\"]";

    	if((plan1.equals("off"))&&(plan2.equals("off"))&&(plan3.equals("off"))) {
    		planText = "なし";
    		assertTrue(connector.testTextX(selector1, planText));
    	}

    	if((plan1.equals("on"))&&(plan2.equals("off"))&&(plan3.equals("off"))) {
    		planText = "あり";
    		assertTrue(connector.testTextX(selector1, planText));
    	}
    	if((plan1.equals("off"))&&(plan2.equals("on"))&&(plan3.equals("off"))) {
    		planText = "昼からチェックインプラン";
    		assertTrue(connector.testTextX(selector2, planText));
    	}
    	if((plan1.equals("off"))&&(plan2.equals("off"))&&(plan3.equals("on"))) {
    		planText = "お得な観光プラン";
    		assertTrue(connector.testTextX(selector3, planText));
    	}

    	if((plan1.equals("on"))&&(plan2.equals("on"))&&(plan3.equals("off"))) {
    		planText = "あり";
    		assertTrue(connector.testTextX(selector1, planText));
    		planText = "昼からチェックインプラン";
    		assertTrue(connector.testTextX(selector2, planText));
    	}
    	if((plan1.equals("on"))&&(plan2.equals("off"))&&(plan3.equals("on"))) {
    		planText = "あり";
    		assertTrue(connector.testTextX(selector1, planText));
    		planText = "お得な観光プラン";
    		assertTrue(connector.testTextX(selector3, planText));
    	}
    	if((plan1.equals("off"))&&(plan2.equals("on"))&&(plan3.equals("on"))) {
    		planText = "昼からチェックインプラン";
    		assertTrue(connector.testTextX(selector2, planText));
    		planText = "お得な観光プラン";
    		assertTrue(connector.testTextX(selector3, planText));
    	}

    	if((plan1.equals("on"))&&(plan2.equals("on"))&&(plan3.equals("on"))) {
    		planText = "あり";
    		assertTrue(connector.testTextX(selector1, planText));
    		planText = "昼からチェックインプラン";
    		assertTrue(connector.testTextX(selector2, planText));
    		planText = "お得な観光プラン";
    		assertTrue(connector.testTextX(selector3, planText));
    	}

    }
```
CodeceptJSでテストシナリオに記述していた分岐文が、StepDefinitionに記述される感じになります。
Cucumberでは、「もし」と「ならば」を使い分けることで、検証に至るまでの操作なのか検証ステップそのものなのかをシナリオから読み取れる点で、CodeceptJSより可読性高いようにも感じます。
CodeceptJSとCucumberとを比較した結果、テストケースの仕様上の違いから、

* 制御をテストシナリオでも記述して、バンドルされているステップを使うCodeceptJS
* テストシナリオには制御は記述せずに、StepDefinitionにステップを記述するCucumber

ということが見えてきました。
テスト目的によっては単に上から順番に実行すれば済むという訳にもいかなくなり、どこかで（テストケースまたはテストコード）記述が必要不可欠です。
そもそも、ローコードってどれくらいならローコードなのか、グレーな気もします。

# テストコードの比較
コード量は、CodeceptJSの場合がCucumberに比べると少なくなりました。エレメント操作と取得についてバンドル済み（CodeceptJS）と自前開発（Cucumber）との違いによるものです。
ただし、ビジネスロジックの検証については、今回の例だと、テストデータに記載の曜日に合わせた宿泊初日を設定するロジックと、画面で選択された連泊数を宿泊初日に加算して宿泊最終日を設定するロジックとは、CodeceptJSもCucumberも、両方とも準備してテストに臨むことになりました。
当たり前ですが、テスト目的によっては、例えローコードと言っても、ビジネスロジックを含んだ機能検証では、フレームワーク等にバンドルされている可能性は低いので、自前で準備は必要です。
## CodeceptJSで作成したテストコード（例）
今日の日付（jtcDate）の曜日から、テストデータで指定される宿泊初日の曜日に合わせるべく、宿泊初日をjtcDateに加算しています。
以下の例はテストデータで宿泊初日に日曜日を指定されていた場合に使う処理です。
日曜日なら、そのままとし、月曜日ならば次の日曜日まで日付を6日加算し、火曜日なら5日加算
といった具合です。

```connector.js
module.exports.sunday = function() {

            		switch(dayName) {
            		case 'Sunday':
            		    dateFrom = jtcDate;
            			break;
            		case 'Monday':
            		    dateFrom = addDays(jtcDate, 6);
            			break;
            		case 'Tuesday':
            		    dateFrom = addDays(jtcDate, 5);
            			break;
            		case 'Wednesday':
            		    dateFrom = addDays(jtcDate, 4);
            			break;
            		case 'Thursday':
            		    dateFrom = addDays(jtcDate, 3);
            			break;
            		case 'Friday':
            		    dateFrom = addDays(jtcDate, 2);
            			break;
            		case 'Saturday':
            		    dateFrom = addDays(jtcDate, 1);
            			break;
            		default:
            		}
                    var testReserveDate = format(dateFrom, 'YYYY/MM/DD');
                    return testReserveDate;
};

```
JavaScriptで利用できる適当な日付ライブラリを探して使う時間が、一番かかりましたね。
date-fns を使いました。
## Cucumberの場合に作成したテストコード（例）
やりたいことはCodeceptJSの場合と変わりないのですが、エレメントに設定した値を入力するのも、こちらのテストコードにて実装しています。繰り返しになりますが、CodeceptJSでは、画面操作と取得についてはバンドルされている機能を使うことになりますので、テストコードでは実装していません。

```WebConnector.java
    	public void sunday(String commandLocater) throws InterruptedException {
    		Date reserveDate;
    		String testReserveDate;
    		Calendar calendar = Calendar.getInstance();
    		calendar.setTime(dt);
    		calendar.add(Calendar.DATE, 1);
    		switch(calendar.get(Calendar.DAY_OF_WEEK)) {
    		case Calendar.SUNDAY:
    			break;
    		case Calendar.MONDAY:
    			calendar.add(Calendar.DATE, 6);
    			break;
    		case Calendar.TUESDAY:
    			calendar.add(Calendar.DATE, 5);
    			break;
    		case Calendar.WEDNESDAY:
    			calendar.add(Calendar.DATE, 4);
    			break;
    		case Calendar.THURSDAY:
    			calendar.add(Calendar.DATE, 3);
    			break;
    		case Calendar.FRIDAY:
    			calendar.add(Calendar.DATE, 2);
    			break;
    		case Calendar.SATURDAY:
    			calendar.add(Calendar.DATE, 1);
    			break;
    		default:
    		}

    		reserveDate = calendar.getTime();
    		SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");
    		testReserveDate = sdf.format(reserveDate);
    		dt = reserveDate;

    		String inputText = testReserveDate.substring(0, 10);

                WebElement inputDate = webDriver.findElement(By.id(commandLocater));
                wait.until(ExpectedConditions.elementToBeClickable(inputDate));
                inputDate.sendKeys(inputText);
    	}
```
使っているライブラリも開発言語が違うので当然異なりますね。CalendarをJavaでは使いました。
# テスト実行と結果の確認について
レポート機能についてはスクリーンショット機能も合わせてバンドル済みのGaugeとの比較になりますが、CodeceptJSではAllureをレポートとして使う場合、codecept.config.js に以下を追記するだけでしたので、導入は簡単でした。

```codecept.config.js
    allure: {
        enabled: true
      }

```
エラー発生時のスクショ撮る / 撮らない 等の設定も可能な仕様です。
CucumberとGaugeとの比較については、[こちら](https://qiita.com/KazuhiroYoshino/items/876f59ce3b8c547ea8f9)に記載していますので、ご覧頂き、感想などいただけると助かります。
意図的に合計料金の期待値を正しい値にしないテストデータ（本当は7000円なのに期待値を6500円にした）を混入させて、レポートがどのように作成されるかを比較してみました。
## CodeceptJSでレポート
Allureのレポートはこんな感じです。
![testResult004.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/939617/5cbd21a2-3a5b-5563-e617-74dc04744b38.png)
Overviewからエラー発生箇所に進んでいくと
テスト結果に赤いマーク付きのものが現れて、スクリーンショットも添付されています。
![testResult002.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/939617/575d5f34-d6be-3206-69cc-5f9b39c8f58f.png)

テスト結果に赤いマーク付きのものが現れて、スクリーンショットも添付されています。
Allureでは、コマンドたたいて（allure serve ./output）表示させます。IDEのターミナルにもテスト結果は表示されるので、htmlだけがテスト結果の確認手段というわけではありません。
![testResult001.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/939617/306b4b1b-219d-6afa-95dd-dc8a5ee113eb.png)

## Gaugeでレポート
Gaugeでは、レポートはhtmlドキュメントを作成して、エラー発生時のスクショを自動的に撮影して添付してくれるので、フォルダに生成されたhtmlドキュメントを開いて確認します。Allureとはこのあたりの使い勝手が異なっていました。
![testResultGauge001.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/939617/aff8ad36-1c6d-a2c2-da8b-4e6570bba2a4.png)
エラー発生箇所のスクリーンショットも、このように記録されます。
![testResultGauge003.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/939617/1bb0eea8-7c77-23a7-0a90-c4b13adf085a.png)
個人的な嗜好では、AllureのレポートよりもGaugeのレポートの方が好きです。
# 総括
テストが実施できるまでの段取りと、テスト実施レポートについて、CodeceptJSの場合とCucumberの場合とで比較してみました。
最後まで読ませて、それは無いだろう！　かもしれませんが、どちらが良いとか悪いとかではなく、

* 準備するテストケース・シナリオに違いがあった。
* ビジネスロジックをテストするなら、いずれの環境でも、それなりにテストコードの記述は発生する。
* スクリーンショットやレポート生成機能に違いがあった。

ということでした。
SeleniumもSelenide（Javaの場合）のようなラッパーを使えば、生で使うよりは使い勝手が良くなると思います。
CucumberやGaugeの導入事例は英語の資料が多い点、もっと国内で導入が進めば、より身近になるのではないでしょうか。
CodeceptJSは、最近よく導入事例を見るようになってきており、書籍はほとんど無いのですが、こちらもどんどん使ってもっと身近に使えるようになればと考えています。
CodeceptJSは未だ始めたばかりなので、まだまだ試していない領域がありそうです。
自動テストを実装する組織やメンバーによって、導入にあたっての壁の高さや維持を鑑みる必要性が生じるのが実情だと思います。プロジェクトの成果物としてテスト環境も価値あるものにしたいと考えるのも当然です。
結局、みんなで既成事実を積み上げましょう　という結論ですが、
同じ目的のテストを、違うツールで比較することは自動テストコンテストでもなければ、なかなか目にする機会もないかもしれませんね。本投稿が、ツール選定の一助になれば幸いですし、これからもインプレッションを投稿できればと思います。