SSブログ

YokohamaUnit 0.3.0 の新機能 [YokohamaUnit]

YokohamaUnit の 0.3.0 をリリースした。

https://github.com/tkob/yokohamaunit/releases/tag/v0.3.0

このリリースには次の2つの新機能が含まれる。

リソース式

Javaのユニットテストではテストデータをクラスパス上に配置して(リソース)読み込むことがよくある。

Let url = `getClass().getResource("text1.txt")`.

この書き方は面倒くさい。そこでリソースのための特別な文法を用意した。

Let url = resource "text1.txt".

次のようにも書ける。それぞれリソースをInputStream, File, URIとして返す。

Let ins = resource "text1.txt" as `java.io.InputStream`.
Let file = resource "text1.txt" as `java.io.File`.
Let uri = resource "text1.txt" as `java.net.URI`.

一時ファイル式

一時ファイルを作成するための式を追加した。

Let tempFile = a temporary file.
Let tempFile = a temp file.

作られたファイルに対しては自動的にdeleteOnExitが呼ばれる。

その他

内部的にJavaslangを使うことにした。 Javaでタプルや関数的リストなどを使うのにこれまでは独自に実装したものを使っていて、自分で書くのも何だなと思っていた。 Functional Javaを検討したこともあったが、 いまいちJavaの中になじんでくれないAPIだったのでやめていた。 Javaslangのほうが比較的使いやすいように感じる。

使用例

nbmtoolsのテストで、上述の新機能を使っているので参考にしてほしい。

https://github.com/tkob/nbmtools/tree/f0cd916a7fa5d28228d6e05a37144413ea808c18/src/test/docy/nbmtools


リモート参照を含むNetBeansのnbmファイルをオフライン化するツール [Java]

NetBeansのプラグインモジュールをオフライン環境下でインストールするには nbmファイルをダウンロードしてくればよいが、 このnbmファイルがリモートのファイルへの参照を含んでいる場合、 単独ではインストールに失敗する。

nbmファイルは実態としてはZIPファイルだが、 その中に.externalというファイルが含まれている場合、 NetBeansが.externalの中に書かれたURLからダウンロードして置き換える仕組みになっているからだ。

一方で、この.externalを置き換えたZIPファイルをあらかじめ用意しておけば、 そのようなnbmファイルでもオフラインでインストールできる。 例えば下記のサイトでそのような方法が紹介されている。

NetBeans80にofflineでJUnitをインストール

しかしこれを手動で行うのは面倒だ。ということで自動で行うツールを作った。

nbmtools: https://github.com/tkob/nbmtools

これを使うと上掲の記事の作業は次のようにすればよい。

なお作ってはみたものの.externalを含むnbmファイルはあまり多くない気もしている。

技術的に特筆すべきことはないが、ScalaとJavaの半々でコーディングし、 テストのためにYokohamaUnitをドッグフーディングしている。 とてもひさしぶりにScalaを書いた。


YokohamaUnit 0.2.0 の新機能 [YokohamaUnit]

YokohamaUnit の 0.2.0 をリリースした。

https://github.com/tkob/yokohamaunit/releases/tag/v0.2.1 (バグフィックスの0.2.1に更新)

このリリースには次の2つの新機能が含まれる。

データ変換

テストを書いていると「フィクスチャをテスト可能なデータ型まで変換する」といった、 テストそのものには本質的でない処理がテストコード中にいくつも紛れ込んでしまうことがある。 例えば resources ディレクトリに配置したデータを InputStream から何らかのデータに変換したりといったようなことだ。

そうするとユーティリティメソッドを使って IOUtils.toString(is) のように書くことになるのだが、 あまり美しくない。

0.2.0 導入された機能を使うと is as InputStream と書けるようになる。 まずユーティリティメソッドを Java か何かで書いて、次のようにアノテーションをつける。

@As
public class DataConverters {
    @As
    @SneakyThrows(IOException.class)
    public static String inputStreamAsString(InputStream is) {
        return IOUtils.toString(is, "UTF-8");
    }
}

そうすると YokohamaUnit のテストからは次のように使えるようになる。

Assert `bais as String` is "hello"
where bais = `new ByteArrayInputStream("hello".getBytes("UTF-8"))`.

これを使うためには YokohamaUnit のオプションで -converter <base-package> と指定すると、指定したパッケージ以下(カンマ区切りで複数指定できる)から変換メソッドを見つけてくれる。

不変条件チェック

テスト対象に下記のように Invariant アノテーションをつける。

@lombok.AllArgsConstructor
@Invariant("({ it.count >= 0 })")
@Invariant("({ it.count < 10})")
public class DummyCounter {
    @Getter
    private int count;
 
    public void increment() {
        count++;
    }
}

そうすると YokohamaUnit の4フェーズテストの Setup と Exercise の後に、 テスト内容にかからわず常にこの不変条件をチェックし、満たされないとテストが失敗するようになる。

この機能を有効にするには -contract のオプションを指定する。


SMLZIP: Standard ML用のZIPファイル読み込みライブラリ [SML]

Standard MLからZIPファイルの読み込み・解凍をするためのライブラリを作った。

SMLZIP https://github.com/tkob/smlzip

特徴はPure-SMLである(Cバインディングではない)という点である。 機能は読み込みのみで、現状は最低限のことができる程度だ。 PKZIP形式から読み込みたい場合と、Deflate形式を直接扱いたい場合で 異なるMLB/CMを選択できるようになっている。 Smackage から利用できるようにするためのファイルも配置してみた。

バグ等をみつけたらGithubのissueに報告してほしい。


Proglrの最近の進展 [SML]

SML用のパーサジェネレータProglrの最近の進展について。

字句解析の自動生成

Proglrはml-ulexで生成された字句解析器と結合するようになっていて、 これまでは字句解析は自分で書く必要があった。

しかし、凝った字句解析をしない場合はやはり自動で生成してくれるのが便利だ。 というわけで文法定義からml-ulexのソースを自動生成する機能をつけた。

具体的にはtoken Add "+";のようなトークン定義があった場合は 指定された文字列のトークンを字句解析にも追加する。

token Integer of int;, token Double of real;, token Char of char;, token String of string;, token Ident of string; のような定義があった場合はそれぞれのリテラルのトークンを追加する。

これにより簡単な言語を作る場合はml-ulexのソースをまったく書かなくてもよくなる。

example/calc/calc.cfというサンプルがあるのでそれを参照してほしい。

コメント定義の自動生成

同様にコメントについても字句解析に自動追加できるようになった。 下記のような定義でそれぞれブロックコメントと行コメントを追加できる。 これはBNFCと同様である。

comment "/*" */";
comment "//";

各処理系のための雛形の自動生成

各ML処理系でビルドするためのドライバ、Makefileなども自動生成するようにした。 ml-lptランタイムが同梱されていない処理系のために、 ml-lptランタイムのソースも展開されるようにした。

これにより基本的には文法定義ファイルだけから実行可能ファイルを作るまでのすべてが生成可能となった。

MLton, Poly/ML, Alice ML, MLKit, Moscow ML では数行のコマンドラインでサンプルプロジェクトがビルドできるようになっている。 起動方法はREADMEを参照してほしい。

一方でProglr自身をビルドするための処理系としては、 当面MLtonとPoly/MLに注力することにした。 SML#向けのファイルも残っているが、動かないものと思ってほしい。

ちなみにAlice MLのビルドの作法を調べていたのだが、 リンクに関してはまだあまりわかっていない。 コンパイルをすると.alcというオブジェクトファイルができ、 これは依存する.alcがしかるべき位置に配置されていればそのまま実行できる。 依存するものも含めて1ファイルにするためにalicelinkを使う…と思っているのだが、 alicelinkを使ってリンクしたつもりのファイルだけで実行しようとすると やはり依存性のエラーが出てしまう。 このためREADMEではリンクせずに実行するような手順を書いている。

Graphvizファイルの生成

文法定義から作られるオートマトンを視覚化するためのdotファイルを生成できるようにした。 -a 出力ファイル名 というオプションを与えると生成できる。

依存するツール

Proglrが依存している外部ツールを挙げる。 基本的には昔ながらのUnixのツールであるため、 利用できないということはないと思う。

  • m4: ファイル生成時にテンプレート機能として使う
  • Perl: ml-ulexの出力結果にパッチを当てるのに使っている(Alice ML互換にするためのパッチ)
  • Expect: Poly/MLでビルドするときにインタラクティブシェルを制御するのに使う
  • DejaGnu: Proglrのテスト(make check)で使う

JCUnitのテストケースジェネレータを切り出して使う [Java]

JCUnit はJUnitの(サードパーティーの)テストランナーの1つで、 テストの入力となるフィールドにアノテーションをつけておくと、 そのアノテーションを元にしてPairwise法を使ってテストデータを自動生成して実行してくれる。

それはそれで便利なのだが、 今回はJCUnitのPairwiseのアルゴリズムを JCUnitのテストランナー経由ではなくて直接切り出して使いたいと思ったので、 その方法を調べた。

import com.github.dakusui.jcunit.constraint.ConstraintManager;
import com.github.dakusui.jcunit.constraint.constraintmanagers.NullConstraintManager;
import com.github.dakusui.jcunit.core.Param;
import com.github.dakusui.jcunit.core.factor.Factor;
import com.github.dakusui.jcunit.core.factor.Factors;
import com.github.dakusui.jcunit.generators.IPO2TupleGenerator;
import com.github.dakusui.jcunit.generators.TupleGenerator;
 
public class Main {
    public static void main(String[] args) {
        ConstraintManager cm = new NullConstraintManager();
        Factor os = new Factor.Builder().setName("OS")
                .addLevel("Windows")
                .addLevel("Linux")
                .build();
        Factor browser = new Factor.Builder().setName("Browser")
                .addLevel("Chrome")
                .addLevel("Firefox")
                .build();
        Factor bits = new Factor.Builder().setName("Bits")
                .addLevel("32")
                .addLevel("64")
                .build();
        Factors factors = new Factors.Builder().add(os).add(browser).add(bits).build();
        TupleGenerator tg = new TupleGenerator.Builder()
                .setTupleGeneratorClass(IPO2TupleGenerator.class)
                .setConstraintManager(cm)
                .setFactors(factors)
                .setParameters(new Param[0])
                .build();
        tg.forEach(tuple -> System.out.println(tuple));
    }
}

テストケースジェネレータとなるのはTupleGenerator型のオブジェクトである。 TupleGenerator.Builderのfluentなコンストラクタでオブジェクトを作る。

IPO2TupleGeneratorは生成されるTupleGeneratorの実装で、 IPOというのはPairwiseのアルゴリズムの一種のようだ。

ContaraintManagerは変数の組み合わせの制約を指定するためのもののようだが、 今回は何も制約をつけないのでNullConstraintManagerを与えている。

Factorsはテストケースの元となる一連の変数(Factor)の集まりである。 Factorは変数名と変数がとりうる値(level)から成る。

ParamはTupleGeneratorの実装固有のパラメタである。 ここでは何も指定しない(デフォルト)ので空の配列を与えている。

このParamというクラスは実際にはアノテーションクラスで、 普通はJUnitテストクラスに書かれたアノテーションがそのまま来るようだ。 もし今回のような使用方法でParamを指定するとしたら、 ちょっとまどろっこしい書き方をすることになると思う。

TupleGeneratorはIterable<Tuple>を継承しているので、 ここではforEachメソッドで生成結果を取得している。 TupleはMap<String, Object>を継承していて、キーが変数名、値が変数の値となる。

上記のコードの実行結果は次のようになる。

{Bits=32, Browser=Chrome, OS=Windows}
{Bits=64, Browser=Firefox, OS=Windows}
{Bits=64, Browser=Chrome, OS=Linux}
{Bits=32, Browser=Firefox, OS=Linux}

2つの値を持つ変数3つの組み合わせは単純に積を取ると8通りとなるが、 ここでは4通りのみ生成されている。 その一方で、任意の2つの変数の組み合わせはすべて登場するようになっている。


Tcl 8.6 のインラインアセンブラを使って簡易言語を作る [Tcl]

Tcl 8.6から::tcl::unsupported::assembleというコマンドでインラインアセンブラが使えるようになっていた。 これを使うとTclの仮想マシンのバイトコードに対するアセンブラをTclコード中に書くことができる。

unsupportedという記載からもわかるように公式なドキュメントはないが、 故意にエラーを出すことによってエラーメッセージから使い方を忖度することができる。

例えば ::tcl::unsupported::assemble help とすると命令の一覧が出てくる。 (helpというサブコマンドがあるわけではなくて、存在しない命令を与えた時のエラー)

% ::tcl::unsupported::assemble help
bad instruction "help": must be push, add, append, appendArray, appendArrayStk, appendStk, arrayExistsImm, arrayExistsStk, arrayMakeImm, arrayMakeStk, beginCatch, bitand, bitnot, bitor, bitxor, concat, coroName, currentNamespace, dictAppend, dictExists, dictExpand, dictGet, dictIncrImm, dictLappend, dictRecombineStk, dictRecombineImm, dictSet, dictUnset, div, dup, endCatch, eq, eval, evalStk, exist, existArray, existArrayStk, existStk, expon, expr, exprStk, ge, gt, incr, incrArray, incrArrayImm, incrArrayStk, incrArrayStkImm, incrImm, incrStk, incrStkImm, infoLevelArgs, infoLevelNumber, invokeStk, jump, jump4, jumpFalse, jumpFalse4, jumpTable, jumpTrue, jumpTrue4, label, land, lappend, lappendArray, lappendArrayStk, lappendStk, le, lindexMulti, list, listConcat, listIn, listIndex, listIndexImm, listLength, listNotIn, load, loadArray, loadArrayStk, loadStk, lor, lsetFlat, lsetList, lshift, lt, mod, mult, neq, nop, not, nsupvar, over, pop, pushReturnCode, pushReturnOpts, pushResult, regexp, resolveCmd, reverse, rshift, store, storeArray, storeArrayStk, storeStk, strcmp, streq, strfind, strindex, strlen, strmap, strmatch, strneq, strrange, strrfind, sub, tclooClass, tclooIsObject, tclooNamespace, tclooSelf, tryCvtToNumeric, uminus, unset, unsetArray, unsetArrayStk, unsetStk, uplus, upvar, variable, verifyDict, or yield

pushという命令について知りたければ同様にエラーを出してみる。

% ::tcl::unsupported::assemble push
wrong # args: should be "push value"

この調子で調べていくと基本的な命令については何となくわかってくる。 そこでTclバイトコードをターゲットとした簡単な言語を作ってみた。

コンパイラはSMLで、パーサジェネレータとしてProglrを使って作る。

以前の記事 をOCaml+BNFC+JavaからSML+Proglr+Tclに変えて行ったものと思えばよい。 ソースコードの全体はGistにアップロードした。

文法定義

文法定義は下記の通り。

token Add "+" ;
token Sub "-" ;
token Mul "*" ;
token Div "/" ;
token LParen "(" ;
token RParen ")" ;
token Eq "=" ;
token Comma "," ;
token Semi ";" ;
token FunKw "fun" ;
token LetKw "let" ;
token InKw "in" ;
token IfKw "if" ;
token ThenKw "then" ;
token ElseKw "else" ;
token Integer of int;
token Ident of string;
token String of string;
 
Grm. Grm ::= [Top] ;
separator Top ";" ;
 
Fun. Top ::= "fun" Ident "(" [Param] ")" "=" Exp ;
Exp. Top ::= Exp ;
 
separator Param "," ;
Param. Param ::= Ident ;
 
Let. Exp ::= "let" Ident "=" Exp "in" Exp ;
Cnd. Exp ::= "if" Exp "then" Exp "else" Exp ;
 
separator Exp "," ;
 
Add. Exp1 ::= Exp1 "+" Exp2 ;
Sub. Exp1 ::= Exp1 "-" Exp2 ;
 
Mul. Exp2 ::= Exp2 "*" Exp3 ;
Div. Exp2 ::= Exp2 "/" Exp3 ;
 
App. Exp3 ::= Ident "(" [Exp] ")" ;
Int. Exp3 ::= Integer ;
Str. Exp3 ::= String ;
Var. Exp3 ::= Ident ;
 
coercions Exp 3;

言語は関数定義と式の連続であり、式中には条件分岐とローカル変数と四則演算と関数呼び出しが書ける。

ProglrはGLRだが、この文法はLALR(1)になっているはずである。 これを確認するには、最初のtokenの行を省くとBNFCの文法定義と互換性があるので、 BNFCに食べさせてocamlyaccに通す。衝突が報告されないことでLALR(1)であることを確認できる。

Proglrでは字句解析についてはml-ulexで行うのだが、割と自明なので掲載を省く。

これをProglrに通すとパーサーと抽象構文木のデータ型のSMLソースコードが生成される。

コンパイラ

メインとなるProglrのドライバは次のように書く。

fun main () =
  let
    val strm = Lexer.streamifyInstream TextIO.stdIn
    val sourcemap = AntlrStreamPos.mkSourcemap ()
    val ast = hd (Parse.parse sourcemap strm)
  in
    check ast;
    compile ast
  end

何故Parse.parseのhdを取っているのかといえば、Parse.parseが構文木のリストを返すからだ。 これはProglrが一般にCFGを扱うからで、CFGの文法は多義的でありうる。

checkは未定義のローカル変数の使用と、ローカル変数の名前の衝突をチェックする。 後者についてはシャドウイングされるものとみなして名前の付け替えをしてもいいと思うが、 今回は単にチェックするだけにした。

fun nameOf (Param (_, name)) = name
 
fun mem (x, []) = false
  | mem (x, y::ys) = x = y orelse mem (x, ys)
 
fun check (Grm (span, tops)) = List.app (fn top => checkTop (top, [])) tops
and checkTop (Fun (span, name, params, body), env) =
      checkExp (body, map nameOf params)
  | checkTop (Exp (span, exp), env) = checkExp (exp, env)
and checkExp (Let (span, name, value, body), env) = (
      checkExp (value, env);
      if mem (name, env) then raise Fail ("dup var: " ^ name)
      else checkExp (body, name::env))
  | checkExp (Cnd (span, cond, t, f), env) =
      (checkExp (cond, env); checkExp (t, env); checkExp (f, env))
  | checkExp (App (span, rator, rands), env) =
      List.app (fn rand => checkExp (rand, env)) rands
  | checkExp (Add (span, e1, e2), env) =
      (checkExp (e1, env); checkExp (e2, env))
  | checkExp (Sub (span, e1, e2), env) =
      (checkExp (e1, env); checkExp (e2, env))
  | checkExp (Mul (span, e1, e2), env) =
      (checkExp (e1, env); checkExp (e2, env))
  | checkExp (Div (span, e1, e2), env) =
      (checkExp (e1, env); checkExp (e2, env))
  | checkExp (Int (span, int), env) = ()
  | checkExp (Str (span, str), env) = ()
  | checkExp (Var (span, var), env) =
      if mem (var, env) then () else raise Fail ("unknown var: " ^ var)

Tclの仮想マシンはJavaVMと同様のスタックマシンである。 したがってコンパイルは前回の記事と大体同様である。

local
    val n = ref 0
in
    fun newLabel () = "label" ^ Int.toString (!n) before n := !n + 1
end
 
fun println s = (print s; print "\n")
 
fun compile (Grm (span, tops)) = List.app compileTop tops
and compileTop (Fun (span, name, params, body)) = (
      println ("proc " ^ name ^ " {" ^ String.concatWith " " (map nameOf params) ^ "} {");
      println ("::tcl::unsupported::assemble {");
      compileExp body;
      println ("}");
      println ("}"))
  | compileTop (Exp (span, exp)) = (
      println ("::tcl::unsupported::assemble {");
      compileExp exp;
      println ("}"))
and compileExp (Let (span, name, value, body)) = (
      compileExp value;
      println ("store " ^ name);
      println "pop";
      compileExp body)
  | compileExp (Cnd (span, cond, t, f)) =
      let
        val falseLabel = newLabel ()
        val trueLabel = newLabel ()
      in
        compileExp cond;
        println ("jumpFalse " ^ falseLabel);
        compileExp t;
        println ("jump " ^ trueLabel);
        println ("label " ^ falseLabel);
        compileExp f;
        println ("label " ^ trueLabel)
      end
  | compileExp (App (span, rator, rands)) = (
       println ("push " ^ rator);
       List.app compileExp rands;
       println ("invokeStk " ^ Int.toString (length rands + 1)))
  | compileExp (Add (span, e1, e2)) = (
      compileExp e1;
      compileExp e2;
      println "add")
  | compileExp (Sub (span, e1, e2)) = (
      compileExp e1;
      compileExp e2;
      println "sub")
  | compileExp (Mul (span, e1, e2)) = (
      compileExp e1;
      compileExp e2;
      println "mult")
  | compileExp (Div (span, e1, e2)) = (
      compileExp e1;
      compileExp e2;
      println "div")
  | compileExp (Int (span, int)) = println ("push " ^ Int.toString int)
  | compileExp (Str (span, str)) = println ("push {" ^ str ^ "}")
  | compileExp (Var (span, var)) = println ("load " ^ var)

実行

コンパイラはtalという実行ファイルになるようにした。 Tcl上での実行を簡単にするためにシェルスクリプトtalexecを書く。

#/bin/sh
 
TEMP=$(mktemp)
tal < $1 > $TEMP
tclsh $TEMP
rm -f $TEMP

ソースファイルを次のように書くと、

fun f(x) =
  if x then x * f(x - 1)
  else 1;
 
puts(f(10))

実行結果はこうなる。

$ talexec fact.tal
3628800

ここでf関数はTclのプロシージャとして定義される。 putsはTclの組み込みのコマンドである。 関数呼び出しの構文で任意のTclコマンドを呼び出すことができる。

使用した命令

命令 説明
push value valueをスタックに積む
pop スタックから1つ取り出す
store varname スタック最上位の値を変数varnameに格納する。スタックは変更されないので注意。
load varname 変数varnameの値をスタックにロードする
jumpFalse label スタック最上位の値が偽のときlabelにジャンプする
jump label 無条件ジャンプ
label name ラベルの定義
invokeStk count スタックの内容でプロシージャ(コマンド)を呼び出す。countは引数の数+1
add スタックの2要素を取り出し、和をスタックに置く
sub スタックの2要素を取り出し、差をスタックに置く
mult スタックの2要素を取り出し、積をスタックに置く
div スタックの2要素を取り出し、商をスタックに置く

感想

今のところTclのVMに関してあまり特殊なことや優位性があるようには思われないので、 一般的な言語処理系のターゲットとする価値があるかというとなさそうである。 アセンブリにオリジナルソースファイルの行番号を埋め込む方法が無いようである点も不利である。

しかしTclの中でDSLをコンパイルして使いたかったり、 Tclと何らかの密な連携を必要とする場合はインラインアセンブラを使う手もあるだろう。


ScalaCheckへの疑念 [Scala]

「プロパティベースのテスト」について初めて知ったのは Fun of Programmingという本で、 それを読んだ当時ScalaにもScalaCheckがあることを知って少しさわっていたんだけど 結果的には「なんか微妙」という感想を持ってやめてしまった。[^1]

その後特にScalaCheckをさわってはおらず、Scalaからもかなり離れてしまったので感想も変わっていないんだけど Scalaのユーザーが増えるとともにScalaCheckも使われているようで、よく聞くようになった。

それで思い出したのでScalaCheckへの疑念を言語化することにした。 タイトルはScalaCheckとしたけれどもQuickCheckやその他の同様のフレームワークにも該当すると思う。

普通にテストケースを書いたほうがいいのではないか

ScalaCheckでは例えば「任意の1024以上の整数nについてP(n)が成立する」のような性質から、 nを例えば100通り自動生成してテストケースを実行してくれる。 人間がテストケースを書く場合、おそらく2,3個のケースを書くにとどまるだろう。

ここで「50倍ものケースを自動実行するのだからより大きい確証が得られるだろう」 ということが言えるだろうか。

人間が書くテストケースにおけるnはおそらく1024と、MAX_INTと、ひょっとしたらその間の適当な値である。[^2]

自動生成されるケースは1024以上MAX_INT以下のランダムな100個で、 1024とMAX_INTはそこに含まれるかもしれないし含まれないかもしれない。

この例で1024とMAX_INTはバグを見つけてくれそうなテストケースである。それは仕様の境界値だから。[^3] プログラムは「以上」と「より大きい」を勘違いしているかもしれないし、オーバーフローを考慮していないかもしれない。

これに対して1025からMAX_INT-1までの値はせいぜい1つ選べばよく、残りは無駄なテストである。 無駄なテストケースは100個あっても10000個あっても品質の向上に貢献しない。 ランダムに100回実行したから1回実行したよりも99回分多くの確信が得られると思うのは偽の確信である。 偽の確信はテストにおける害悪だ。

これらに対して「それはジェネレーターの定義次第だ」という反論があるかもしれない。 でもそれを意識してジェネレータを作るならば、 それは人間がテストケースを決めるのを遠回りにしただけではないだろうか。

ランダムな組み合わせはどうか

複数の変数に対する組み合わせテストを自動生成してくれる点が ScalaCheckなどの方法の有利な点だという考えがあるかもしれない。 これは確かに手で書くのは煩雑だ。

でも同じ自動生成するのでも直交表やPairwise法などの、経験的な研究に裏付けられた、 バグ検出に効率的な組み合わせの自動生成方法があり、 ランダムな生成が有利だという根拠はない。

確率的なものは難しい

以前ScalaCheckをさわっていた時の記事で、 確率的な分布を意識していないと有効なテストが生成されないことがあるという注意点を取り上げていた。

テストの中に確率的なものが現れるのは悪いことだと思う。 おそらく多くの開発者にとって確率はプログラムよりずっと理解しにくいからだ。 プログラムコードの結果を予測するよりもテストコードの結果を予測するほうが難しいというのは、 明らかに望ましい状況ではない。

再帰的なデータ構造についてはどうか

再帰的なデータ構造に対するテストデータの生成はScalaCheckのような方法が活躍できる領域かもしれない。 再帰的なデータ構造では変数の数自体が固定的でなく、 そのような問題に対して既存の組み合わせ技法をどう適用すればいいかはあまりはっきりしないからだ。

これについてもしかし、ランダムである必要があるのかという点についてはやはり疑問が残る。 (有界モデル検査のようにある決まった範囲を全網羅するような方法が代わりに考えられる)


以上ScalaCheckや類似の手法についての疑念を書いた。

大体においては 「同値分割や境界値分析やPairwiseなどの既存のテスト技法でテスト設計したほうがいいのでは」 と感じているということだ。

一方で「ScalaCheckは駄目だと思っている」かというとあまりそこは断言できない。

1つにはこれだけ皆使っているので自分の理解が何か間違っているのではないかという不安がある。 (上記に書いたようなことを補填するような洗練されたやり方が考案されているのかもしれない)

もう1つはテスト手法の良しあしは経験的に決まるものだという点で、 例えば「実際にプロパティベースのテストのほうが既存のテスト手法より良くバグを見つけるのだ」 という経験的研究結果があればなるほどそういうものかと思って納得するかもしれない。 (読んでいる人でご存知の人がいたら教えてください)

  • [^1] 左下の「ScalaCheckを試す」のリンク集を参照
  • [^2] n=1023でP(n)が成立しないこともテストするかもしれないが、比較のためここでは取り上げない
  • [^3] MAX_INTは「暗黙の」境界値であるともいえる

YokohamaUnitの紹介 (5) スタブ [YokohamaUnit]

YokohamaUnit はスタブ作成のための文法を提供する。

一般的には次の形式をとり、式が現れる場所に置かれる。

a stub of `クラス名` such that method `メソッドシグニチャ` returns ...

例えばMapクラスのスタブであり、 getメソッドに対して42を返却するスタブは次のように書く。

a stub of `java.util.Map` such that method `get(java.lang.Object)` returns 42

テスト全体としてはたとえばこのように書ける。

# Test: Collections.unmodifiableMap preserves lookup
 
Assert `unmodifiableMap.get("answer")` is 42
 where map is a stub of `java.util.Map`
              such that method `get(java.lang.Object)` returns 42
   and unmodifiableMap is `java.util.Collections.unmodifiableMap(map)`.

こうした記述はMockitoのメソッドを呼ぶようなバイトコードにコンパイルされる。

スタブ/モック機能を言語機能としてどのように取り入れるかのバランスは難しいところであり、 どのように発展させるか(させないか)は模索している段階にある。

ある程度高機能なスタブを作るための表記は結局のところある種のプログラミング言語になる。 JavaやGroovyで書くのでなく、 スタブ/モック用の言語を使う利点があるスイートスポットは(もしあるとしたら)どの位の用途なのか。 自然言語として読み下せることはスタブ/モックにとってどの程度重要なのだろうか。

またYokohamaUnitについて言えば、バイトコードを自分で生成できる前提なので、 あえてMociktoを使用する必然性も実はあまりないかもしれない。


YokohamaUnitの紹介 (4) 4フェーズテスト [YokohamaUnit]

YokohamaUnit で4フェーズテストを書く方法を取り上げる。

4フェーズテストはテスト対象のメソッドや関数を呼んだ後のオブジェクト (やその他の)状態の変化を確かめるテストの一般的なパターンで、 事前準備(setup)、実行(exercise)、検証(verify)、事後処理(teardown)からなる。

YokohamaUnitでは見出しをつかってそれぞれのフェーズを表す。

# Test: AtomicInteger.incrementAndGet increments the content
## Setup
Let i be `new java.util.concurrent.atomic.AtomicInteger(0)`.
 
## Exercise
Do `i.incrementAndGet()`.
 
## Verify
Assert `i.get()` is `1`.

Setupでは主にLet文を使って変数の束縛を行う。 Let文は複数並べてもよいし、andでつなげてもよい。

Let x be 1 and y be 2.

beでなく=でもよい。

Let x = 1 and y = 2.

ExerciseではDo文を使ってテスト対象のメソッドの実行を行う。 Do文は式言語の(副作用を起こすことが期待される)式をとる。

これも複数並べてもよいし、andでつなげてもよい。

## Exercise
Do `i.incrementAndGet()` and `i.incrementAndGet()`.
Do `i.incrementAndGet()`.

Verifyフェーズは4フェーズでない関数的なテストの場合と同様だが、 複数並べた場合やfor allを使った場合の実行単位が異なる。

Assertを複数並べたり、テーブルを参照した場合、 関数的なテストでは複数のJUnitテストメソッドにコンパイルされたが、 4フェーズテストの中でそのように書いた場合は、 1つのメソッドの中で続けて実行されるようなコードにコンパイルされる。

4フェーズテストをパラメタ化する方法については別の記事で紹介する。

次の例はTeardownフェーズを含むテストの例である。 Teardownフェーズの中でもExerciseフェーズと同様、Do文を使う。

# Test: The size of a new temporary file is zero
## Setup
Let temp be `java.io.File.createTempFile("prefix", null)`.
 
## Verify
Assert `temp.length()` is `0L`.
 
## Teardown
Do `temp.delete()`.

Teardownフェーズはテストの成否にかかわらず必ず実行される。


この広告は前回の更新から一定期間経過したブログに表示されています。更新すると自動で解除されます。