準備
ReScriptでJestを使うを参考に設定を整えます。
npm create rescript-app@latest
npm install --save-dev @glennsl/rescript-jest
mkdir __tests__
rescript.json
{
"name": "jest-test",
"sources": [
{
"dir": "src",
"subdirs": true
},
{
"dir": "__tests__",
"type": "dev"
}
],
"package-specs": {
"module": "commonjs",
"in-source": true
},
"suffix": ".res.js",
"bs-dependencies": [
"@rescript/core"
],
"bs-dev-dependencies": [
"@glennsl/rescript-jest"
],
"bsc-flags": [
"-open RescriptCore"
]
}
package.json
{
"name": "jest-test",
"version": "0.0.0",
"scripts": {
"res:build": "rescript",
"res:clean": "rescript clean",
"res:dev": "rescript -w",
"test": "jest"
},
"keywords": [
"rescript"
],
"author": "",
"license": "MIT",
"dependencies": {
"@rescript/core": "^1.5.0",
"rescript": "^11.1.1"
},
"devDependencies": {
"@glennsl/rescript-jest": "^0.11.0"
}
}
Unit Testにかけるファイル
ReScriptでDOMを扱うで作成したものを使います。
SousekiForm.res
module Document = {
@send external getElementById: (Dom.document, string) => Dom.element = "getElementById"
@send external getElementsByName: (Dom.document, string) => Dom.nodeList = "getElementsByName"
@get external getBody: Dom.document => Dom.element = "body"
}
module Element = {
@send external addEventListener: (Dom.element, string, () => unit, bool) => unit = "addEventListener"
@get external getDataset: Dom.element => Dom.domStringMap = "dataset"
@get external getValue: Dom.element => string = "value"
@get external getChecked: Dom.element => bool = "checked"
@set external setInnerHTML: (Dom.element, string) => unit = "innerHTML"
}
module NodeList = {
@get external getLength: Dom.nodeList => int = "length"
@send external item: (Dom.nodeList, int) => Dom.element = "item"
}
module DomStringMap = {
@get external getCorrectChoice: Dom.domStringMap => string = "correctChoice"
}
// 目的:設問部のinputのNodeをReScriptのlistに格納する
// getListedNodeList : Dom.nodeList -> Dom.element list
let getListedNodeList = nodeList => {
let length = nodeList->NodeList.getLength
// 目的: nodeList->item(0)からnodeList->item(i)までのDom.elementをReScriptのlistに格納する
// loop : int -> Dom.element list
let rec loop = i =>
if i < 0 {list{}}
else {list{nodeList->NodeList.item(i), ...loop(i - 1)}}
loop(length - 1)
}
/* Js.List.foldRight を使うと require が必要でブラウザ実行がめんどう */
// 目的: initから始めてlstの要素を右から順にfに流し込む
// foldRight : ('a, -> 'b -> 'b) -> 'a list -> 'b
let rec foldRight = (f, lst, init) => switch lst {
| list{} => init
| list{first, ...rest} => f(first, foldRight(f, rest, init))
}
// 目的: 指定された選択肢のvalueを返す
// getAnswer : (Dom.element -> bool) -> Dom.element list -> string
let getAnswer = (lst, f) => foldRight(
(first, rest_value) => if f(first) {first->Element.getValue} else {rest_value},
lst,
""
)
// 目的: ボタンがクリックされたときの処理
// onBtnClick : unit -> unit
let onBtnClick = () => {
let listedNodeList = document->Document.getElementsByName("Q1")->getListedNodeList
let correctAnswer = listedNodeList->getAnswer(e => e->Element.getDataset->DomStringMap.getCorrectChoice == "true")
let selectedAnswer = listedNodeList->getAnswer(Element.getChecked)
let judgement = correctAnswer == selectedAnswer ? "正解です!" : "不正解です。"
let message = "正解は" ++ correctAnswer ++ "。あなたの回答は" ++ selectedAnswer ++ "でした。"
document->Document.getBody->Element.setInnerHTML(judgement ++ "<br>" ++ message)
}
document->Document.getElementById("btn")->Element.addEventListener("click", onBtnClick, false)
souseki_form.html
<!doctype HTML>
<html lang="ja">
<head>
<title>漱石先生クイズ</title>
</head>
<body>
<form>
<dl>
<dt>Q1. 漱石先生のデビュー作は?</dt>
<dd><p>漱石先生のデビュー作は次のうちどれでしょう?</p>
<p><input type="radio" name="Q1" value="坊っちゃん" checked>坊っちゃん<br>
<input type="radio" name="Q1" value="吾輩は猫である" data-correct-choice="true">吾輩は猫である<br>
<input type="radio" name="Q1" value="明暗">明暗</p>
</dd>
</dl>
<button id="btn" type="button">答え合わせ</button>
</form>
<script type="module" src="SousekiForm.res.js"></script>
</body>
</html>
Unit Test
DOMに関係ないところで試験してみます。
__tests__
ディレクトリに以下のファイルを作成します。
open Jest
open SousekiForm
describe("foldRight", () => {
open Expect
test("concat", () => {
expect(foldRight((a, b) => a ++ b, list{"春", "夏", "秋", "冬"}, "")) -> toBe("春夏秋冬")
})
})
ディレクトリ構成
jest-test/
├── rescript.json
├── package.json
├── src/
│ └── SousekiForm.res
│ └── souseki_form.html
└── __tests__/
└── SousekiForm_test.res
ビルドとテストの実行
npm run res:build
npm run test
実行結果
FAIL __tests__/SousekiForm_test.res.js
● Test suite failed to run
The error below may be caused by using the wrong test environment, see https://jestjs.io/docs/configuration#testenvironment-string.
Consider using the "jsdom" test environment.
ReferenceError: document is not defined
失敗しました。
"jsdom" test environment
を使いましょうと言われています。
jsdom
を使えるようにする方法
package.json
に書く
"jest": {
"testEnvironment": "jsdom"
}
jest.config.js
を作成して、そこに書く
module.exports = {
testEnvironment: 'jsdom'
}
docblockに記述する
/**
* @jest-environment jsdom
*
この方法が一番よいと思ったんですが、ReScriptでコメントを書いても、JavaScriptにコンパイルする際に消えてしまいます。
残せる良い方法が分からなかったので諦めました。
再度、ビルドとテスト
package.json
に追記する方法を採用しました。
ビルドしてテストします。
FAIL __tests__/SousekiForm_test.res.js
● Test suite failed to run
TypeError: Cannot read properties of null (reading 'addEventListener')
57 | }
58 |
> 59 | document.getElementById("btn").addEventListener("click", onBtnClick, false);
| ^
なんでこのエラーが出るのか分からなかったです。考えていたら、分かった気がしました。
-
SousekiForm_test.res
でopen SouekiForm
としてます - そのためコンパイルされた
SousekiForm_test.res.js
にrequire("../src/SousekiForm.res.js")
が含まれます -
SousekiForm.res.js
のトップレベルにdocument.getElementById("btn").addEventListener("click", onBtnClick, false);
があります - モジュール読み込み時にこれが実行され「
null
に対してaddEventListner
はできないよ」と言われています
モジュールを読み込む前にdocument.getElementById("btn")
が値を持つようにすれば良いです。
document.getElementById("btn")
に値を持たせる
"jest": {
...
"testEnvironmentOptions": {
"html": "<button id=\"btn\"></button>"
}
...
}
testEnvironmentOptions
のhtml
にid="btn"
を含む要素を渡しました。1
実行結果
PASS __tests__/SousekiForm_test.res.js
foldRight
✓ concat (1 ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
うまくいきました。
他の関数もテストする
let form = `
<form>
<input type="radio" name="Q1" value="坊っちゃん" checked>坊っちゃん
<input type="radio" name="Q1" value="吾輩は猫である" data-correct-choice="true">吾輩は猫である
<input type="radio" name="Q1" value="明暗">明暗
</form>
`
document->Document.getBody->Element.setInnerHTML(form)
let lst = document->Document.getElementsByName("Q1")
let element1 = lst->NodeList.item(0)
let element2 = lst->NodeList.item(1)
let element3 = lst->NodeList.item(2)
describe("getListedNodeList", () => {
open Expect
test("three forms", () => {
expect(lst->getListedNodeList) -> toEqual(list{element3, element2, element1})
})
})
describe("getAnswer", () => {
open Expect
test("correct answer", () => {
expect(lst->getListedNodeList->getAnswer(e => e->Element.getDataset->DomStringMap.getCorrectChoice == "true")) -> toBe("吾輩は猫である")
})
test("selected answer", () => {
expect(lst->getListedNodeList->getAnswer(Element.getChecked)) -> toBe("坊っちゃん")
})
})
PASS __tests__/SousekiForm_test.res.js
foldRight
✓ concat (1 ms)
getListedNodeList
✓ three forms
getAnswer
✓ correct answer (1 ms)
✓ selected answer
Test Suites: 1 passed, 1 total
Tests: 4 passed, 4 total
うまくいきました。
ブラウザで動作確認
Live Serverを起動して、Chromeで確認すると動作はしてました。
ただConsoleにはエラーが表示されます。
SousekiForm.res.js:61 Uncaught
ReferenceError: exports is not defined
at SousekiForm.res.js:61:1
いまCommon JSフォーマットになるようにコンパイルしているからですね。
JavaScript module フォーマットにしてテスト
"package-specs": {
...
"module": "esmodule",
...
}
FAIL __tests__/SousekiForm_test.res.js
● Test suite failed to run
Jest encountered an unexpected token
Jest failed to parse a file. This happens e.g. when your code or its dependencies use non-standard JavaScript syntax, or when Jest is not configured to support such syntax.
Out of the box Jest supports Babel, which will be used to transform your files into valid JS based on your Babel configuration.
By default "node_modules" folder is ignored by transformers.
Here's what you can do:
• If you are trying to use ECMAScript Modules, see https://jestjs.io/docs/ecmascript-modules for how to enable it.
• If you are trying to use TypeScript, see https://jestjs.io/docs/getting-started#using-typescript
• To have some of your "node_modules" files transformed, you can specify a custom "transformIgnorePatterns" in your config.
• If you need a custom transformation specify a "transform" option in your config.
• If you simply want to mock your non-JS modules (e.g. binary assets) you can stub them out with the "moduleNameMapper" config option.
You'll find more details and examples of these config options in the docs:
https://jestjs.io/docs/configuration
For information about custom transformations, see:
https://jestjs.io/docs/code-transformation
Details:
/Users/segi/Desktop/jest-test/__tests__/SousekiForm_test.res.js:3
import * as Jest from "@glennsl/rescript-jest/src/jest.res.js";
^^^^^^
SyntaxError: Cannot use import statement outside a module
at Runtime.createScriptFromCode (node_modules/jest-runtime/build/index.js:1728:14)
Test Suites: 1 failed, 1 total
Tests: 0 total
またエラーです。
Troubleshooting
解決方法がTroubleshootingにあったので、やってみます。
npm install --save-dev esbuild-jest esbuild
npx rescript build -with-deps
"jest": {
...
"transform": {
"^.+\\.jsx?$": "esbuild-jest"
},
"transformIgnorePatterns": ["<rootDir>/node_modules/(?!(rescript|@glennsl/rescript-jest)/)"]
...
}
再々度、ビルドとテスト
PASS __tests__/SousekiForm_test.res.js
foldRight
✓ concat (1 ms)
getListedNodeList
✓ three forms
getAnswer
✓ correct answer (1 ms)
✓ selected answer
Test Suites: 1 passed, 1 total
Tests: 4 passed, 4 total
うまくいきました。2
Chromeで実行してもエラーはでません。
関数を修正する
getAnswer(list<Dom.element>, Dom.element => bool)
でfoldRight
を使っていますが「find
の方が良いのかな」と思ったので修正します。
let getAnswer = (lst, f) => switch List.find(lst, f) {
| Some(e) => e->Element.getValue
| None => ""
}
ビルド、テスト、エラー、修正、ビルド、テスト、成功
FAIL __tests__/SousekiForm_test.res.js
● Test suite failed to run
Jest encountered an unexpected token
Details:
/Users/segi/Desktop/jest-test/node_modules/@rescript/core/src/Core__List.res.js:3
import * as Caml_option from "rescript/lib/es6/caml_option.js";
^^^^^^
SyntaxError: Cannot use import statement outside a module
同じようなエラーがでました。
@rescript
配下がtransform対象になっていないからかも知れません。
"transformIgnorePatterns": ["<rootDir>/node_modules/(?!(@rescript|rescript|@glennsl/rescript-jest)/)"]
@rescript
を追加して、ビルド、試験すると成功しました。
PASS __tests__/SousekiForm_test.res.js
再びブラウザで動作確認
Uncaught TypeError: Failed to resolve module specifier "@rescript/core/src/Core__List.res.js". Relative references must start with either "/", "./", or "../".Understand this error
エラーが出て動作しません。
モジュールがうまく読み込めないようです。
これは「バンドラ」というものの出番なのでしょうか。
次はバンドラを勉強します。
関数の再修正
let rec getAnswer = (lst, f) => switch lst {
| list{} => ""
| list{first, ...rest} => if f(first) {first->Element.getValue} else {getAnswer(rest, f)}
}
List.find
を使ったためにモジュール利用になったので、高階関数を利用しない再帰で書き直しました。
これでfoldRight
も不要になったので、消したものを完成版としてます。