252
222

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で、非エンジニアでも読みやすいテストコードを書く

Last updated at Posted at 2019-03-05

はじめに

この記事は我が名は神龍……どんなテストもひとつだけ自動化してやろうの焼き直し……もとい、利用した技術のちゃんとした解説です。
決して元記事がバズったから二匹目のドジョウを狙っているわけではありません。本当です。

エピソード

突然ですが、あなたは非エンジニアです。
あなたの仕事はテスト手順書に従って、リリースの前に手動でテストをすることです。

  • Chromeで https://kids.yahoo.co.jp/ にアクセス
  • 検索ワードに ねこ と入力して
  • さがすをクリック
  • 検索結果にネコ - Wikipediaが含まれていることを確認
  • 検索結果に 買い方 を追加
  • さがすをクリック
  • さがしているのは?と表示されることを確認
  • クリックすると猫の飼い方で再検索されることを確認

ある日、あなたの前にエンジニアがやって来て、こう言いました。

「テスト大変そうだったから自動化してみたよ!どう?」

そう語るエンジニアが指差す先では、ブラウザが超高速で勝手に動きまくっています。
もうこれでテストをしなくていいんだ……!働かなくても給料を貰えるんだ……!


リリース後、あなたの元に顔を真っ赤にしたプロジェクトマネージャーがやってきました。

「おい!不具合出てるじゃねーか!ちゃんとテストしてんのか!」

おかしいですね。テストは自動実行されているはずなので不具合が出るはずはない……
あなたはバグレポートを片手に、件のエンジニアのところに相談に行きました。

「あれ〜〜〜ごめ〜んここのテスト手順の実装漏れてたわ〜〜ハハハ〜〜〜」

ちょっとちょっと!ハハハじゃないって!他にも抜けてるとこないの!?
あわててあなたはエンジニアにテストコードを見せてもらいました。

test.js

const assert = require('assert')

describe('Yahoo!きっず', () => {
    it('検索できる', () => {
        browser.url('https://kids.yahoo.co.jp');
        $('#searchTextArea').setValue('ねこ')
        $('button[type=submit]').click()
        const title = $('.PageItem__title').getText()
        assert.strictEqual(title, 'ネコ - Wikipedia')
        $('input.SearchInput__input').Value(' 買い方')
        $('button[type=submit]').click()
        const spellerText = $('.Speller__text').getHTML()
        assert.ok(spellerText.includes('さがしているのは'))
        $('a.Speller__link').click()
        const searchText = $('input.SearchInput__input').getValue()
    });
});

なるほど!わからん!
これじゃ、他にも抜け漏れが無いかどうか、チェックすることもできないですね……!


この事件は社内で大問題になりましたが、幸いあなたはクビにはならず、代わりに次のような改善命令を下されました。

  • エンジニアは自動実行した内容を毎回動画撮影する
  • あなたはその動画を毎回チェックして、テスト項目がきちんと実行されているか確認する

リリース前日、自動実行された動画を眺めながら、あなたは思います。

「あれ……?これ手動でやってんのとあんま変わらなくね…………?」

誰のための可読性か

プログラミングにおいて『可読性』が重要であることは間違いありませんが、E2E、あるいは受け入れテストと呼ばれるテストの自動化においては、可読性は単にプログラマのものだけではなく、そのソフトウェアに関わる全ての人間にとって重要な意味を持ちます。
テストコードやレポートが、エンジニアでなくても読めるレベルの可読性を保っていれば、プロジェクトに関わる全員が「このソフトウェアの品質がどのレベルで担保されているのか」を知ることが出来ます。

「手動でやってたテストをエンジニアに自動化してもらったはいいけど、テストコードに抜け漏れが無いか不安……!だけどプログラミング1mmもわからない……!レビューできない……!」
「テストコード書いたからだれかにレビューしてもらいたいけど、一番この機能に関わってるCSの××さんはコード読めないからシナリオ書き出さないと……めんどくさ……」

このような悲劇を生み出さないために、非エンジニアでもレビューできるテストコードと、みんなが見やすいレポートを目指してみましょう。

(※なお、念の為ですが、上記のエピソードは私個人の創作であり、実在の人物・団体とは一切関係ありません。本当です。本当ですって!おれちゃんとテストしてますって!信じて!ビリーブミー!クビにしないで!!!)

前提

テスト対象のページ

Yahoo!きっず (https://kids.yahoo.co.jp) を例に取り上げます。
なお、本記事の執筆にあたり、Yahoo!Japan様には特に許可を取っておりません……!
万一問題がありましたらお手数ですがコメント欄等でご連絡頂ければと思います。

また、再掲ですが冒頭のエピソードはYahoo!Japan様含めいかなる人物・団体とも関係ありません。
自分のクビより先にこっちを心配すべきだった。

利用するライブラリ・フレームワーク

image.pngimage.png

Node製のE2EテストフレームワークCodeceptJSを用います。
https://codecept.io

CodeceptJSそのものにはブラウザを操作する機能はなく、WebDriverIOやPuppeteerなどのブラウザ操作ライブラリと組み合わせることでブラウザテストを実行する仕組みになっています。
今回はPuppeteerを使います。
https://github.com/GoogleChrome/puppeteer

余談ですが、Appiumにも対応していたりするので、モバイルアプリのテストにも使える(らしい)です。
自分はまだ試したことがないのですが、興味のある方は是非トライしてみてください。

準備

事前に

  • NodeJS v8.9以上
  • npm

をインストールしておいてください。

次にフレームワークをインストールします。
説明の簡略化のためにグローバルインストールしています。

npm install -g codeceptjs puppeteer

終わったら、作業用ディレクトリを作り、CodeceptJSの初期設定を行いましょう。
初期設定は対話式で行われますが、 What helpers do you want to use?Puppeteer と答え、その他はひたすらEnterでOKです。


$ mkdir e2e && cd e2e
$ codeceptjs init

  Welcome to CodeceptJS initialization tool
  It will prepare and configure a test environment for you

Installing to /home/tsuemura/e2e
? Where are your tests located? ./*_test.js
? What helpers do you want to use? Puppeteer
? Where should logs, screenshots, and reports to be stored? ./output
? Would you like to extend I object with custom steps? Yes
? Do you want to choose localization for tests? English (no localization)
? Where would you like to place custom steps? ./steps_file.js
Configure helpers...
? [Puppeteer] Base url of site to be tested http://localhost
Steps file created at /home/tsuemura/e2e/steps_file.js
Config created at /home/tsuemura/e2e/codecept.conf.js
Directory for temporary output files created at `_output`
Almost done! Create your first test by executing `codeceptjs gt` (generate test) command

動作確認

テストコードを記述するためのファイル sample_test.js を作成し、次のコードを記載してください。

sample_test.js

Feature('Yahoo!きっず')

Scenario('検索のテスト', async (I) => {
  I.amOnPage('https://kids.yahoo.co.jp/')
  I.see('Yahoo')
})

次に、コンソールで codeceptjs run sample_test.js と実行してください。
次のような結果が出力されればOKです。


$ codeceptjs run sample_test.js
CodeceptJS v2.0.6
Using test root "/home/tsuemura/codeceptjs_sample"

Yahoo!きっず --
  ✔ 検索のテスト in 908ms

  OK  | 1 passed   // 1s

可読性を上げる

さて、冒頭で登場したテストコードを見直してみましょう。
(ちなみにこのコードはWebDriverIOで書いてますが、WebDriverIOをDisるつもりは皆無です)


const assert = require('assert')

describe('Yahoo!きっず', () => {
    it('検索できる', () => {
        browser.url('https://kids.yahoo.co.jp');
        $('#searchTextArea').setValue('ねこ')
        $('button[type=submit]').click()
        const title = $('.PageItem__title').getText()
        assert.strictEqual(title, 'ネコ - Wikipedia')
        $('input.SearchInput__input').addValue(' 買い方')
        $('button[type=submit]').click()
        const spellerText = $('.Speller__text').getHTML()
        assert.ok(spellerText.includes('さがしているのは'))
        $('a.Speller__link').click()
        const searchText = $('input.SearchInput__input').getValue()
        assert.strictEqual(searchText, '猫の飼い方')
    });
});

あえてインパクト重視で分かりにくく書いてる部分も多いのですが、これでは非エンジニアはおろか、当のエンジニア本人すら明日には分からなくなっている可能性がありますね……!
元のテストケースはこうだったはずです。

  • Chromeで https://kids.yahoo.co.jp/ にアクセス
  • 検索ワードに ねこ と入力して
  • さがすをクリック
  • 検索結果にネコ - Wikipediaが含まれていることを確認
  • 検索結果に 買い方 を追加
  • さがすをクリック
  • さがしているのは?と表示されることを確認
  • クリックすると猫の飼い方で再検索されることを確認

これがそのままテストコードになればとても分かりやすいのですが、手順とコードを1対1で対応させることは出来ないのでしょうか?

ユーザー目線でテストコードを書く

CodeceptJSの最大の特徴として、 I から始まる キモい 独特の記法が挙げられます。


I.click('検索')                                          // '検索'と書かれたボタンをクリックする
I.fillField('メールアドレス', 'johndo@example.com')        // inputやtextareaなどに文字を入力する
I.see('ネコ 飼い方')                                      // ページに文字列が表示されていることを確認する
I.waitForText('ネコ 飼い方')                              // ページに文字列が表示されるまで待つ
I.uploadFile(filepath)                                  // ファイルをアップロードする
I.saveScreenshot(filepath)                              // スクリーんショットを保存する

上記は代表的なメソッドで、他にも色々あります。
興味のある方は Puppeteerで利用できるメソッドの一覧をご覧ください。

それでは、これを使って冒頭のテストを書き直してみましょう。

sample_test.js

Feature('Yahoo!きっず')

Scenario('検索できる', async (I) => {
  I.amOnPage('/')
  I.fillField('#searchTextArea', 'ねこ')
  I.click('button[type=submit]')
  I.see('ネコ - Wikipedia')
  I.appendField('input.SearchInput__input', ' 買い方')
  I.click('button[type=submit]')
  I.see('さがしているのは')
  I.click('a.Speller__link')
  I.seeInField('input.SearchInput__input', '猫の飼い方')
})

どうですか? $ が消えて I に変わっただけではありますが、どことなく読みやすくなった感じがしません?
同時に assertsee に、 setValuefillField に変わりました。

このように、CodeceptJSのキモ……独特の記法は、「(一人称で書かれた)テスト手順書を、そのままコードに置き換える」という思想に基づいて設計されています。
エンジニア目線だと一瞬ギョッとしますが、言われてみると理にかなっている気がしますね。

余談ですが、この I から始まるメソッドは自由に拡張することができます。
手前味噌ですが、以前書いた記事があるので興味のある方は是非ご覧ください。
https://qiita.com/tsuemura/items/3c95803382df85b42a9c

手順に無い記述を減らす

また、勘の良い方はお気づきかと思いますが、修正後のテストコードでは変数代入が一切行われていません。
元のテストコードでは、「検索結果にネコ - Wikipediaが含まれていること」を確認するために、以下のように2ステップ必要でした。


const title = $('.PageItem__title').getText()
assert.strictEqual(title, 'ネコ - Wikipedia')

これが、改善後のテストコードでは


I.see('ネコ - Wikipedia')

に変わっています。
一度 getText() のようなステップを挟まなくても、 I.see() だけで確認できちゃうのは、エンジニア的にもコード量が少なくなって嬉しいですね。

ちなみに、 .PageItem__title すら無くなっているのは、単に比較対象がこっそりページ全体になっているためで、まあ言うなればズルです。
ちゃんと書くと


I.see('ネコ - Wikipedia', '.PageItem__title')

のようになります。なんかよくわかんない英語が一つ増えてしまいましたね。
速度や検索範囲などの問題が無さそうであれば、ページ全体からでも大抵の場合問題ないと思いますし、読みやすいのでこのままにしましょう。

セレクタをわかりやすくする

さて、テストコードにはまだ分かりにくいところがありますね。
突然出てくる #searchTextArea などのCSSセレクタです。

sample_test.js

I.fillField('#searchTextArea', 'ねこ')

image.png

idで指定しているので明示的といえば明示的なのですが、なんだかいかにも非エンジニアには馴染みがなさそうです。
Placeholderの「調べたいことばをいれてみよう!」で指定できるととても分かりやすいのですが、こんな風に書けないものなのでしょうか?


I.fillField('調べたいことばをいれてみよう!', 'ねこ')

動くわけがない!……と思いましたか?いいえ、動きます!

Semantic Locator

この魔法を実現しているのは Semantic Locator という機構です。
魔法と言いつつ、やっていることは大したことではありません。

  • I.fillField() にCSSでもXPathでもない文字列が与えられた場合
  • <input> または <textarea> のうち、該当するlabelまたはplaceholderを持つ要素を探す

そのため、ある種限定的な用途にしか使えないのですが、使える部分ではテスト対象が明確になり、非常に分かりやすく書くことができます。

同様に、クリックについても次のように書くことができます。


I.click('さがす')

こちらも仕組みとしては非常に単純です。

  • I.click() にCSSでもXPathでもない文字列が与えられた場合
  • Clickableな要素(<a>, <button>など)のうち、該当する文字列を含むものを探す

…………と、ここまで調子よく書いてきたのは良いのですが、実はYahoo!きっずでは I.click('さがす')動きません

image.png image.png

自分も書きながら気づいたんですが、ページ内に さがす という表示が2つあるんですね。
普段は隠れているソフトウェアキーボードの さがす を誤検知してしまってるみたいです。
(ちょっとこれはCodeceptJS側の実装があんまりだと思うので今度プルリク出します……)

一応、こんな感じで書くと、「検索フォームの中にある さがす」をクリックしてくれます。


I.click('さがす', '.Search__form')

これは……CSSセレクタですね……わかりやすくないですね……ごめんなさい……可読性の低いコードを書いてしまった……
ドヤ顔で「E2Eテストの可読性とはなんぞ!(ドン!)」とか書いたのに……恥ずかしい……

まあ、なんかこんなかんじで変なところあると思うんで、気になったらIssue上げといて下さい(雑

修正後のコード

さて、ここまでのリファクタリングで、テストコードはどんなふうになったでしょうか?

sample_test.js

Feature('Yahoo!きっず')

Scenario('検索できる', async (I) => {  
  I.amOnPage('https://kids.yahoo.co.jp')
  I.fillField('調べたいことばをいれてみよう!', 'ねこ')
  I.click('さがす', '.Search__form')
  I.see('ネコ - Wikipedia')
  I.appendField('調べたいことばを入れてみよう!', ' 買い方')
  I.click('さがす', '.Search__form')
  I.see('さがしているのは')
  I.click('猫の飼い方')
  I.seeInField('調べたいことばを入れてみよう!', '猫の飼い方')
})

すごい!!劇的にわかりやすいですね!!!! これなら非エンジニアでも読めそうです!!!!!
ですが、文字だけじゃなんだか分かりにくいですし、出来れば I.click とかも日本語になってほしいですね……

翻訳する

そんなわけで(どんなわけだ)、CodeceptJSには全人類待望の Translation機能があります。

説明の前に、まずは翻訳なしで、先程のコードを「ステップつきで」実行してみましょう。
先程のコードをsample_test.jsとして保存し、コンソールで codeceptjs run sample_test.js --steps と実行してみてください。


$ codeceptjs run sample_test.js --steps
CodeceptJS v2.0.6
Using test root "/home/tsuemura/e2e"

Yahoo!きっず --
  検索できる
    I am on page "https://kids.yahoo.co.jp"
    I fill field "調べたいことばをいれてみよう!", "ねこ"
    I click "さがす", ".Search__form"
    I see "ネコ - Wikipedia"
    I append field "調べたいことばを入れてみよう!", " 買い方"
    I click "さがす", ".Search__form"
    I see "さがしているのは"
    I click "猫の飼い方"
    I see in field "調べたいことばを入れてみよう!", "猫の飼い方"
  ✔ OK in 2950ms

このレポートを日本語に翻訳してみます。
codecept.conf.js に、次の一行を追加してください。

codecept.conf.js
  translation: 'ja-JP',

さて、それではテストを再実行してみましょう。


$ codeceptjs run sample_test.js --steps
CodeceptJS v2.0.6
Using test root "/home/tsuemura/e2e"

Yahoo!きっず --
  検索できる
    私は ページを移動する "https://kids.yahoo.co.jp"
    私は フィールドに入力する "調べたいことばをいれてみよう!", "ねこ"
    私は クリックする "さがす", ".Search__form"
    私は テキストがあるか確認する "ネコ - Wikipedia"
    私は フィールドに文字を追加する "調べたいことばを入れてみよう!", " 買い方"
    私は クリックする "さがす", ".Search__form"
    私は テキストがあるか確認する "さがしているのは"
    私は クリックする "猫の飼い方"
    私は フィールドに文字が入っているか確認する "調べたいことばを入れてみよう!", "猫の飼い方"
  ✔ OK in 3446ms

すごい!!日本語になった!!読める!!!!!!

ちなみに、今回はレポートのみ日本語化してますが、上記の設定のみでコード自体も日本語で書くことができます。
また、独自の翻訳ファイルを利用することもできます。
詳細や、標準で利用可能な言語については公式ドキュメントをご参照下さい。

ステップごとにスクリーンショットを取る

このぐらい分かりやすい表記になってくると、なんだかユーザーマニュアルも自動生成できそうな気がしてきませんか?
さすがにそこまでキレイなものはできませんが、近いものを生成する機能は用意されています。

それが、StepByStep Reportプラグインです。
有効化すると、こんな感じのレポートが自動生成されます。

Peek 2019-03-05 00-52.gif

いやもう圧倒的に美しいですね……!
有効化の仕方は簡単で、 codecept.conf.js に以下を追加するだけです。

codecept.conf.js

  plugins: {
    stepByStepReport: {
      enabled: true,
      deleteSuccessful: false,
    },
  }

有効化されると、実行後に以下のような形で生成されたHTMLへのパスが表示されます。


◉ Step-by-step preview: file:///home/tsuemura/e2e/output/records.html

これで、テスト結果の確認をするのも簡単になりましたね!

成果

いま、あなたの手元には、分かりやすいテストコードと、分かりやすいレポートがあります。

sample_test.js

Feature('Yahoo!きっず')

Scenario('検索できる', async (I) => {  
  I.amOnPage('https://kids.yahoo.co.jp')
  I.fillField('調べたいことばをいれてみよう!', 'ねこ')
  I.click('さがす', '.Search__form')
  I.see('ネコ - Wikipedia')
  I.appendField('調べたいことばを入れてみよう!', ' 買い方')
  I.click('さがす', '.Search__form')
  I.see('さがしているのは')
  I.click('猫の飼い方')
  I.seeInField('調べたいことばを入れてみよう!', '猫の飼い方')
})

Yahoo!きっず --
  検索できる
    私は ページを移動する "https://kids.yahoo.co.jp"
    私は フィールドに入力する "調べたいことばをいれてみよう!", "ねこ"
    私は クリックする "さがす", ".Search__form"
    私は テキストがあるか確認する "ネコ - Wikipedia"
    私は フィールドに文字を追加する "調べたいことばを入れてみよう!", " 買い方"
    私は クリックする "さがす", ".Search__form"
    私は テキストがあるか確認する "さがしているのは"
    私は クリックする "猫の飼い方"
    私は フィールドに文字が入っているか確認する "調べたいことばを入れてみよう!", "猫の飼い方"
  ✔ OK in 3446ms

あとはこれを、CIで自動実行するも、WebDriverIOと組み合わせてクロスブラウザ実行するも自由自在です。

おわりに

自分がCodeceptJSを知ったのは今年の1月のことでしたが、あまりに便利すぎたので1月の終わりには仕事で使うテストライブラリをCodeceptJSに乗り換えてしまったほどです。
いやマジでCodeceptJS良いんですよ……!流行ってほしい……!心から……!

というわけで、末筆ではありますが、CodeceptJSのGithubリポジトリはこちらでございます。
https://github.com/codeception/CodeceptJS/

この記事を見て「お、CodeceptJSええやん」と思ったら、是非Githubの方も見に行ってあげてください。

それでは、良い自動化ライフを!
 
 
 
 
 
 
 

(おまけ)もしもYahoo!きっずがSPAだったら

CodeceptJSの大好きな機能の一つに、retryFailedStepがあります。
これは、SPAなど非同期での描画が多いWebサイトで、要素がレンダリングされるまでの待ち時間を認識できずに「そんな要素ねえよ!」と怒られて死ぬのを回避するためのプラグインです。

例えば、次のようなテスト手順を素直に実装すると、テスト対象のサイトがSPAだったり、Ajaxを多用していたりする場合、ページ切り替えを待たずに確認しようとして失敗します。

  • 検索ワードに ねこ と入力
  • さがすをクリック
  • 検索結果にネコ - Wikipedia が含まれていることを確認 ←ここでつまづく

こうしたケースに対応するため、これまでは画面が切り替わったことを確認するか、切り替わるまで固定秒数のwaitを挟む必要がありました。


I.fillField('検索したい言葉をいれてみよう!', 'ねこ')
I.click('検索')

// 「検索結果」ページに遷移したことを確認
I.waitForText('検索結果')

// または固定で5秒待つ
I.wait(5)

I.see('ねこ')

これらの記述が全く無意味であるとは言いませんが、できれば省略していきたいものですね。
RetryFailedStep を有効にしておくと、操作対象の要素が非表示だったり操作不可能だったりしたときに、少し待ってから勝手にリトライしてくれます。

インストールは簡単です。codecept.conf.js に、次の記述を追加しましょう。

codecept.conf.js

"plugins": {
    "retryFailedStep": {
       "enabled": true
    }
}

デフォルトでは、各手順が失敗した際に1秒おきに5回繰り返してくれます。
テストコードはこんな感じで読みやすくなりました。


I.fillField('検索したい言葉をいれてみよう!', 'ねこ')
I.click('検索')
I.see('ねこ')

他にも色々な機能があるので是非触ってみてもらえるとー。
それでは、今度こそ終わりです。ご愛読ありがとうございました!tsuemura先生の次回作にご期待下さい!!

252
222
2

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
252
222

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?