なんの話か
Reactで作られたWebページ(申込フォーム)のテスト自動化のために、Seleniumを導入しようとしたのですが、Reactのステート管理の壁に阻まれて、思ったように値が入らないーーーとなりましたが、なんとか解消したという話です。
ついでに、需要があるかわかりませんが、Seleniumテストケースを生成するExcelツールにまとめてみました。詳細は後述
はじめに
Seleniumを選定した理由は以前使ったことがあり、なんとかなりそうだと思ったからです。SeleniumはSeleniumでも、ブラウザエクステンションのSelenium IDEです。
作ったのは、これに読み込ませるテストケースを生成するツールです。
経緯
少し前にとあるサイトでSeleniumで自動テストをできないかということになり、いろいろやってみました。すんなり行くんじゃないかと思いきや、画面は変わっても実際にinput項目に値が反映されてない状態に。
確認したところ、Reactステート管理されているから外から値は設定できないよ、とDev Leaderに一蹴されました。
React経験がなく、実はかなりビックリしました。ただ「はいそうですか」とはならず試行錯誤してみてたら、うまく値が設定できる方法があったので、おなじ境遇の方がいらっしゃれば参考になるかと思い投稿します。本来はどうすべきかを考えずにやっちゃってるので、詳しい方がいらっしゃればご指摘いただければ幸いです。
※当時は時間かけたくなかったのでReactの勉強は置いておいちゃいました。ただ、どうやれば目的を達成できるかしぼってリバースエンジニアリングだけしてました(とりあえず動けば目的達成)。
Seleniumのコマンドはrun script
Selenium IDEではCommand
Targeg
Value
をきちんと設定しないといけません。Recボタンを押してから画面をポチポチやってもテストケースを作れますが、まともなものができたことが無い気がします。
なので、私は面倒でもかならずid
class
name
などを調べて設定するようにしています。
人それぞれかと思いますが、Selenium IDEではrun script
コマンドをつかって、JavaScriptで値を詰めたほうがいいと思ってます。大抵の自動テストツールには任意のスクリプトを実行できるオプションがあるので、JavaScriptでできるようにしておけば、別のツールへの以降が楽だ、と考えてるからです。
フォームに値をいれるJavaScript
最終的に以下のようなJavaScriptでなんとか解決しました。環境やWeb側の実装方法によってはうまく行かないかもしれませんが、全体的なアイディアは流用できるのではないかと。
前述していますが、Seleniumのコマンドはrun script
を選択してます。このコマンドはTarget
項目に記載したJavaScriptを実行してくれます。
ClickとCheck
クリックとチェックボックスは単純です。要素探してクリックするだけです。
if(document.querySelector("#selector#")) {
document.querySelector("#selector#").click();
}
※#selector# を実際のセレクタに置き換えてください
DOMとしては以下のような感じのものに対してです
<input name="r-u-cat-or-not" type="checkbox" value="yes-i-am">
input
Reactで作られたWebフォームで最初に苦戦したのがinputでした。
DOMとしてはこういうやつです。
if (document.querySelector("#selector#")) {
(Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, "value").set).call(document.querySelector("#selector#"), "#target#");
document.querySelector("#selector#").dispatchEvent(new Event('input', {
bubbles: true
}));
}
※#selector# を実際のセレクタに、#target# を設定する値に置き換えてください
元々は単純にdocument.querySelector("#selector#").value = "#target#";
で行けるとおもっていたのですが、Reactの場合は、ただ値を設定するだけでは無理でした。
やったことは、
- getOwnPropertyDescriptorでInput要素の完全なプロパティを取得
- セッターを取得して、対象項目に値を詰める(Call)
- dispatchEventでinputイベントを発火させてバブリング
これで何とか行けました。inputイベントじゃなくて、changeイベントでもいいかもです。
radio
ClickやCheckと同じようにできます。ただクリックする要素を見つけるのに探索をするくらいの違いです。
if (document.querySelectorAll("#selector#")) {
document.querySelectorAll("#selector#").forEach(v => {
if (v.parentNode.textContent.trim() === "#target#") v.click()
})
}
※#selector# を実際のセレクタに、#target# を設定する値に置き換えてください
select
inputをベースにできます。
if (document.querySelector("#selector#")) {
document.querySelector("#selector#").focus();
document.querySelector("#selector#").value = "#target#";
document.querySelector("#selector#").dispatchEvent(new Event("change", {
bubbles: true
}));
}
※#selector# を実際のセレクタに、#target# を設定する値に置き換えてください
なぜか、最初にfocusしないとうまくいきませんでした。
おまけ CaptureScreenshot
var CaptureScreenshot = async (filename) => {
await import("https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js");
html2canvas(document.querySelector("body"), {
onrendered: (canvas) => {
var a = document.createElement('a');
a.href = canvas.toDataURL("image/jpeg").replace("image/jpeg", "image/octet-stream");
a.download = filename;
a.click();
}
});
};
var dateFormat = (date) => {
var y = date.getFullYear(),
m = date.getMonth() + 1,
d = date.getDate(),
h = date.getHours(),
mi = date.getMinutes(),
ss = date.getSeconds();
return y + (('0' + m).slice(-2)) + (('0' + d).slice(-2)) + (('0' + h).slice(-2)) + (('0' + mi).slice(-2)) + (('0' + ss).slice(-2))
};
CaptureScreenshot(`#target#-${dateFormat(new Date())}.jpg`);
※#target# を設定する値に置き換えてください
結構前のSelenium IDEにはスクリーンショットを取るコマンドがあったのですが、最近のはなくなったみたいなので。
もしかすると、SPA等の作りによっては、Wrapper要素の高さをまずauto等に設定してやらないとページ全体のキャプチャが取れないかもです。
効率化のために作ったもの
今回の動機であり元凶であるinputタグをなんとかできたことで、一気に他のタグにも応用できるようになりました。各項目にどうやって値を設定すればいいかがわかっので、効率化するためにツール化してみました。
Selenium Test Case Generator
Github:
https://github.com/taukuma/excel-vba-selenium-test-case-generator
ダウンロード:
https://github.com/taukuma/excel-vba-selenium-test-case-generator/raw/main/SeleniumTestCaseGenerator.xlsm
実際にテストをしたWebフォームのみで動作確認してます。ご利用の際は適宜修正いただく必要があると思いますので、参考程度にしていただければ。
使い方
Excelのツールです。事前設定がいろいろと必要ですが、一度定義さえしちゃえば、コードを書かずに、テストケース(Test Suite)を生成できます。実際にこのツールでテストやデータ投入を自動化できたので、おおきな省力化ができましたし、高価なサービスやツールに頼らなくても、ある程度はできることが証明できたと思ってます。
基本操作方法
SeleniumTestCaseGenerator.xlsmは開くと以下のようになってます。
※Test Case Generation
がメインのシートになります。
テストケースの生成
Test Case GenerationシートのGenerate Test Suite
をクリックすると定義したテストパターンのTest Suiteを生成します。
生成されたTest Suiteは以下のようになってます。
ソースコードを表示
{
"id": "c2b08328-fece-dcbf-d8d4-626b0b2594d6",
"name": "Selenium_TestSuit_20220623175143.side",
"version": "2.0",
"url": "https://wagahai-wa-neko-de-aru.jp",
"tests": [{
"id": "65699b60-e654-7fd6-4a5a-72904db8bb8b",
"name": "TestCase01",
"commands": [{
"id": "6956f3a7-721f-5624-919b-9faccc12de4",
"comment": "",
"command": "open",
"target": "https://wagahai-wa-neko-de-aru.jp",
"targets": [],
"value": "0"
},
{
"id": "25514d73-46e8-3d72-1e94-79fa7b8ee925",
"comment": "",
"command": "run script",
"target": "if(document.querySelector(\"input[name=r-u-cat-or-not]\")) {document.querySelector(\"input[name=r-u-cat-or-not]\").click()}",
"targets": [],
"value": "0"
},
{
"id": "617bcae6-359b-4f96-e28c-9e1fb848a676",
"comment": "",
"command": "run script",
"target": "var CaptureScreenshot = async (filename) => {await import(\"https://cdnjs.cloudflare.com/ajax/libs/html2canvas/0.4.1/html2canvas.js\");document.querySelector(\"body, #virtual-body, div.App__slides, section.Branch__root\").style.height = \"auto\"; html2canvas(document.querySelector(\"body\"), {onrendered: (canvas) => {var a = document.createElement('a');a.href = canvas.toDataURL(\"image/jpeg\").replace(\"image/jpeg\", \"image/octet-stream\");a.download = filename;a.click();document.querySelector(\"body, #virtual-body, div.App__slides, section.Branch__root\").style.height = \"\";}});}; var dateFormat = (date) => {var y = date.getFullYear(), m=date.getMonth()+1,d=date.getDate(),h=date.getHours(),mi=date.getMinutes(),ss=date.getSeconds();return y+(('0'+m).slice(-2))+(('0'+d).slice(-2))+(('0'+h).slice(-2))+(('0'+mi).slice(-2))+(('0'+ss).slice(-2))};CaptureScreenshot(`TestCase01_STEP01-${dateFormat(new Date())}.jpg`);",
"targets": [],
"value": "0"
},
{
"id": "dd82e5cb-74be-ceb7-69fa-b81d9650c87e",
"comment": "",
"command": "run script",
"target": "if(document.querySelector(\"a.next\")) {document.querySelector(\"a.next\").click()}",
"targets": [],
"value": "0"
},
{
"id": "8f2871c0-c731-149d-caba-2941847b0ce",
"comment": "",
"command": "run script",
"target": "if(document.querySelector(\"input[name=name1]\")) {(Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, \"value\").set).call(document.querySelector(\"input[name=name1]\"), \"珍野\"); document.querySelector(\"input[name=name1]\").dispatchEvent(new Event('input', { bubbles: true}));}",
"targets": [],
"value": "0"
},
{
"id": "b929db59-17fc-8043-10a9-ee74dd8cee34",
"comment": "",
"command": "run script",
"target": "if(document.querySelector(\"input[name=name2]\")) {(Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, \"value\").set).call(document.querySelector(\"input[name=name2]\"), \"苦沙弥\"); document.querySelector(\"input[name=name2]\").dispatchEvent(new Event('input', { bubbles: true}));}",
"targets": [],
"value": "0"
},
{
"id": "51f75012-49a0-d4a4-9a28-7c2b85747f4f",
"comment": "",
"command": "run script",
"target": "if(document.querySelector(\"input[name=name1Kana]\")) {(Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, \"value\").set).call(document.querySelector(\"input[name=name1Kana]\"), \"ちんの\"); document.querySelector(\"input[name=name1Kana]\").dispatchEvent(new Event('input', { bubbles: true}));}",
"targets": [],
"value": "0"
},
{
"id": "bb38e13b-278d-e03f-42b4-4d5bae4e2393",
"comment": "",
"command": "run script",
"target": "if(document.querySelector(\"input[name=name2Kana]\")) {(Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, \"value\").set).call(document.querySelector(\"input[name=name2Kana]\"), \"くしゃみ\"); document.querySelector(\"input[name=name2Kana]\").dispatchEvent(new Event('input', { bubbles: true}));}",
"targets": [],
"value": "0"
},
{
"id": "2218b356-39c-188a-f76b-4df42d7ea694",
"comment": "",
"command": "run script",
"target": "if (document.querySelector(\"select[name=birthYear]\")) {document.querySelector(\"select[name=birthYear]\").focus();document.querySelector(\"select[name=birthYear]\").value = \"1867\"; document.querySelector(\"select[name=birthYear]\").dispatchEvent(new Event(\"change\", {bubbles: true}));}",
"targets": [],
"value": "0"
},
{
"id": "defd8f9c-3b82-d3c7-8b3b-bc4fd94485",
"comment": "",
"command": "run script",
"target": "if (document.querySelector(\"select[name=birthMonth]\")) {document.querySelector(\"select[name=birthMonth]\").focus();document.querySelector(\"select[name=birthMonth]\").value = \"2\"; document.querySelector(\"select[name=birthMonth]\").dispatchEvent(new Event(\"change\", {bubbles: true}));}",
"targets": [],
"value": "0"
},
{
"id": "ac4df41e-5995-4887-fd5d-91f87e934f64",
"comment": "",
"command": "run script",
"target": "if (document.querySelector(\"select[name=birthDay]\")) {document.querySelector(\"select[name=birthDay]\").focus();document.querySelector(\"select[name=birthDay]\").value = \"9\"; document.querySelector(\"select[name=birthDay]\").dispatchEvent(new Event(\"change\", {bubbles: true}));}",
"targets": [],
"value": "0"
},
{
"id": "857adb96-1621-dbcc-b9c7-1d9f7daea71a",
"comment": "",
"command": "run script",
"target": "if(document.querySelectorAll(\"input[name=gender]\")) {document.querySelectorAll(\"input[name=gender]\").forEach(v => {if(v.parentNode.textContent.trim() === \"男\") v.click()})}",
"targets": [],
"value": "0"
},
{
"id": "4220b9d4-d156-3144-e68d-6fadec4c4c56",
"comment": "",
"command": "run script",
"target": "var CaptureScreenshot = async (filename) => {await import(\"https://cdnjs.cloudflare.com/ajax/libs/html2canvas/0.4.1/html2canvas.js\");document.querySelector(\"body, #virtual-body, div.App__slides, section.Branch__root\").style.height = \"auto\"; html2canvas(document.querySelector(\"body\"), {onrendered: (canvas) => {var a = document.createElement('a');a.href = canvas.toDataURL(\"image/jpeg\").replace(\"image/jpeg\", \"image/octet-stream\");a.download = filename;a.click();document.querySelector(\"body, #virtual-body, div.App__slides, section.Branch__root\").style.height = \"\";}});}; var dateFormat = (date) => {var y = date.getFullYear(), m=date.getMonth()+1,d=date.getDate(),h=date.getHours(),mi=date.getMinutes(),ss=date.getSeconds();return y+(('0'+m).slice(-2))+(('0'+d).slice(-2))+(('0'+h).slice(-2))+(('0'+mi).slice(-2))+(('0'+ss).slice(-2))};CaptureScreenshot(`TestCase01_STEP1-${dateFormat(new Date())}.jpg`);",
"targets": [],
"value": "0"
},
{
"id": "95a3e136-adb8-47cf-c555-13a88cfa7572",
"comment": "",
"command": "run script",
"target": "if(document.querySelector(\".complete\")) {document.querySelector(\".complete\").click()}",
"targets": [],
"value": "0"
}
]
},
{
"id": "0d21430d-b68d-8b49-b7d7-1e541b0f6e35",
"name": "TestCase2",
"commands": [{
"id": "a42ef73-cfd0-7940-1bf7-4862bb96b1de",
"comment": "",
"command": "open",
"target": "https://wagahai-wa-neko-de-aru.jp",
"targets": [],
"value": "0"
},
{
"id": "2ee6b430-b5d-bb36-10df-606a4e5aa644",
"comment": "",
"command": "run script",
"target": "if(document.querySelector(\"input[name=r-u-cat-or-not]\")) {document.querySelector(\"input[name=r-u-cat-or-not]\").click()}",
"targets": [],
"value": "0"
},
{
"id": "89f695e-e5ef-afdb-27fa-114d778e32bb",
"comment": "",
"command": "run script",
"target": "var CaptureScreenshot = async (filename) => {await import(\"https://cdnjs.cloudflare.com/ajax/libs/html2canvas/0.4.1/html2canvas.js\");document.querySelector(\"body, #virtual-body, div.App__slides, section.Branch__root\").style.height = \"auto\"; html2canvas(document.querySelector(\"body\"), {onrendered: (canvas) => {var a = document.createElement('a');a.href = canvas.toDataURL(\"image/jpeg\").replace(\"image/jpeg\", \"image/octet-stream\");a.download = filename;a.click();document.querySelector(\"body, #virtual-body, div.App__slides, section.Branch__root\").style.height = \"\";}});}; var dateFormat = (date) => {var y = date.getFullYear(), m=date.getMonth()+1,d=date.getDate(),h=date.getHours(),mi=date.getMinutes(),ss=date.getSeconds();return y+(('0'+m).slice(-2))+(('0'+d).slice(-2))+(('0'+h).slice(-2))+(('0'+mi).slice(-2))+(('0'+ss).slice(-2))};CaptureScreenshot(`TestCase2_STEP0-${dateFormat(new Date())}.jpg`);",
"targets": [],
"value": "0"
},
{
"id": "8ba9c573-d674-97c-ee61-3d8e11ed637",
"comment": "",
"command": "run script",
"target": "if(document.querySelector(\"a.next\")) {document.querySelector(\"a.next\").click()}",
"targets": [],
"value": "0"
},
{
"id": "85db7130-84c7-13c8-37b0-6b066d0d061",
"comment": "",
"command": "run script",
"target": "if(document.querySelector(\"input[name=name1]\")) {(Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, \"value\").set).call(document.querySelector(\"input[name=name1]\"), \"名前は\"); document.querySelector(\"input[name=name1]\").dispatchEvent(new Event('input', { bubbles: true}));}",
"targets": [],
"value": "0"
},
{
"id": "507ab946-690-cbb3-b6b1-257179a82a93",
"comment": "",
"command": "run script",
"target": "if(document.querySelector(\"input[name=name2]\")) {(Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, \"value\").set).call(document.querySelector(\"input[name=name2]\"), \"まだない\"); document.querySelector(\"input[name=name2]\").dispatchEvent(new Event('input', { bubbles: true}));}",
"targets": [],
"value": "0"
},
{
"id": "cdd44183-1db1-5151-f450-a4756c1f1317",
"comment": "",
"command": "run script",
"target": "if(document.querySelector(\"input[name=name1Kana]\")) {(Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, \"value\").set).call(document.querySelector(\"input[name=name1Kana]\"), \"なまえは\"); document.querySelector(\"input[name=name1Kana]\").dispatchEvent(new Event('input', { bubbles: true}));}",
"targets": [],
"value": "0"
},
{
"id": "6f663b16-c155-cc4-ee5a-3764cac295d",
"comment": "",
"command": "run script",
"target": "if(document.querySelector(\"input[name=name2Kana]\")) {(Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, \"value\").set).call(document.querySelector(\"input[name=name2Kana]\"), \"まだない\"); document.querySelector(\"input[name=name2Kana]\").dispatchEvent(new Event('input', { bubbles: true}));}",
"targets": [],
"value": "0"
},
{
"id": "ee15d819-2477-cc34-4bc4-96b316217e7f",
"comment": "",
"command": "run script",
"target": "if (document.querySelector(\"select[name=birthYear]\")) {document.querySelector(\"select[name=birthYear]\").focus();document.querySelector(\"select[name=birthYear]\").value = \"1900\"; document.querySelector(\"select[name=birthYear]\").dispatchEvent(new Event(\"change\", {bubbles: true}));}",
"targets": [],
"value": "0"
},
{
"id": "1a8133a-ff21-edd8-894f-856343ad942",
"comment": "",
"command": "run script",
"target": "if (document.querySelector(\"select[name=birthMonth]\")) {document.querySelector(\"select[name=birthMonth]\").focus();document.querySelector(\"select[name=birthMonth]\").value = \"1\"; document.querySelector(\"select[name=birthMonth]\").dispatchEvent(new Event(\"change\", {bubbles: true}));}",
"targets": [],
"value": "0"
},
{
"id": "3731f1e9-ebc5-9d7-366e-e12e6b35502",
"comment": "",
"command": "run script",
"target": "if (document.querySelector(\"select[name=birthDay]\")) {document.querySelector(\"select[name=birthDay]\").focus();document.querySelector(\"select[name=birthDay]\").value = \"1\"; document.querySelector(\"select[name=birthDay]\").dispatchEvent(new Event(\"change\", {bubbles: true}));}",
"targets": [],
"value": "0"
},
{
"id": "2f64abd4-1314-90c5-3438-5949fbe222c5",
"comment": "",
"command": "run script",
"target": "if(document.querySelectorAll(\"input[name=gender]\")) {document.querySelectorAll(\"input[name=gender]\").forEach(v => {if(v.parentNode.textContent.trim() === \"男\") v.click()})}",
"targets": [],
"value": "0"
},
{
"id": "367ca7fa-8d41-8ec-5ad7-b0ff984fddde",
"comment": "",
"command": "run script",
"target": "var CaptureScreenshot = async (filename) => {await import(\"https://cdnjs.cloudflare.com/ajax/libs/html2canvas/0.4.1/html2canvas.js\");document.querySelector(\"body, #virtual-body, div.App__slides, section.Branch__root\").style.height = \"auto\"; html2canvas(document.querySelector(\"body\"), {onrendered: (canvas) => {var a = document.createElement('a');a.href = canvas.toDataURL(\"image/jpeg\").replace(\"image/jpeg\", \"image/octet-stream\");a.download = filename;a.click();document.querySelector(\"body, #virtual-body, div.App__slides, section.Branch__root\").style.height = \"\";}});}; var dateFormat = (date) => {var y = date.getFullYear(), m=date.getMonth()+1,d=date.getDate(),h=date.getHours(),mi=date.getMinutes(),ss=date.getSeconds();return y+(('0'+m).slice(-2))+(('0'+d).slice(-2))+(('0'+h).slice(-2))+(('0'+mi).slice(-2))+(('0'+ss).slice(-2))};CaptureScreenshot(`TestCase2_STEP1-${dateFormat(new Date())}.jpg`);",
"targets": [],
"value": "0"
},
{
"id": "b98d4da9-75a6-7a7c-c1f7-858f71c2dd2",
"comment": "",
"command": "run script",
"target": "if(document.querySelector(\".complete\")) {document.querySelector(\".complete\").click()}",
"targets": [],
"value": "0"
}
]
}
]
}
フォーム要素の定義
まず、事前準備としては、テストしようとしているWebページにどんな要素があるかを調べます。基本的な対象は入力項目・選択項目・クリック項目です。調べながらtarget master
シートを埋めていきます。
項目 | 意味 |
---|---|
Target Selection | 入力項目の項目名です。テストパターン定義の際に参照する値になります |
Selector | DOMのセレクタです。Webページ全体で一意になるように設定します。idやclass、nameを定義する感じです。 |
テスト項目の定義
Seleniumシートでテストパターンを定義します。
上述のフォーム要素の定義とあわせて、どの項目をどのページで設定するのかを定義します。このとき、入力項目
欄にはtarget masterシートで設定したTarget Selectionから選択して入力します。コマンドは後述するcommand master
で定義してあるコマンドを選びます(click、check、input、radio、select等)。
テストパターンの定義
各ページの入力項目の定義が終わったら、今度はテストパターンごとに、どの項目になにを入れるかを設定します。
この部分がこのツールで一番こだわったところです。一度入力項目やコマンドを定義しちゃえば、簡単にテストパターンを作っていけるようになります。
テストパターンは列を増やしていけば、何パターンでも定義できます。
使えるコマンドを増やす
command masterシートにSelenium IDEのCommand
Target
Value
を書いていけば、どんなコマンドでもテストパターンに含めることができます。
特にrun script
コマンドは強力なのでこれだけ知っていれば、大抵のことはなんとかできると思ってます。
おわりに
React作られた全てのWebページに使えるとは思っていませんが、同じ境遇の方にとって、少しでも参考になれば幸いです。
決してReactを嫌っているわけではありません!
良さは(何となくですが)わかってるつもりです...