Web知識不要なTestCafe 導入
インターン先で結合テストを実施するにあたりTestCafeを使用したので、誰かの参考になればと思い備忘録として残す。
TestCafeとは
Javascript・Typescript系のテストツール。
私は上からTestCafeを指定されたので、他との違いはわかりません。
参考にさせていただいたページを置いておきます、理解の補助になりました、ありがとうございました!
前提
筆者はプログラマーでもエンジニアでも(今は)ない。
ただのソフトウェアテスターで、コーディングスキルは大学の文系向けプログラミング授業に毛が生えた程度。
テストの自動化としてTestCafeを導入することになり、エンジニアのサポートはほぼない中でイチから仕様を理解してコードを書くこととなった。 (本来フロントエンジニアの仕事では)
自動テストツールは他のものを使ったことがないので、それらとの比較は他の方を参考にしてください
対象読者
- TestCafeやTypescriptが初めての人
- エンジニア初心者の人
TestCafeは比較的ドキュメントや質問板、GitHubのissueが充実しているので困ったら英語でググる。DeepLに突っ込んででも英語で。
Guide:
Reference:基礎知識
テストコードには大きく分けて「アクション」と「アサーション」の2つがあり、それらに共通の「セレクター」がある。
アクションは.clickや.typeTextなどのアクション実行のことで、実行できればtrue、実行できなければfalseが返される。
アサーションは.expectなど要素の評価のことで、要素があればtrue、要素がなければfalseが返される。
これらを組み合わせてテストコードを組み上げていく。
セレクターは.clickや.expectを行なう要素を指定するもので、HTMLタグや属性をトリガーにする。
一つのテストファイルには原則1つのfixtureと複数のtestで成り立っている。
fixtureにはtestに共通の情報を書く。
testは成功や失敗すると次に進む。よって、連続したテストを行いたい場合はひとつのtestにまとめる必要がある。(例外あり)
インストール
※フォルダ移動:cd、フォルダ内ファイル確認:lsなどは各自調べてください
0.DocumentフォルダにGitHubからリポジトリをクローンしておく
1.Node.jsインストール
以下からインストーラをダウンロード・setupファイルを実行
完了したら以下のコマンドをコマンドプロンプト(Windows)かターミナル(Mac)で実行し、Node.jsとnpmのバージョンが表示されることを確認
node --version
npm --version
2.TestCafeインストール
インストール用のpackage.jsonがリポジトリにあるので、それを用いてtestcafeをインストールします。
npm install
TestCafeの実行方法
以下で実行
npm run testcafe chrome <file>
npm run testcafe chrome ./exam/example.ts
fileにはテストコードの書かれた.tsファイルの場所を指定する。
Fixture Class
.beforeEach
それぞれのテスト開始時に行なうことを指定する。
fixture`Fixture.beforeEach`
.page`https://devexpress.github.io/testcafe/example/`
.beforeEach(async t => {
await t.click('#submit-button');
});
.skipJsErrors()
ページでJavascriptエラーが出るとテストが失敗する仕様です。
テスト用のコードであれば良いですが、自動化にtestcafeを使用したい際にはこれをつけてエラー回避することができます。
fixture `Authentication tests`
.page `https://devexpress.github.io/testcafe/`
.skipJsErrors(); // or .skipJsErrors(true)
Selector Object / セレクター
HTMLタグやid, class、属性などを知るにはブラウザのデベロッパーツールを使用する。
ルール
- Selector('HTMLtag')
- Selector('#id')
- Selector('.class')
- Selector('HTMLtag.class')
- Selector('HTMLtag#id')
例:
Selector('h2.css-xxxx')
Selector('h2.css-xxxx.Disabled')
t.click(Selector('#id'))
# 上と下は同じ
t.click('#id')
t.click('#id').withText('foo') <- NG
t.click(Selector('#id').withText('foo')) <- OK
セレクターメソッド
セレクター.methods()
フィルターメソッド
セレクターのnth番目(nthは0スタート)
セレクター.nth(num)
// 3番目のinput要素
const secondInput = Selector('input').nth(2);
// 一番最後のdiv要素(-1は最後となる)
const lastDiv = Selector('div').nth(-1);
textが含まれるセレクター
セレクター.withText('text')
// labelタグでfooが含まれる要素
// 'foo', 'foobar'にはマッチするが 'bar', 'Foo'にはマッチしない
const elWithTextFoo = Selector('label').withText('foo');
// 正規表現も使用可能
const elWithRegExp = Selector('div').withText(/a[b-e]/);
textと一致するセレクター
セレクター.withExactText('text')
// 'label'タグで 'I have tried TestCafe'に完全一致する要素
// 'bar', 'foobar', 'Foo'にはマッチしない
const elWithText = Selector('label').withExactText('I have tried TestCafe');
属性と一致するセレクター
セレクター.withAttribute('属性', '属性値')
// 'for'属性を持つlabelタグ
const elWithAttrName = Selector('label').withAttribute('for');
// 'for'属性に'continuous-integration-embedding'を持つlabelタグ
const elWithAttrNameAndValue = Selector('label').withAttribute('for', 'background-parallel-testing');
find
findの引数に一致する子孫要素を返す。
セレクター.find(CSSセレクター)
Selector('div.col-1').find('input')
// div.col-1要素の子孫にあるinput要素
検索メソッド
find:子孫要素の検索
// 'div.col-1'要素の子孫のinputタグを探す。
const foundSelector = Selector('div.col-1').find('input');
// 'div.col-1'要素の子孫の4番目のinputタグを探す。
const foundSelector = Selector('div.col-1').find('input').nth(3);
parent:祖先要素の検索
// すべてのdivのすべての祖先要素。一つではなく複数の可能性があるため配列になる
const divParents = Selector('div').parent();
// すべてのinputの一番近い祖先要素。一つではなく複数の可能性があるため配列になる
const inputClosestParents = Selector('input').parent(0);
// すべてのlabelの一番遠い祖先要素。一つではなく複数の可能性があるため配列になる
const columnFurthestParents = Selector('label').parent(-1);
// すべての'fieldset'祖先要素であるすべてのdiv要素。一つではなく複数の可能性があるため配列になる
const fieldsetDivParents = Selector('fieldset').parent('div');
child:子要素の検索
// すべてのfieldset要素のすべての子要素
const fieldsetChildren = Selector('fieldset').child();
// すべてのdiv要素の最も近い子要素:child(n)で指定
const divClosestChildren = Selector('div').child(0);
// すべてのfieldset要素のすべてのpの子要素
const fieldsetPChildren = Selector('fieldset').child('p');
sibling:兄弟要素の検索
// すべてのinput要素のすべての兄弟要素
const siblingsInput = Selector('input').sibling();
// すべてのdiv要素から一番近い兄弟要素
// that go first in their parent's child lists.
const closestSiblingsDiv = Selector('div').sibling(0);
// すべてのbutton要素のすべてのfieldset要素を持つ兄弟要素
const pSiblingsDiv = Selector('button').sibling('fieldset');
nextSibling:次の兄弟要素の検索
// すべてのheader要素から後ろのすべての兄弟要素
const siblingsHeader = Selector('header').nextSibling();
// すべてのdiv要素の直後の兄弟要素
const closestSiblingsDiv = Selector('div').nextSibling(0);
// すべてのlegend要素から後ろにあるすべてのp要素
const pSiblingsDiv = Selector('legend').nextSibling('p');
prevSibling:前の兄弟要素の検索
// すべてのp要素より前のすべての兄弟要素
const siblingsP = Selector('p').prevSibling();
// 4番目のdiv要素の直前の兄弟要素
const pSiblingsDiv = Selector('div').nth(3).prevSibling(0);
その他セレクターメソッド
count
要素がいくつあるのかカウントするメソッド。検索メソッドで配列になったものに対して使うことが多い。
await t.expect(Selector('div').parent().count).eql(3)
// divの親要素が3つか判定するアサーション。eqlはアサーションメソッド参照
exists
セレクターにフィルターメソッドや検索メソッドを行ない、要素が存在しているか確認するメソッド。1以上ならtrueとなる
await t.expect(Selector('input').parent('span').exists).ok()
// inputの親要素にspan要素が存在するか判定するアサーション。okはアサーションメソッド参照
innerText
セレクターの中にあるテキストデータを返す。
await t.expect(Selector('input').nth(0).innerText).contains('password')
// 1番目のinput要素に囲まれたテキストにpasswordが含まれていればtrue
アクションメソッド
.click()
画面上の要素をクリックする
await t.click('セレクター')
.closeWindow()
現在アクティブなウィンドウを閉じる
await t.closeWindow()
.dragToElement()
要素を指定した先へドラッグ&ドロップする
await t.dragToElement(ドラッグしたい要素のセレクター, ドラッグ先のセレクター)
.hover()
要素の上にカーソルを動かす
await t.hover(セレクター)
.maximizeWindow()
アクティブなウィンドウを最大化する
await t.maximizeWindow()
.navigateTo()
指定したURLを開く
await t.navigateTo(URL)
.openWindow()
指定したURLを別ウィンドウで開く
await t.openWindow(URL)
.pressKey()
指定したキーを押す。
await t.pressKey('a')
await t.pressKey('enter')
await t.pressKey('delete')
.resizeWindow()
ウィンドウサイズを変更する。width=横幅、height=高さ
await t.resizeWindow(width, height) // 公式
await t.resizeWindow(1920, 1080) // 例
.scroll()
指定した方向へスクロールする
await t.scroll('top')
.scrollIntoView()
指定した要素を表示するようにスクロールする
await t.scrollIntoView(セレクター)
.selectText()
指定した要素のテキストを選択する
await t.selectText(セレクター)
.setNativeDialogHandler()
ブラウザダイアログをハンドリングする。ブラウザの離脱ダイアログが出ているか確認するために使用した
await t.setNativeDialogHandler(() => true)
.typeText()
セレクターにテキスト入力する。既に入力がある場合はreplaceを使用して置き換える
await t.typeText('セレクター', 'text')
await t.typeText('セレクター', 'text', { replace: true }) // 入力値を削除してから入力
.wait()
テストを一時停止する。数値はミリ秒
await t.wait(5000)
応用
text delete テキスト削除
await t
.selectText(Selector('input').withAttribute('name', 'title'))
.pressKey('delete')
アサーションメソッド
アサーションの軸は.expect。これにセレクターとアサーションメソッドを加えて評価する。
.expect.contains()
セレクター要素に文字が含まれる
await t.expect(Selector("title").innerText).contains('password')
.expect.eql()
// セレクター要素のテキストとイコール
await t.expect(セレクター.innerText).eql('text')
// セレクター要素の数とイコール
await t.expect(セレクター.count).eql(num)
.expect.gt() ※gte,lt,lte含む
セレクターの数が指定[より大きい]場合テストは成功する。
await t.expect(セレクター).gt(3) //3より大きい
await t.expect(セレクター).gte(3) //3以上
await t.expect(セレクター).lt(3) //3より小さい
await t.expect(セレクター).lte(3) //3以下
.expect.notContains()
セレクターにテキストが含まれていないときにテストは成功する
await t.expect(セレクター.innerText).notContains(テキスト)
.expect.notEql()
セレクターの数が数値に一致しないときテストは成功する
await t.expect(セレクター.count).notEql(2)
.expect.notOk()
セレクター要素がないときテストは成功する
await t.expect(セレクター.exists).notOk()
.expect.ok()
セレクター要素がある
await t.expect(セレクター.exists).ok()
注意点・よく起こる問題
- .click()はクリックできればtrueになるので、ロード途中などの理由によりリンクやボタンが動作しなくてもtrueになってしまう。その場合falseになるのは.click()より先になる。バナーなど、メイン要素の上に被っているものがあるときもその上からclickするので、期待した結果にならない事が多い。その場合は.scroll()や.scrollIntoView()を使用してから.click()を実行する。
- testを書く上で、javascriptやtypescriptの非同期処理(async/await)がわかっていると書きやすい。
- headlessモードはブラウザがEnglishになるのでwithText周りがfailureになるためテストできない(testcafeのchromium自体を日本語にすればできるかもしれないが、実行時のコマンドで日本語化する手段は試せる限りで失敗した)
Discussion