51
49

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

エンジニア視点で見たCodeceptJSのよさ

Posted at

CodeceptJS?ああ、あのキモい書き方のやつでしょ?

待ってくれ。
実はキモくない書き方も出来るんだ。

キモい書き方.js

I.amOnPage('https://qiita.com')
I.fillField('キーワードを入力', '神龍')
I.pressKey('Enter')
I.see('我が名は神龍……どんなテストもひとつだけ自動化してやろう')
キモくない書き方.js

step.moveTo('https://qiita.com')
step.setValue('キーワードを入力', '神龍')
step.pressKey('Enter')
step.expectTextExists('我が名は神龍……どんなテストもひとつだけ自動化してやろう')

やってることはシンプルで、Translation を使ってメソッド名を置き換えているだけだ。

translation.js

module.exports = {
  I: 'step',
  actions: {
    amOnPage: 'moveTo',
    fillField: 'setValue',
    see: 'expectTextExists',
  }
codecept.conf.js
translation: "./translation.js"

まだキモいって?いいよもう好きなように変えてくれよ。変えれるからさ。

というわけで

最近 An Overview of JavaScript Testing in 2019 という記事が話題になってましたが、そこでのCodeceptJSの扱いはこんな感じでした。

If you believe this syntax is better for your needs, give it a shot.
この書き方が気に入ったら試してみてもいいんじゃない?

……違う!違うんだよ!! 書き方以外にもいいところがたくさんあるんだよ!!!!
そんなわけで今回は、エンジニア目線で見たCodeceptJSのよさを心ゆくまで紹介してまいります。

ちなみにおれはこの変な記法を広い心で受け入れ済みなので、以降のサンプルコードはデフォルトの I からはじまる記法で記載します。

覚えることが少ない

image.png

恐らくこの記事をご覧の皆様はまず

「CodeceptJSって結局なんなの?Seleniumなの?

と思われていると思いますので軽く説明すると、CodeceptJSはいろんなブラウザ操作ライブラリのラッパーです。
CodeceptJSそのものにはブラウザ操作する機能はなく、WebDriverIOやPuppeteerなどと組み合わせて使うことでブラウザ操作を実現しています。

E2Eテストと一口に言いつつも、いろんなライブラリが世の中には存在しており、予算や規模感に応じて適切なライブラリを選択しなければいけません。
その度に新しい記法を覚えないといけないのは正直辛いですよね?おれは辛いです。

CodeceptJSはすごいので、CodeceptJSの キモい 書き方を覚えれば、いろんなライブラリを使うことができます。

  • ガッツリインフラ整えてクロスブラウザテストしたい→WebDriverIO(Selenium)
  • ライトにChromeだけで動作確認したい→Puppeteer
  • ヘッドレスでサクッと自動テストしたい→Nightmare
  • ネイティブアプリも忘れないでね→Appium

こんな感じで、シーンに応じて適切なライブラリを選択することができます。

ちなみに、言語はJavascript(ES7)です。
好き嫌いはさておき、Javascriptならたぶん大体のエンジニアは読み書きできると思うので、例えばアプリケーション側のエンジニアにレビューしてもらいたいときも気軽にお願いできますね。

同期で書ける

async/awaitがよく分からなくてpuppeteerを諦めた人は全世界に約6億人ぐらいいると思われますが、絶望してエンジニアを辞める前にまずCodeceptJSを使いましょう。
CodeceptJSはすごいので何も考えなくても同期で書けます。


Feature('同期で')

Scenario('書けるよ', (I) => {
  I.click('すごいね')
})

Scenario('要素内のテキストとか取得したいときはおとなしくasync/await使ってNE★', async (I) => {
  const text = await I.grabTextFrom('#submit')
})

上述の例でも出ましたが、たまーーにasync/awaitを使わざるを得ない時もあるので、そういうときはグッとこらえて使いましょう。
ただ、そもそも 要素内のテキストを取得してゴニョゴニョ…… みたいに複雑なことをE2Eでテストしようとしてるのはなんか根本的におかしい可能性があるので、テスト設計見直すことも検討してください。
大抵はただのアサーションで何とかなります。

Custom Locator

A/Bテストや多言語対応などで、ページのスタイルや文言などがちょいちょい変わる場合、 data-* 属性を使って「テストのためのユニークなロケータ」を作ることは多いですね。


<!-- 'Login' か 'SignIn' のどちらかがランダムで表示されます -->
<button data-test="login"> {{ login }} </button>

デザインとテストが切り離されて大変よろしいんですが、テストコード側のロケータ記述が長ったらしくなるのが悩みどころですね。


// IDならこんな感じで短く書けるのに
I.click('#login')

// data-*だと長くなっちゃう
I.click('[data-test=login]')

IDには#、classには.がショートハンドとして用意されているように、例えば*で始まる文字列はdata-test属性として扱うみたいなのがあると楽そうですね。
そこでCustom Locatorの登場です。

customLocators.js

const codeceptjs = require('codeceptjs')

codeceptjs.locator.addFilter((providedLocator, locatorObj) => {
    // CSSやXPathではなく文字列である
    if (typeof providedLocator === 'string') {
      // *から始まる文字列である
      if (providedLocator[0] === '*') {
        locatorObj.value = `[data-test=${providedLocator.substring(1)}]`;
        locatorObj.type = 'css';
      }
    }
});

これで、テストコードはこんな感じでシンプルに書けるようになります。最高ですね。


I.click('*login')

DI

調子こいてPageObjectめっちゃ作りまくった結果、テストシナリオの前にimportが延々続くみたいなケースって割とあるじゃないですか。

PageObjectいっぱい呼び出す.js
import LoginPage from './pages/login'
import PortalPage from './pages/portal'
import UserPage from './pages/user'

const loginPage = new loginPage
const portalPage = new portalPage
const userPage = new userPage

Feature('いろんなテスト')

Scenario('ログインのテスト', async (I) => {
  await loginPage.login()
})

Scenario('ポータルのテスト', async (I) => {
  await portalPage.doSomething()
})

Scenario('マイページのテスト', async (I) => {
  await userPage.doSomething()
})

テストケースが増えてきてファイルを分割したら、その分だけimport書かないといけないじゃないですか。
そんなのめんどくさいので、なんか使いたいときにテストシナリオ側で宣言したら自動で使えるようになってるといいですよね。

CodeceptJSには標準でDIコンテナが備わっているので、設定ファイル codecept.conf.js で定義しておけば、シナリオ側で必要なものを呼び出すことができます。

codecept.conf.js

include: {
  loginPage: './pages/login.js',
  portalPage: './pages/portal.js',
  userPage: './pages/user.js',
}
シナリオ.js

Feature('いろんなテスト')

Scenario('ログインのテスト', async (I, loginPage) => {
  await loginPage.login()
})

Scenario('ポータルのテスト', async (I, portalPage) => {
  await portalPage.doSomething()
})

Scenario('マイページのテスト', async (I, userPage) => {
  await userPage.doSomething()
})

Actor

CodeceptJSのテストハンドラは Actor と呼ばれています(例のあのキモい I です)
ここにはclick()fillField() などデフォルトで定義されているステップの他に、自作のステップを生やすことができます。

例えば、ページへのログイン手順をまとめた I.login() というステップを定義してみましょう。

custom_step.js

module.exports = function() {
  return actor({
    login: function(email, password) {
      this.fillField('Email', email);
      this.fillField('Password', password);
      this.click('Submit');
    }
  });
}

これで、テストシナリオ中で I.login(email, password) として呼び出すことが出来ます。
ログイン以外にも、良く使う操作をまとめておくと便利です。

ちなみに、定義したステップを含む型定義ファイルを以下のコマンドで出力することができます。


$ npx codeceptjs def
TypeScript Definitions provide autocompletion in Visual Studio Code and other IDEs
Definitions were generated in steps.d.ts
Load them by adding at the top of a test file:

/// <reference path="./steps.d.ts" />

最後に出てきた /// <reference path="./steps.d.ts" /> という宣言をテストコードの一番上に追加すると、エディタ上で補完が効くようになります。

プラグインが作りやすい

詳細は公式ドキュメントの Plugins を見てもらいたいのですが、hookが充実しており、プラグインが作りやすくなってます。
例として、クリックしたら ポゥ! と標準出力に吐き出す謎のプラグインを開発してみます。

foo.js

const event = require('codeceptjs').event
const recorder = require('codeceptjs').recorder

module.exports = function () {
  event.dispatcher.on(event.step.after, async (step) => {
    if (step.name === 'click') {
      recorder.add('say pow', async () => {
        console.log('ポゥ!')
      })
    }
  })  
}

急に出てきた recorder ですが、録画とかをしてくれるわけではなくて、テストコードの同期的実行を実現してくれているやつです。
テストコードは直接実行されるのではなく、一度この recorder オブジェクトの中に登録されて、順番に実行されるので、テスト中に何か処理をはさみたい場合、recorder.addを経由する必要があります。

プラグインが完成したら、設定ファイル codecept.conf.js で読み込み&有効化させます。

codecept.conf.js

plugins: {
  foo: {
    require: './foo.js',
    enabled: true,
  },
},

テストコードを実行してみます。
サンプルとして、Githubにサインアップするシナリオを実行してみます。

github_test.js

Feature('Github')

Scenario('Can Sign Up', async (I) => {
  I.amOnPage('https://github.com')
  I.fillField('Username', 'shen-long1234')
  I.fillField('Email', 'shen-long1234@example.com')
  I.fillField('Password', 'P@ssword1234')
  I.click('Sign up for GitHub')
})

$ npx codeceptjs run github_test.js --steps

image.png

I.click() の後だけ ポゥ! と表示されましたね。

おわりに

どうですか?書き方がキモいとか言ってられなくなったのではないでしょうか?
興味を持っていただけたら是非公式サイトのQuickstartから始めてみてください!

51
49
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
51
49

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?