【Node.js】Seleniumでテスト自動化ツール用ライブラリを作った

1982 語
10 分
【Node.js】Seleniumでテスト自動化ツール用ライブラリを作った

はじまり#

135ml avatar
135ml
うーん・・・、OutSystemsで作ったシステムだから、打鍵しないと確認できないなぁ・・・。せっかく、製造で工数を減らしているのに、テストコードを書けないのはネックだよなぁ・・・。
135ml avatar
135ml
一体どうすりゃこんな膨大な手作業を無くせるんだぁ・・・。
リサちゃん avatar
リサちゃん
テッテレ〜! Selenium〜!
135ml avatar
135ml
あれ? まるで、ド◯えもんのような容姿になった? 声も変わった?
リサちゃん avatar
リサちゃん
容姿が変わったのは錯覚だね。それから声も変わってないね。ヘリウムじゃなくてセレニウムだね。 なんか、いろんな言語で使えるテスト自動化モジュールっぽいよ。
135ml avatar
135ml
おー! じゃあ、今回はそれを使ってみるか!

Seleniumについて#

Seleniumとは、テスト自動化ツールであり、主にJava、Ruby、Python、Javascriptで使用できます。

今回は、Javascript(Node.js)上で動くテスト自動化ツールを作っていきました。利用したOSはWindows 10で、ブラウザはChromeです。

Seleniumを使える環境を作る#

まずは環境構築です。最初にNode.jsをインストールします。こんなPowershellの画像が出てきたらインストール完了です。

次に、npm installします。

Terminal window
npm install selenium-webdriver --save
npm install mocha --save

そしたら、最後にchromedriverを入手します。僕はこのページから入手しました。

そして、入手したchromedriverを実行したいフォルダの中に置いたら、Seleniumを実行する環境は完成です!

最小構成:

Terminal window
<execution_directory>
chromedriver.exe
index.js
package.json

このフォルダの状態で以下のコマンドを実行すると、テストが開始されます。

Terminal window
npx mocha index.js --timeout 0

package.jsonの中身#

package.jsonはそのフォルダ内に適用されるコンフィグの意味合いを持つファイルです。

そして、Package.jsonを編集すると、実行するスクリプトを短く出来ます。例えば以下のように設定するとこのように入力するとテストを開始できます。

package.json:

{
"scripts": {
"psl": "npx mocha kouhochi-ichiran.js --timeout 0"
}
}

シェル:

Terminal window
npm run psl

ツールを作るためのライブラリ#

今回、テスト自動化ツールを作るためのライブラリを作りました。

ディレクトリ構成としては下記のようなイメージです。

./
├── lib/
│ ├── lib-methods.js
│ ├── lib-variables.js
│ └── getXpathByElement.js
├── screen-01.js
├── screen-02.js
└── package.json

細かいソースは公開できませんが、役割としては、以下のように分担させました。

lib-variables.js📖#

このファイルにはクラスだけが入っています。そして、それらのクラスは、変数として使用してもらうための属性値を持っています。例えば、個々の画面の各々の要素のXPathが入っていたりします。

lib-methods.js🦾#

関数とクラスが入っているファイルです。テストをするためによく使う処理をまとめました。

OperatorやInspectorは、基本的にとある属性(もしくは要素)からどの属性(もしくは要素)を取得したりクリックするかが記述されたメソッドを持っているクラスになります。こんなイメージです。

getXpathByElement.js🧗#

一般的にDOMから要素のXPathを取得する方法は、ブラウザの開発者ツールからとされています。 しかし、Seleniumで要素にアクセスするためにいちいちDOMを右クリックしてXPathを取得するのも面倒だと思います。僕の場合は、300回右クリックしてXPathをメモするのは嫌だったので、一気にXPathを取得する方法を探しました。

今回の方法では、2つファイルを作り実現しました。作るファイルは下記の責務で分かれています:

  • ブラウザで実行させるスクリプトが入ったファイル
  • 普通にseleniumを実行するファイル

Chromeで実行し、対象のCSSクラス名はcheckboxでした。

まず、ブラウザ側のファイルです。こちらのQiita記事を参考にしました。 最後の方は文法としては変ですが、配列のインデックスをスクリプトに載せるために"scriptSeparator"と記述しています。

function getXpathByElement (element) {
var NODE_TYPE_ELEMENT_NODE = 1;
if (element instanceof Array) {
element = element[0];
}
if (element.nodeType != NODE_TYPE_ELEMENT_NODE) {
throw new ErrorException('nodes other than the element node was passed. node_type:'+ element.nodeType +' node_name:'+ element.nodeName);
}
var stacker = [];
var node_name = '';
var node_count = 0;
var node_point = null;
do {
node_name = element.nodeName.toLowerCase();
if (element.parentNode.children.length > 1) {
node_count = 0;
node_point = null;
for (i = 0;i < element.parentNode.children.length;i++) {
if (element.nodeName == element.parentNode.children[i].nodeName) {
node_count++;
if (element == element.parentNode.children[i]) {
node_point = node_count;
}
if (node_point != null && node_count > 1) {
node_name += '['+ node_point +']';
break;
}
}
}
}
stacker.unshift(node_name);
} while ((element = element.parentNode) != null && element.nodeName != '#document');
return '/' + stacker.join('/').toLowerCase();
}
return getXpathByElement(document.getElementsByClassName("checkbox")["scriptSeparator"]);

そして、seleniumを実行するファイルです。

const { Builder, By } = require('selenium-webdriver');
const fs = require("fs");
const _ = require("lodash");
const getTextLines = (fileName) => {
const separator = '\n';
let text = fs.readFileSync(`./${fileName}`, 'utf8');
let lines = text.toString().split(separator);
return lines;
}
const getXpathsByClassName = async(driver, className) => {
let elements = [];
let xpath = '';
let xpaths = [];
const scriptFileName = 'lib/getXpathByElement.js';
let script = await getTextLines(scriptFileName).join('\t');
let scripts = await script.toString().split('\"scriptSeparator\"');
elements = await driver.findElements(By.className(className));
for (let i = 0; i < elements.length; i++){
xpath = await driver.executeScript(`${scripts[0]}${i}${scripts[1]}`);
xpaths.push(_.cloneDeep(xpath));
}
return new Promise( resolve => resolve(xpaths) );
};
let driver;
describe("テスト", () => {
before(() => {
driver = new Builder().forBrowser("chrome").build();
});
after(() => {
return driver.quit();
});
it{(`URLを開いたり、対象の要素を画面上に表示する。`), async () => {
// URLを開いたり、対象の要素を画面上に表示する処理
}};
it(`XPath取得`, async () => {
xpathArray = await getXpathsByClassName(driver, 'checkbox');
});
});

上記の他にも試したんですけど、上手く行きませんでした。

  • driver.findElements(By.className(className))で取得した要素をexecuteScriptに渡す。: ブラウザ側で認識している要素とどうやら違うようで、返り値がすべてnullになったので、失敗。
  • ブラウザ側でXPathの配列を作る。: メモリ不足エラーでブラウザが止まり、失敗。

Functions 🔧#

  • sleep: レンダリングが終わるであろう時間だけ処理を待ちます。
  • assertEqual_and_log: アサーションを行って、同時にログも出力します。
  • outputLog: ログを出力します。
  • getStrRepeatedToMark: 引数の文字列を繰り返して出力します。主にログの部分を見やすくするために使います。

Classes 👨‍👨‍👧‍👦#

  • Operator 🖱: 画面をスクロールしたり、クリックしたりする係です。クリックする際に同時に画面をスクロールするように実装しているつもりです。openAppViaO365()についてはこの記事で紹介しています。
  • Inspector 🔍: 画面の要素から属性値を取得したりする係です。getPseudoElementsContentsByClassName()についてはこの記事で紹介しています。
  • Photographer 📸: 画面をスクショする係です。
  • Gofer 🧹: 雑用です。 今のところ、配列をエクセルに貼り付けやすい形で出力したり、数字を0埋めします。

使用例#

例えば、Office365の画面を操作する場合:

const { Builder, By, until } = require('selenium-webdriver');
const util = require('util');
const assert = require("assert");
const _ = require("lodash");
const path = require('path');
const fs = require("fs");
const fsp = require('fs/promises');
// 一般ツール
const sleep = waitTime => new Promise( resolve => setTimeout(resolve, waitTime) );
const assertEqual_and_log = (case_no, expected, actual) => {
assert.equal(expected, actual);
console.log(`${case_no},${expected},${actual}`);
};
const outputLog = (funcName, remark) => {
console.log(`${funcName}: ${remark}`);
};
const getStrRepeatedToMark = (repeatStr, repeatNumberToMark=15) => {
return repeatStr.repeat(repeatNumberToMark);
};
const getTextLines = (fileName) => {
const separator = '\n';
let text = fs.readFileSync(`./${fileName}`, 'utf8');
let lines = text.toString().split(separator);
return lines;
}
// Selenium関連ツール
class Operator{
openAppViaO365 = async(driver, url, username, password, waitTimeToDisplay=40000) => {
const funcName = 'openAppViaO365';
await driver.get(url);
await driver.manage().window().maximize();
await outputLog(funcName, getStrRepeatedToMark('a'));
await sleep(10000);
await driver.findElement(By.id("i0116")).sendKeys(username);
await driver.findElement(By.id("idSIButton9")).click();
await outputLog(funcName, getStrRepeatedToMark('b'));
await sleep(10000);
await driver.findElement(By.id("i0118")).sendKeys(password);
await driver.findElement(By.id("idSIButton9")).click();
await outputLog(funcName, getStrRepeatedToMark('c'));
await sleep(10000);
await driver.findElement(By.id("idBtn_Back")).click();
await outputLog(funcName, getStrRepeatedToMark('d'));
await sleep(waitTimeToDisplay);
};
openAppDirect = async(driver, url, waitTimeToDisplay=40000) => {
const funcName = 'openAppDirect';
await driver.get(url);
await outputLog(funcName, getStrRepeatedToMark('a'));
await sleep(waitTimeToDisplay);
};
openAppDev = async(driver, url, username, password, waitTimeToDisplay=40000) => {
const funcName = 'openAppDev';
await driver.get(url);
await driver.manage().window().maximize();
await outputLog(funcName, getStrRepeatedToMark('a'));
await sleep(10000);
await driver.findElement(By.xpath('//*[@id="Input_UsernameVal"]')).sendKeys(username);
await outputLog(funcName, getStrRepeatedToMark('b'));
await driver.findElement(By.xpath('//*[@id="Input_PasswordVal"]')).sendKeys(password);
await outputLog(funcName, getStrRepeatedToMark('c'));
await driver.findElement(By.xpath('//*[@id="b6-Button"]/button')).click();
await sleep(waitTimeToDisplay);
await outputLog(funcName, getStrRepeatedToMark('d'));
}
clickElementsByElementArray = async(driver, elementArray) => {
const funcName = 'clickElementsByElementArray';
for (let i = 0; i < elementArray.length; i++){
// scroll in the window.
let element = await driver.findElement(By.id(idArray[i]));
await outputLog(funcName, `${i}: ${getStrRepeatedToMark('a')}`);
await driver.executeScript("arguments[0].scrollIntoView()", elementArray[i]);
await outputLog(funcName, `${i}: ${getStrRepeatedToMark('b')}`);
await sleep(1000);
// click element.
await elementArray[i].click();
await sleep(1000);
await outputLog(funcName, `${i}: ${getStrRepeatedToMark('c')}`);
}
};
clickElementsByXpathArray = async(driver, xpathArray) => {
const funcName = 'clickElementsByXpathArray';
for (let i = 0; i < xpathArray.length; i++){
// scroll in the window.
let element = await driver.findElement(By.xpath(xpathArray[i]));
await outputLog(funcName, `${i}: ${xpathArray[i]}: ${getStrRepeatedToMark('a')}`);
await driver.executeScript("arguments[0].scrollIntoView()", element);
await sleep(500);
await outputLog(funcName, `${i}: ${xpathArray[i]}: ${getStrRepeatedToMark('b')}`);
// click element.
await element.click();
await sleep(1000);
await outputLog(funcName, `${i}: ${xpathArray[i]}: ${getStrRepeatedToMark('c')}`);
}
};
clickElementsByIdArray = async(driver, idArray, sleepMilisecond=1000) => {
const funcName = 'clickElementsByIdArray';
for (let i = 0; i < idArray.length; i++){
// scroll in the window.
let element = await driver.findElement(By.id(idArray[i]));
await outputLog(funcName, `${i}: ${idArray[i]}: ${getStrRepeatedToMark('a')}`);
await driver.executeScript("arguments[0].scrollIntoView()", element);
await outputLog(funcName, `${i}: ${idArray[i]}: ${getStrRepeatedToMark('b')}`);
await sleep(sleepMilisecond);
// click element.
await element.click();
await sleep(sleepMilisecond);
await outputLog(funcName, `${i}: ${idArray[i]}: ${getStrRepeatedToMark('c')}`);
}
};
clickElementByXpath = async(driver, xpath) => {
const funcName = 'clickElementByXpath';
// scroll in the window.
let element = await driver.findElement(By.xpath(xpath));
await driver.executeScript("arguments[0].scrollIntoView()", element);
await outputLog(funcName, `${element}: ${getStrRepeatedToMark('a')}`);
// click element.
await element.click();
await sleep(1000);
await outputLog(funcName, `${element}: ${getStrRepeatedToMark('b')}`);
};
scrollDisplayToTargetXpath = async(driver, xpath) => {
const funcName = 'scrollDisplayToTargetXpath';
// scroll in the window.
let element = await driver.findElement(By.xpath(xpath));
await driver.executeScript("arguments[0].scrollIntoView()", element);
await sleep(1000);
await outputLog(funcName, `${element}: ${getStrRepeatedToMark('a')}`);
}
};

おしまい#

135ml avatar
135ml
おし、なんとかテスト自動化の土台ができたな・・・
リサちゃん avatar
リサちゃん
これでローコードとかノーコードのテスト工数を減らしたいね

以上になります!

記事を共有

この記事が役に立ったなら、ぜひ他の人と共有してください!

【Node.js】Seleniumでテスト自動化ツール用ライブラリを作った
https://endorphinbath.com/posts/nodejs-selenium-auto-test-library/
著者
kinkinbeer135ml
公開日
2021-12-28
ライセンス
CC BY-NC-SA 4.0
関連記事 スマート
1
【JavaScript】数値を0埋めされた文字列として加工する
Code JavaScriptで、IDなどを採番する時に0埋めした数値が欲しい時があります。その時に利用できるスニペットを紹介します。
2
【Node.js】Markdown内のimgタグの画像の大きさを変える
Code README.md内のimgタグで記載された画像のサイズを変更する処理を作成しました。沢山画像を貼っていると、いちいちサイズを変更するのが面倒ですが、この処理で一気に直してしまいましょう!
3
【JavaScript】実行中の関数自身の関数名を取得する
Code 実行している関数やメソッド自身の名前を取得する方法を紹介します。この方法は、その関数名を取得する関数を別の関数から呼び出してもらわなければなりません。thisに関数をバインドする必要があるためです。その呼び出し方の種類を掲載しています。
4
【JavaScript】Errorタイプのオブジェクトかどうかを判定する
Code JavaScriptで渡した値がErrorオブジェクトかどうかを判定する関数が見つからなかったので、僕が書いたものを紹介します。
5
【Node.js】GitHub ActionsでREADME.mdに投稿した記事のリンクを表示する(FirebaseでCORS対応)
Code Zenn、Qiita、ブログ、noteに投稿した記事のURLをREADM.mdに掲載するアプリを作成しました。このアプリを参考にして自分のGitHubのプロフィールページをカッコよくしてみましょう!GitHub ActionsやFirebaseを題材にしています。
ランダム記事 ランダム
Profile Image of the Author
kinkinbeer135ml
SIerをやめて、プログラミングを勉強しています。※Amazonアソシエイトに参加しています。
お知らせ
私のブログへようこそ!これはサンプルのお知らせです。
音楽
カバー

音楽

再生中なし

0:00 0:00
歌詞なし
カテゴリ
タグ
サイト統計
記事
287
カテゴリー
8
タグ
93
総文字数
486,174
運用日数
0
最終活動
0 日前

目次