CodeceptJSのfuzzy locatorは若干遅いことがある

E2Eテストを書くためのツールである、CodeceptJSにはロケータ（操作対象要素を特定するもの）の指定方法にいくつかあります。
ロケータを文字列で指定すると、複数ある特定方法を順に試して適当に結果を返してくれるのですが、いかにも性能面の面倒がありそうです。

そこで、簡単に試してみて、どのくらい性能に影響があるのか確認してみたところ、たしかにいくらか遅いようでした。

# CodeceptJSでの、ロケータに関わる用語の定義
[公式サイトのLocatorドキュメント](https://codecept.io/locators/)で定義を確認。

## Semantic Locator
ロケータが指定されたコンテキストに応じて、CodeceptJSが適切な方法で操作対象要素を探してくれる方法。

以下がドキュメントでの記述。
> CodeceptJS can guess an element's locator from context. For example, when clicking CodeceptJS will try to find a link or button by their text When typing into a field this field can be located by its name, placeholder.

「CodeceptJSはコンテキストに応じて、要素に使うべきロケータを推測できる。例えば、CodeceptJSは、クリックしたい時にはリンクやボタンをその文字列で探そうとし、フィールドに入力したいときにはその名前やプレースホルダーで特定できる」といった意味です。
例えば、表示テキストが「検索」というボタンをクリックさせたい場合

```js
I.click('検索');
```

で指定可能です。classやカスタムデータ属性で指定するより可読性が高いですが、ドキュメントによると、CSSやXPathで明示的にセレクタを書くときよりも遅くなるとのこと。
>  they may run slower than specifying locator by XPath or CSS.

## 指定方法による分類
CodeceptJSに渡すロケータの型により、内部的な動作が異なります。
### fuzzy locator（文字列で渡す場合）
CodeceptJSが提供する関数に文字列でロケータを渡すと、関数の種類によって適した方法で対象要素を探してくれます。

以下は、os-test-id属性にNameが設定されているinputタグに「ダミー」という文字列を入力する例。

```js
I.fillField('input[os-test-id=Name]', 'ダミー');
```

[ドキュメント](https://codecept.io/locators/#css-and-xpath)によると、fillFieldにfuzzy locatorを渡すと、以下の手順で対象要素を探すそうです。
> 1. Does the locator look like an ID selector (e.g. "#foo")? If so, try to find an input element matching that ID.
> 2. If nothing found, check if locator looks like a CSS selector. If so, run it.
> 3. If nothing found, check if locator looks like an XPath expression. If so, run it.
> 4. If nothing found, check if there is an input element with a corresponding name.
> 5. If nothing found, check if there is a label with specified text for input element.
> 6. If nothing found, throw an ElementNotFound exception.

「1. ロケータがIDセレクタのように見える（例："#foo"）か？　そうであれば、input要素でそのIDに合致するものを探す」
「2. 何も見つからず、ロケータがCSSセレクタのように見えれば、CSSセレクタとして要素を探す」
「3. 何も見つからず、ロケータがXPathのように見えれば、XPathで要素を探す」
「4. 何も見つからなければ、input要素で指定された名前を持つものがないか探す」
「5. 何も見つからなければ、指定された文字列を持つLabelがついているinput要素がないか探す」
「6. 何も見つからなければ、ElementNotFound例外をthrowする」

### strict locator（オブジェクトで渡す場合）
CodeceptJSが提供する関数にオブジェクトのロケータを渡すと、対象要素を探す方法を特定できます。オブジェクトは{ ロケータの探索方法: ロケータ }という記法です。以下はCSSセレクタで対象要素を指定する例。

```js
I.fillField({ css: 'input[os-test-id="Name"]' }, 'ダミー');
```

fuzzy locatorの例のように色々な方法を順に試していく余分な手順が必要ないため、性能的にはこちらのほうが良いようです。

[ドキュメント](https://codecept.io/locators/#css-and-xpath)にも以下の記述がありました。
> If speed is a concern, it's recommended you stick with explicitly specifying the locator type via object syntax.

「速度が問題であれば、オブジェクト構文を使って、ロケータの種別を明示することが推奨される」
という意味ですね。

# 実験してみる
簡単なフォーム入力の画面を用意し、ロケータ指定方法のそれぞれを試して速度の違いを見ます。
## フォーム画面
サンプル画面はOutSystemsのTraditional WebのScreen Template "Product Detail"で用意しました。
CodeceptJSを使って、input, textarea, selectに入力させるテストを書いて実行時間を比較します。
![qCodecept1_1.GIF](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/332987/0b9d9a60-2d40-6213-7fe4-e7cb60994709.gif)

## Semantic Locatorで指定する
ロケータの指定方法にfuzzy locatorを使い（文字列で指定する）、ラベルの文字列の一致で対象要素を特定する書き方。

試行回数が1回だと実行タイミングによるゆらぎがあるかもしれないので、loopCountで指定した回数分繰り返して入力を行っています。合計7つのフォーム要素に適当に値を入力するコード。

```javascript
Scenario('locate with semantic locator', ({ I }) => {
    I.amOnPage('ProductDetail.aspx');

    for (var i = 0; i < loopCount; i++) {
        I.fillField('Name', 'NameDummy_Name_' + i);
        I.selectOption('Category', 'Laptops');
        I.fillField('Description', 'NameDummy_Description_' + i);
        I.fillField('Price', 'NameDummy_Price_' + i);
        I.checkOption('Charge taxes');
        I.fillField('Stock', 'NameDummy_Stock_' + i);
        I.fillField('Stock Threshold', 'NameDummy_Stock Threshold_' + i);
    }
});
```

## fuzzy locatorでCSSセレクタを指定する
OutSystemsでは、[テスタビリティを考慮した開発](https://success.outsystems.com/ja-jp/Documentation/11/Developing_an_Application/Developing_for_Testability)に、UIテストで対象要素を特定する方法として、
1. WidgetのNameプロパティを利用する（HTML上ではidの末尾にNameプロパティの値が出るので、id属性の後方一致で検索する）
2. os-test-id属性を対象Widgetに付与する（Traditional Webの場合は、Extended Propertiesを利用する）

があげられていますが、ここでは2.の方を利用します。

```javascript
Scenario('locate with fuzzy locator (css)', ({ I }) => {
    I.amOnPage('ProductDetail.aspx');

    for (var i = 0; i < loopCount; i++) {
        I.fillField('input[os-test-id=Name]', 'NameDummy_Name_' + i);
        I.selectOption('select[os-test-id=Category]', 'Laptops');
        I.fillField('textarea[os-test-id=Description]', 'NameDummy_Description_' + i);
        I.fillField('input[os-test-id=Price]', 'NameDummy_Price_' + i);
        I.checkOption('input[os-test-id="Charge taxes"]');
        I.fillField('input[os-test-id=Stock]', 'NameDummy_Stock_' + i);
        I.fillField('input[os-test-id="Stock Threshold"]', 'NameDummy_Stock Threshold_' + i);
    }
});
```

## strict locatorでCSSセレクタを指定する
fuzzy locatorと同じCSSセレクタをstrict locator方式で渡して速度の違いを確認。

```javascript
Scenario('locate with strict locator (css)', ({ I }) => {
    I.amOnPage('ProductDetail.aspx');

    let locatorName = { css: 'input[os-test-id="Name"]' };
    let locatorCategory = { css: 'select[os-test-id="Category"]' };
    let locatorDescription = { css: 'textarea[os-test-id="Description"]' };
    let locatorPrice = { css: 'input[os-test-id="Price"]' };
    let locatorChargeTaxes = { css: 'input[os-test-id="Charge taxes"]' };
    let locatorStock = { css: 'input[os-test-id="Stock"]' };
    let locatorStockThreshold = { css: 'input[os-test-id="Stock Threshold"]' };
    for (var i = 0; i < loopCount; i++) {
        I.fillField(locatorName, 'NameDummy_Name_' + i);
        I.selectOption(locatorCategory, 'Laptops');
        I.fillField(locatorDescription, 'NameDummy_Description_' + i);
        I.fillField(locatorPrice, 'NameDummy_Price_' + i);
        I.checkOption(locatorChargeTaxes);
        I.fillField(locatorStock, 'NameDummy_Stock_' + i);
        I.fillField(locatorStockThreshold, 'NameDummy_Stock Threshold_' + i);
    }
});
```

### 実行時間の測定
loopCountを10に設定して、各ロケータ指定方法を10回ずつ実行してコンソールに表示される実行時間の違いを確認してみました。

以下が実行結果。どうも最初のテストだけ、余計に時間がかかっていそうだったので、ダミーのテストを先頭に追加してあります（「dummy test」の行）。

```
  √ dummy test in 3889ms
  √ locate with semantic locator in 37188ms
  √ locate with fuzzy locator (css) in 54563ms
  √ locate with strict locator (css) in 39805ms
```

この結果を見ると、便利で読みやすいsemantic locator（inputのLabelやButtonのtextで検索できる）と、使用するセレクタの種類まで指定するstrict locatorがだいたい同じくらい。

fuzzy locatorでCSSセレクタを利用すると明らかに遅いですね。ただ、このくらいだとユニットテストではないので、fuzzy locator使うのもありかもしれませんが。

semantic locatorは指定方法としては、fuzzy locatorにあたるはず。上のfuzzy locatorの項に書いた順で対象を探索するなら、strict locatorと同じくらいの実行速度になるのは不思議ですね。

実用方針としては、ちょっと悩みますが、OutSystemsならidの後方一致かos-test-idをCSSセレクタのstrict locatorで指定、が良さそうです。実際には対象要素のセレクタはPageObjectのプロパティに隠蔽されて、「pageObject名.プロパティ」とかでアクセスするのでそんなに可読性は低くならないんじゃないかと思います。

可読性重視で速度が最優先でないなら、semantic locatorをfuzzy locatorで渡すのもありかもしれませんが。

# OutSystems開発ツール上でのos-test-idの設定例
参考までに。Traditional WebでWidgetのHTMLに出力する属性を指定する方法です。
Extended Propertiesに指定します。
![qCodecept1_2.GIF](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/332987/42db7c92-0bdf-02ab-b096-f94ce2bee629.gif)

# Dateやperformanceで時間を測定していない理由
本当は対象のFillFieldの部分だけJavaScriptで時間を測ろうかと思ったのですが。
CodeceptJSでは、I.に対して呼び出した関数はPromiseに変換されて、recorderというものに登録されてまとめて実行されるそうなので、その方法ではうまくいきませんでした。

以下のようなコードでstartとendの差を取ると1秒未満になります。performance.now()は該当行で直ちに実行されるのに対して、テストの実行が別途まとめて行われるためですね。

```javascript
var start = performance.now();
I.amOnPage('ProductDetail.aspx');
I.fillField('Name', 'NameDummy_Name_' + i);
...
var end = performance.now();
```

この辺の事情は、以下のYouTubeの動画で説明されていました。
[How CodeceptJS works with Promises](https://www.youtube.com/watch?v=MDLLpHAwy_s)