JavaScriptを使ってDOMを操作し、
Webページに情報を追加するようなWebサービスに対して、
良い感じに単体テストを書けないかを考えてみました。

検討した方法では、
適用できないことも多いと思いますが、
適用できる場合には、テストの効率化に役立つと思うので、
メモとして残しておきます。

このエントリで使用している言語やライブラリとそれらのバージョンは次の通りです。

  • Python 3.7.12
  • pytest 7.1.2
  • mock 4.0.3
  • Js2Py 0.71
  • Flask 2.1.3

想定するWebサービス

このエントリで、想定するWebサービスは、
数値を2つ渡すとその和を、id=resultのタグ配下に挿入するサービスです。

  • 入力:
    • a: 数値
    • b: 数値
  • 出力: id=resultのタグ配下にa+bの結果を挿入するJavaScript

Webサービスの作成

それではFlaskで、Webサービスを作っていきます。

作業ディレクトリと仮想環境を作り、Flaskをインストールします。

mkdir flask-jstest-study && cd $_
python -m venv venv
. venv/bin/activate
pip install Flask

main処理を実装します。

main.py

from flask import Flask

app = Flask(__name__)

@app.route('/')
def home():
    return '<html><div id="result"/><script src="/add/1+2"></script></html>'

@app.route('/add/<int:a>+<int:b>')
def add(a, b):
    result = a + b
    return f"""
      let div = document.getElementById('result');
      div.appendChild(document.createTextNode('{a + b}'));
    """

Flaskアプリケーションを実行します。

export FLASK_APP=main.py
export FLASK_ENV=development
flask run

http://localhost:5000/ にアクセスすると、
1+2の計算結果の「3」が表示されることが確認できます。

テストの追加

Webサービスにテストを追加します。

テストに使用するライブラリをインストールします。

pip install pytest
pip install js2py
pip install mock

テストコードを実装します。

HTMLDocumentクラスでは、
xml.dom.minidomでgetElementByIdを使うために、
以下のブログのコードを拝借しています。

Python の xml.dom.minidom で GetElementById をするメモ | bunji square
https://bunji2.hatenablog.com/entry/2016/10/09/211046

test_main.py

import pytest
import mock
import js2py
import xml.dom.minidom
from xml.dom.minidom import Node
import main

class HtmlDocument(xml.dom.minidom.Document):
  # getElementByIdを使うためのメソッドの書き換え
  # see. https://bunji2.hatenablog.com/entry/2016/10/09/211046
  def getElementById(self, id):
    def _get_element_by_id_helper(parent, id):
      for node in parent.childNodes:
        if node.nodeType == Node.ELEMENT_NODE and \
                node.getAttribute("id") == id:
          return node
        r = _get_element_by_id_helper(node, id)
        if r:
          return r
      return None

    return _get_element_by_id_helper(self.documentElement, id)

@pytest.mark.parametrize(
    "a,b,expect_xml",
    [
        (1, 2, '<div id="result">3</div>'),
        (1, -2, '<div id="result">-1</div>')
    ]
)
def test_add(a,b,expect_xml):
  # HTMLのDOMを準備 <html><div id="result"></html>
  doc = HtmlDocument()
  doc.appendChild(doc.createElement("html"))
  div = doc.createElement("div")
  div.setAttribute("id", "result")
  doc.documentElement.appendChild(div)

  # JavaScriptのContext準備、documentにDOMを設定
  context = js2py.EvalJs()
  context.document = mock.MagicMock(wraps=doc)

  # サービスの名処理呼び出し・結果のJavaScriptの実行
  context.execute(main.add(a, b))

  # 処理実行後のDOMの検証
  div_result = context.document.getElementById("result")
  assert div_result.toxml() == expect_xml

テストは、以下のように実行します。

pytest test_main.py  

xml.dom.minidomが、HTMLのDOMの機能を持っているわけでは無いので、
document.writeができない、innerHTMLが使えないなど、
不足する部分があって適用しにくいケースも多いと思うので、参考程度に。

以上。