TeXだってテスト書きたい

これはTeX & LaTeX Advent Calendar 2014,14日目の記事です.
13日目はneruko3114さん,15日目はh-kitagawaさんです.

はじめに

TeXでマクロを記述するにあたり,問題になるのはマクロが想定した動作をするか否かである.他のプログラミング言語*1と比較しても,TeXキチガイとしか言いようのな文法と評価規則をもち,思ったような動作をさせるのに苦労する.LaTeXのパッケージを公開するなら,せめてテストくらいしておきたい.特に,マクロでよくわからないことを実装しようとする私のような人間には切実な問題である.実は,これをサポートするパッケージがある.今回は,そのパッケージqstest.styを紹介する.

qstest.styとは

CTANにも登録されているLaTeXパッケージで,LaTeXにunit testを提供する.2007年のTUGboat記事がある.

基本的な使い方は,次のソースコードのようになる.

\documentclass{article} % 実際は何でもよい
\usepackage{qstest}
\IncludeTests{*}
\LogTests{lgout}{*}{*}
\begin{document}
  \begin{qstest}{Example}{label}
     \Expect*{\the\numexpr1+2}{3}
  \end{qstest}  
\end{document}

まず,qstest.styを使うために\usepackage{qstest}とする.

\IncludeTestsは実際に実施するテストに付加されたキーワード*2を指定する.テスト用に別ファイルを用意するなら*,すなわちすべてのテストを実施するように指定するのがよいだろう.テストを別ファイルに定義しておき,時間のかからないテストだけ実施するようなことも可能である.

次の\LogTestsはログファイルへの出力設定を行う.第一引数はログファイルの拡張子で,この例の場合,ログは\jobname.lgoutというファイルに出力される.第二引数は成功したテストのうち,ログへ出力するもののキーワード,第三引数は失敗したテストのうちログへ出力するもののを指定する.大抵の場合,すべてのログを記録するため,\LogTests{lgout}{*}{*}としておけばよいだろう.

テストケースはqstest環境に記述する.環境に与える引数は,テストケース名とキーワード*3である.

qstest環境中では,値の確認を\Expectを使って行う.\Expectは\Expect{確かめたい値}{予想する結果}のように使用する.要はassertである.引数を展開したい場合は,開き波括弧の前に*をつける.先に挙げた例でも,\the\numexpr1+2を展開するために*を使用している.もちろん,予想する結果の方を展開することもできる.ただし,展開には\edefを使用しているため,代入などの副作用をもっている場合には,注意が必要である.

値の確認に関しては,\Expect以外にもマクロが用意されている.確かめたい長さが特定の範囲に含まれるかを確認する\InRangeなど,TeXに特化したものもあるので,興味のある人はマニュアルを読んでみるといいだろう*4

先の例を実行すると,ログファイルに次の出力を得る*5.これは,Exampleテストをパスしたことを表している.

Passed: Example


テストにパスしなかった場合も示しておこう.次の例を考える.

\documentclass{article}
\usepackage{qstest}
\IncludeTests{*}
\LogTests{lgout}{*}{*}
  \begin{document}
    \begin{qstest}{FailExample}{label}
    \Expect*{\the\numexpr1+2}{hoge}
  \end{qstest}
\end{document}

これを実行すると,コンソールに予想していた値と実際の値が出力される.

! Package qstest Error: Failed: FailExample
 \Expect: \the \numexpr 1+2
 <hoge
 >3.

この後,実行を続けるか否かの入力を求められる*6ので,ENTERを押して実行を続け,ログファイルに次の出力を得る.

Failed: FailExample
 \Expect: \the \numexpr 1+2
 <hoge
 >3

副作用のみが重要なマクロに対する知見

さて,\Expectで展開しようとした場合,\edefを使うというということは先に述べた.これにより,\Expectでは内部で\defなどの代入を伴うプリミティブを単純にテストできない.LISP on TeXのテストコードを記述しようとした際,これは大きな問題だった.ここで,私はLISP on TeXのマクロのほとんどが空のトークンに展開され,副作用のみが重要であるという特徴を利用した.

LISP on TeXのコードでは大きすぎるため,次の例を考える.マクロ\hogeは二引数をとり,\fugaに第一引数と第二引数の和を代入することを目的としたマクロである.当然,展開結果には空のトークン列であることを想定している.

\newcount\fuga
\newcommand{\hoge}[2]{
  \fuga=#1
  \advance\fuga by #2
}

これをテストするために

\Expect*{\hoge{4}{2}\the\fuga}{6}

と記述することはできない.\fugaへの代入や\advenceが展開できないため,次のようにテストをパスできないことがログに残る.

Failed: Example
 \Expect: \hoge {4}{2}\the \fuga 
 <6
 > \fuga =4 \advance \fuga by 2 0

ここで,\fugaに和が代入されていること,\hogeは空のトークン列に展開されるという性質をテストするには,次のようにすればよい.

 \setbox0=\hbox{\hoge{4}{2}\xdef\fugafuga{\the\fuga}}
 \Expect*{\fugafuga}{6}
 \Expect*{\the\wd0}{0.0pt}

わざと水平ボックス内でマクロを展開し,結果を大域環境に出してやる.そして,出した結果が想定した結果であるかを\Expectでチェックする.空のトークン列に展開されているかどうかは作った水平ボックスの幅が0.0ptかどうかを確認すればよい.この手のマクロは空白トークンを入れてしまいおかしくなることがほとんどであるため,これで十分である.

さて,実際にテストしてみよう.次のようなログを得ることができる.

Failed: Example
 \Expect: \the \wd 0
 <0.0pt
 >3.33333pt

あれ……

そう,\hogeを定義する際,開き波括弧の後の改行をコメントアウトしていないため,空白トークンが出力されてしまっているのである.\hogeを次のように修正して再度テストする.

\newcount\fuga
\newcommand{\hoge}[2]{%
  \fuga=#1
  \advance\fuga by #2
}

テストを実行して次の結果を得る.

Passed: Example

世界が平和になった.

課題

副作用を持ちつつトークン列を出力する場合,どうしたらいいのかまだ知見が得られていない*7

*1:比較対象として妥当かは議論しないこととする

*2:正確にはキーワードのパターン

*3:ストファイルを別で作成している場合,特に入れるものがないので,私はテストするマクロのコントロール・シーケンスを入れるようにしている

*4:そんなに長くない

*5:誰かJenkins用のXMLに変換してくれないかなー(棒

*6:LaTeXのいつものあれ

*7:\unhboxとかでいいのかなぁ