📋

XSLTをユニットテストする(XSpec)

6 min read

「Extensible Stylesheet Language (XSL)関連」といったら、こんな話もありなのかな?

https://adventar.org/calendars/5027
ということで、15日目として参加してみます。

XSLTでもユニットテストしたい

プログラム開発を続けているうちに、ユニットテストが必要になる日がやって来ます。言語がXSLTでも、それは同じです。

そんな日の役に立つ、XSpecというフレームワークを紹介します。

https://github.com/xspec/xspec

テストの書きかたとかまで説明しだすとキリがないので、ここでは、テストの実行や得られる結果の、ほんのさわりの紹介だけにとどめます。

XSpecとは

XSLTをユニットテストするためのフレームワークです。

『Beginning XSLT』などの著作もあるJeni Tennisonさんが2005年に原型を開発し、改良して2008年に名を改めてリリースしたのが、始まりのようです。
メンテナーが替わりながら、2020年12月現在も、改良やリリースは続いています。

ここでは例として取りあげませんが、XSLTだけでなく、XQueryやSchematronもテストできます。

XSpecの特長

依存関係が少ない

XSpec自体がXSLTで実装されているので、Saxon(Java版)さえあれば動きます。

シンプルな記法

テストシナリオの記法は、入力に対して期待する出力結果を書いていく、シンプルなものです。
いま現在XSLTを書けている人なら、難しくないと思います。

シナリオ例
<x:scenario label="h1要素をcapitalizeモードで変換すると">
  <x:context mode="capitalize">
    <h1>hello</h1>
  </x:context>

  <x:expect label="先頭が大文字になるはず">
    <h1>Hello</h1>
  </x:expect>
</x:scenario>

ここでは、x:contextの中が、テスト用の入力です。
その入力に対して期待される出力結果が、x:expectの中に書いてあります。

XSLT 3.0対応

mapやarrayの入出力や、xsl:packageのテストもできます。
特定の入力に対してエラーの発生を期待するシナリオも書けます。

エラー発生を期待するシナリオの例
<x:scenario label="my:func()の引数が負のとき"
            catch="true">
  <x:call function="my:func">
    <x:param select="-1" />
  </x:call>

  <x:expect label="エラーXTTE0570になるはず"
            test="?err?code"
            select="xs:QName('err:XTTE0570')" />
</x:scenario>

XSpecのインストール

XSLTの開発にOxygenを使っている場合、インストール作業は不要です。
Oxygenに最初からXSpecが組み込まれています。

  1. Javaをインストールしておきます。
    Saxonを動かすためなので、バージョンはJava 8、もしくはそれ以降。
    JREだけで可。JDKは不要。

  2. Saxon(Java版)をダウンロードして、.jarファイルをどこかに置きます。
    2020年12月現在、いちばん無難なバージョンは9.9.1.8です。
    とにかく試してみたいだけなら、https://repo1.maven.org/maven2/net/sf/saxon/Saxon-HE/9.9.1-8/Saxon-HE-9.9.1-8.jarをダウンロードするだけ。

  3. XSpec本体は、レポジトリをクローンするだけでよいです。
    要は、https://github.com/xspec/xspec/archive/master.zipをダウンロードして、.zipファイルの中身をどこかに展開します。

これで、実行準備は完了です。

とりあえずテストを実行してみる

何はともあれ、XSpecレポジトリにあるサンプルを使って、テストを実行してみましょう。

XSLTの開発にOxygenを使っている場合は、コマンドラインで実行する必要はありません。
.xspecファイルをOxygenで開いて、Apply Transformation Scenario(s) ボタン(赤い右矢印のやつ)を押せば、テストが実行されます。

  1. Windowsならコマンドプロンプト、*nixならターミナルを開きます。

  2. 環境変数SAXON_CPに、Saxonの.jarファイルのパスをセットします。

    • Windows(コマンドプロンプト)

      set SAXON_CP=C:\path\to\saxon9he.jar
      
    • Windows(PowerShell)

      $env:SAXON_CP = "C:\path\to\saxon9he.jar"
      
    • *nix

      export SAXON_CP=/path/to/saxon9he.jar
      
  3. XSpecレポジトリのbin フォルダの、xspec.bat(Windows)またはxspec.sh(*nix)を実行します。
    引数に、テストシナリオのファイル(.xspecファイル)を指定します。

    引数に指定するのは、.xspecファイルです。
    XSLTファイル(.xslファイル)ではありません。

    ここでは、XSpecレポジトリに存在するtutorial/escape-for-regex.xspecを指定してみます。

    • Windows

      bin\xspec.bat tutorial\escape-for-regex.xspec
      
    • *nix

      bin/xspec.sh tutorial/escape-for-regex.xspec
      
  4. テストシナリオがコンパイルされ、実行されます。

    C:\xspec>set SAXON_CP=C:\Saxon\saxon9he.jar
    
    C:\xspec>bin\xspec.bat tutorial\escape-for-regex.xspec
    Creating XSpec Directory at C:\xspec\tutorial\xspec...
    
    Creating Test Stylesheet...
    
    
    Running Tests...
    Testing with SAXON HE 9.9.1.8
    No escaping
    Must not be escaped at all
    Test simple patterns
    ..When encountering parentheses
    escape them.
    ..When encountering a whitespace character class
    escape the backslash
    result should have one more character than source
    When processing a list of phrases
    All phrase elements should remain
    Strings should be escaped and status attributes should be added
          FAILED
    
    Formatting Report...
    passed: 5 / pending: 0 / failed: 1 / total: 6
    Report available at C:\xspec\tutorial\xspec\escape-for-regex-result.html
    Done.
    

    最後のテストシナリオ(Strings should be escaped and status attributes should be added)でFAILEDと出ます。そのシナリオでテストが失敗したということです。

テスト結果レポート

テスト結果のレポートを見てみましょう。

テスト実行時にReport available at C:\xspec\tutorial\xspec\escape-for-regex-result.htmlのように表示されていたとおり、そこにHTMLファイル形式のレポートが作成されています。
Webブラウザで開いて、失敗したシナリオの部分に注目すると、こんな感じです:

テスト結果レポート(の一部)
テスト結果レポート(の一部)

  • 左側(Result)が、テストシナリオで設定した入力に対して、XSLTから出力された結果。

  • 右側(Expected Result)が、期待していた出力です。

濃いピンクの部分が、結果と期待とが異なっているところです。
テストしたXSLTのどこかで、@status属性の値設定をまちがえてる、ということですね。

テストカバレッジ

テストカバレッジをざっくり見ることもできます。

カバレッジ取得の実行例

コマンドラインだと-cオプションです:

C:\xspec>bin\xspec.bat -c tutorial\coverage\demo.xspec
Creating XSpec Directory at C:\xspec\tutorial\coverage\xspec...

Creating Test Stylesheet...


Running Tests...
Collecting test coverage data...
****************************************
controller=net.sf.saxon.trans.XsltController@3c46e67a
Testing with SAXON HE 9.9.1.8
'iron' element
is transformed to 'shield' element

Formatting Report...
passed: 1 / pending: 0 / failed: 0 / total: 1

Formatting Coverage Report...
Report available at C:\xspec\tutorial\coverage\xspec\demo-coverage.html
Done.

テストカバレッジのレポート
テストカバレッジのレポート

レポートには、テスト対象のXSLT(xsl:includexsl:importされたものも含む)の全行が、色分けされて表示されています。

赤い行が、テストシナリオの実行中に、一度も実行されなかった行です。
少なくとも、これらの行が実行されるようなテストを追加したほうがよい、ということですね。

使いどころ

開発のベースとなるXSLT/XQueryライブラリにユニットテストを書いておくと、特に有用です。リファクタリングや、依存関係を変更するときの、効率と安全性が格段に違います。

ドキュメントの形式的構造は別のスキーマでチェックして、内容寄りのチェックはSchematronスキーマで行う、というケースも多いと思います。テスト無しの状態でSchematronにエンバグすると気付きにくいので、そこでもユニットテストが有用です。


以上、XSLT(やXQueryやSchematron)にもユニットテストのフレームワークがあるという話でした。

Discussion

ログインするとコメントできます