Scheme:テストファースト

Scheme:テストファースト

開発の始めかた

うーん,何を書けばよいかわからないので,とりあえず,私が何か Gaucheで作るときの手順でも書いてみます.- kou

前提

下準備

プロジェクト(ちょっとしたものなら「ちょっとした」プロジェク トになる)用のディレクトリを作る.プロジェクト用ディレクトリ は以下のような構成となる.ここでは,プロジェクト名をXXXとする.

XXX -+- lib  ;; スクリプトを置くディレクトリ
     |
     +- test ;; テストスクリプトを置くディレクトリ
% cd ~/work/gauche/
% mkdir -p XXX/{lib,test}

XXX/test/run-test.elを作成する.ほとんどの場合, gaunit/test/run-test.scmをコピーしている.ちなみに,~ /work/gauche/gaunit/以下にはGaUnitのワーキングコピーがある.

% cd ~/work/gauche/XXX
% cp {../gaunit/,}test/run-test.scm

ちなみに,test/run-test.scmはこんな感じ.

#!/usr/bin/env gosh

(use gauche.interactive)
(use file.util)
(use test.unit)

(if (symbol-bound? 'main)
    (define _main main))

(define (main args)
  (let ((dir (sys-dirname (car args))))
    (for-each (lambda (test-script)
                (load (string-join (list dir test-script) "/")))
              (directory-list dir
                              :filter (lambda (x)
                                        (rxmatch #/^test-.+\.scm$/ x))))
    (if (symbol-bound? '_main)
        (_main `(,(car args) "-vp" ,@(cdr args)))
        (run-all-test))))

テストファーストするとき(!!!)は,test/test-YYY.scmを作成して 最初のテストを書く.こんな感じ.

#!/usr/bin/env gosh

(use test.unit)

(use YYY)

(define-test-case "YYY test"
  ("ZZZ test"
    (assert-equal '(Test first is very difficult)
                  (ZZZ 'what-do-you-think?))))

最初に

#!/usr/bin/env gosh

を書いているのは,単独でテストを実行するときに便利だから.と いうのも,テストが増えてくると,毎回の全てのテストを実行する と結構時間がかかったりするから.そんなときは, test/run-test.scmの(rxmatch #/^test-.+\.scm$/ x)というところ の正規表現をいじったり,直接test/test-YYY.scmを実行して特定 のテストだけ実行する.

将来的には,指定したパターンにマッチするテスト名のテストだけ 実行する機能を入れようと考えているけど手つかずのまま.難しく はないんだけど,なかなかね.

初テストで準備完了

最初のテストを作成したら,C-cC-tでとりあえずテストを実行する. (use YYY)としているところで,YYY.scmが見付からないとかいうエ ラーが出ていることで,下準備がきちんと整っていることを確認す る.

開発開始

あとは,lib/YYY.scmを作成してテストにパスするコードを書いて いく.こんな感じ.

(define-module YYY
  (export ZZZ))
(select-module YYY)

(define (ZZZ message)
  '(Test first is very difficult))

(provide "YYY")

後は,コードを書く,テストを書くの繰り返し.テストはシェルに lsを入力する感覚で(意味が無くても)わりと頻繁に実行する.

あ,そうそう,run-test.elを使いながらプログラムを書くときは, C-x 5 2でフレームを2つ開いて,1つはテストの実行結果が表示さ れる*run-test*バッファのみ,もう1つはC-x 2でフレームを2分割 してソースコードとテストコードを表示している.テストが失敗し たらコードが表示されている方のフレームでC-x `してエラー行に 飛んだりする.*run-test*バッファを表示させているフレームには 移動することはなく,表示専門.

まとめ

私は,run-test.elがなければテストファーストなんて出来ないなぁ. あっても出来ないことが多いけど.ま,run-test.el自体はGaUnit 固有のものではないのでgauche.test使うときでも使えるんだけど. 実際,Rubyでテストしながらコードを書くときもrun-test.el使っ てるし.

うーん,なんか開発環境紹介みたいになっちゃったな.たぶん, GaUnitはどこどこがダメでgauche.testはどこどこがよい,みたい なことを書けばよかったんだろうなぁ. - kou

コメント

Shiro (2004/06/15 06:35:06 PDT): そうかあ。gauche.testが関数中心なのは、テスト自身を高階関数に よって抽象化することを重視しているからなのですが(例:ext/charconv/test.scm、 ext/dbm/test.scm)、 それだとソースに飛べても本当に知りたい情報(何がfailしたか)とは 離れてるからあんまり意味ないかな、と思っていました。

charconvやdbmのように複数の条件を少しづつ変えながらテストする場合、 GaUnitでも高階化してゆくと思うんですが、その場合でもC-x `とかは 使い勝手がいいでしょうか。

テストメッセージに行番号を入れ込むマクロってのはありかも。 それなら高階テスト関数を呼んでいる一番外側でそのマクロを呼べば、 どの箇所でfailが起きたかがわかりやすいですね。

kou (2004/06/15 18:29:22 PDT): えーと,これは

(define (map-test tester file from-codes to-codes)    ;; (A)
  (for-each (lambda (from)
              (for-each (lambda (to) (tester file from to)) to-codes))
            from-codes))

...

(define (test-output type reader writer file from to) ;; (B)
  (let ((infostr  (format #f "~a ~a => ~a (~a)" file from to type))
        (fromfile (format #f "~a.~a" file from))
        (tofile   (format #f "~a.~a" file to)))
    (if (ces-conversion-supported? from to)
        (test infostr
              (file->string tofile)
              (lambda ()
                (file->string-conv/out fromfile to from reader writer)))
        (test infostr "(not supported)"
              (lambda () "(not supported)")))
    ))

(define (test-output/byte file from to)
  (test-output "byte" read-byte write-byte file from to))

...

(map-test test-output/byte "data/jp1"                 ;; (C)
          '("EUCJP" "UTF-8" "SJIS" "ISO2022JP")
          '("EUCJP" "UTF-8" "SJIS" "ISO2022JP"))

とあって,(C)でテストを失敗した時に(A)とか(B)の方にジャンプ しちゃうと嫌だということでよいですか?(というかそういうこと で話を進めますが,)GaUnitは(C)にジャンプするようにスタック トレースを調整して出力しています(「テストメッセージに行番号 を入れ込むマクロ」が返す結果と同じような結果になるのかしら). というのも,(私は)どのテストで失敗したかを見たいからです. ちなみに,GaUnitではこんな感じに書きます.

(define (AAA ...)
  (assertなんちゃらとか ...)
  ...)

...

(define-test-case "..."
  ("..."
    (assert-each AAA
                 '((AAAへの入力)
                   ...)
                   ;; 上のgauche.testの例の
                   ;;   '("EUCJP" "UTF-8" "SJIS" "ISO2022JP")
                   ;; みたいな感じ
                   )))

で,これが使い勝手がよいかどうかというと,まぁまぁ使い勝手が よいです.まぁまぁというぐらいですから気に入っている所と気に いならい所があるわけでして,こんな感じです.

気に入っている所: テストケースをちょこちょこ変更しながらテストを走らせる時 (私はよくやります)に,テストケースにジャンプできるので楽.

気に入らない所: 上の例でいうAAAがどんな入力でエラーになったかを失敗時のメッ セージに含めてくれないと,どのテストケースで失敗したのかわ かりづらいときがある.例えば,こんなとき.

(assert-each (lambda (except arg1 arg2)
               (assert-equal except (+ arg1 arg2)))
             '((10 2 8)
               (10 3 8)
               (10 2 9)
               (10 1 10)))
;;; =>
;;; ファイル名:行番号: (assert-each (lambda (except arg1 arg2) (assert-equal except (+ arg1 arg2))) '((10 2 8) (10 3 8) (10 2 9) (10 1 10)))
;;;  expected:<10>
;;;   but was:<11> in ...

これは,上の例でいうAAAをちゃんとメッセージを表示するように 作ればよいだけなので,まぁ,問題ないと言えば問題ない.こんな 感じ.

(define-assertion (assert-plus except arg1 arg2)
  (assert-equal except (+ arg1 arg2)
                (make-message-handler except
                                      :after-actual
                                      (format #f
                                              " when ~s"
                                              (list '+ arg1 arg2)))))
...

(assert-each assert-plus
             '((10 2 8)
               (10 3 8)
               (10 2 9)
               (10 1 10)))
;;; =>
;;; ファイル名:行番号: (assert-each assert-plus '((10 2 8) (10 3 8) (10 2 9) (10 1 10)))
;;;  expected:<10>
;;;   but was:<11> when (+ 3 8) in ...

Last modified : 2012/02/07 08:29:19 UTC