DOM操作を含むJavaScriptコードをJestでテストした - マトリョーシカ的日常

マトリョーシカ的日常

ワクワクばらまく明日のブログ。

DOM操作を含むJavaScriptコードをJestでテストした

前書き

 午前中ずっと降り続いていた雨は突然として止み、子供らが庭にわらわらと集まってきた。最近はけん玉がブーム用で、お互いが技を繰り出している。空気中のちりほこりが雨で洗い流されたためか、空気澄んでいる。平和な世界だ。

Webサービスの話

 WEBサービスを引き続きつくっている。JavaScriptのテストを書いていた。入力フォームに値を入れて保存を押すと、任意のJavaScriptのオブジェクトの値が変わる。実際にはしっかりと動作していているが、これをテストするのに少し苦労した。忘れても良いように、ここに書いておく。

 JavaScriptのテストツールはJestを導入した。ドキュメントも日本語のものがあり、参考になるページが多くありそうだったためだ。JestだけでなくTesting Libraryというツールも導入する。

Jest · 🃏 Delightful JavaScript Testing

Testing Library | Testing Library

コードの紹介

 はじめにテストしたい関数をexportする。今回はapp/javascript/src/simulation.js の一部である。

//略
export function setObjectparams(e, params, facilities, facilityDialog) {
  e.preventDefault(); 
  let id = params.id;
  let name = params.name;
  let processingTime = params.processingTime;
  let selectedFacility = facilities.find((facility) => facility.id == id.value);
  selectedFacility.name = name.value;
  selectedFacility.processingTime = processingTime.value;
  facilityDialog.close;
}

// 略
export function setFacilityDataToModal(facility) {
  let id = document.getElementById("hidden-id");
  let name = document.getElementById("name");
  let processingTime = document.getElementById("processingTime");

  id.value = facility.id;
  name.value = facility.name;
  processingTime.value = facility.processingTime;
}

 次にテストファイルを作成する。app/javascript/test/savesimulation.test.js とした。

import { fireEvent, screen, waitFor } from "@testing-library/dom";
import "@testing-library/jest-dom";

import { setObjectparams } from "../src/simulation.js";
import { setFacilityDataToModal } from "../src/simulation.js";
let facilities;
let facilitiesInitial;

beforeEach(() => {
//略
  facilities = facilitiesInitial;
});

test("button click and set params", async () => {
// ここにテストを書く
});

解説

 import { fireEvent, screen, waitFor } from "@testing-library/dom"; で @testing-library/dom から fireEvent などのオブジェクトを扱えるようになる。

 import { setObjectparams } from "../src/simulation.js"; でローカルのファイルからexportされた関数 setObjectparams が使える。

 beforeEach は各テストが実行される前に処理される部分。 test(.. はテストだ。今回は非同期処理を含むので、引数の指定に非同期処理の関数 async が入っている。

テストを書く

1. テスト用にDOMをセットアップ

 実際のサービスで使うDOMでなく、テスト用のDOMを準備する。

test("button click and set params", async () => {
  const div = document.createElement("div");
  div.innerHTML = `
    <dialog id="facilityDialog">
      <div id="hidden-id" hidden></div>
      <div>
        <label for="name">設備名</label>
        <input type="text" id="name" name="name" maxlength="16" size="15" />
      </div>
      <div>
        <label for="processingTime">加工時間</label>
        <input type="number" id="processingTime" name="name" min="0" max="400" />
      </div>
      <div>
        <button id="confirmBtn" value="default">保存</button>
      </div>
    </dialog>
  `;
  document.body.appendChild(div); 

2. 値のセットと値の取得

 実際の作成した関数 setFacilityDataToModal() を使って、テスト用のDOMの入力フォームに値をセットしておく。それができたら次はDOM上から名前と加工時間を取得する。

 fireEvent.change を使うことでフォームに値を入力させることができる。

 あとはボタンを押した時のイベントを追加する。

  document.body.appendChild(div); 
  setFacilityDataToModal(facilities[0]);

  const nameInput = screen.getByLabelText("設備名");
  const processingTimeInput = screen.getByLabelText("加工時間");

  fireEvent.change(nameInput, { target: { value: "Test Facility" } });
  fireEvent.change(processingTimeInput, { target: { value: 30 } });

  const button = screen.getByText("保存");
  button.addEventListener("click", (e) => {
    let params = {};
    params.id = document.getElementById("hidden-id");
    params.name = document.getElementById("name");
    params.processingTime = document.getElementById("processingTime");
    let facilityDialog = document.getElementById("facilityDialog");

    setObjectparams(e, params, facilities, facilityDialog);
  });

 

3. DOM操作が終わったあとに差異をみる

 テストコードの中でボタンを押し、そのイベントによってDOMが操作される。(今回はモーダルウィンドウが閉じる)さらにはJavaScriptの変数の中身が変わる。それをテストする。

//上のコードのつづき
  await fireEvent.click(button);

  await waitFor(() => expect(screen.queryByText("設備名")).not.toBeVisible());
  let selectedFacility = facilities.find((facility) => facility.id == "n0");
  expect(selectedFacility.processingTime).toBe("30");
  expect(selectedFacility.name).toBe("Test Facility");

waitFor... を使うことで、モーダルが閉じてからそれ以降のアサーションを実行できる。モーダルウィンドウが閉じるのはDOMがなくなるわけではなく、見えなくなるだけである。だからnot.toBeVisible() を使っている。

以上。だいぶ長くなってしまった。  

Unsplash Marek Piwnicki