[[UnitTest]] // [[RUnit(R ユニットテストフレームワーク)パッケージ中のオブジェクト一覧]]
----

UnitTest は関数をテストするコードを記述することで,関数の正しい振舞いを保証します.&br;その結果,随時改変が行なわれてもバグが存在しない(であろう)ことが期待されます.

[[RUnit:http://cran.md.tsukuba.ac.jp/src/contrib/Descriptions/RUnit.html]] は R での UnitTest 実行のためのフレームワークです.

RUnit の使い方を簡単にまとめてみます.

* 仕様を決める
これから定義する関数の満たすべき仕様を考えます.

和を求める mysum() を作成することにします.実際には sum() をラッピングするだけです.

mysum() がクリアするべきテストのひとつとして,

  mysum(1:10) == 55

がすぐに思い浮かびます.

* テスト
ワーキングディレクトリに tests.R ファイルを作成します.

** その 1

tests.R に test.mysum() を記述.慣習として,test.テストする関数名 とします.&br;これは,runTestFile() の引数 testFuncRegexp で解釈されますので,&br;testFuncRegexp に適当な正規表現で与えれば変更できます.

  ### tests.R
  test.mysum <- function() {
    checkEqualsNumeric(mysum(1:10), 55)
  }

そして,おもむろに runTestFile() します.

  > runTestFile("tests.R")
  Error in data.class(target) : couldn't find function "mysum"
  Timing stopped at: 0 0 0 0 0 
  Number of test functions: 1 
  Number of errors: 1 
  Number of failures: 0 

テストは失敗します.まだ mysum() を定義していませんので当然です.&br;ここで大切なのは,「テストは確実に行なわれている」ということです.

** その 2
mysum() を定義します.

  ### tests.R
  test.mysum <- function() {
    checkEqualsNumeric(mysum(1:10), 55)
  }
  
  mysum <- function(x) {
  
  }

再びテストを実行します.

  > runTestFile("tests.R")
  Error in checkEqualsNumeric(mysum(1:10), 55) : 
  	Mean relative  difference: 10
  Timing stopped at: 0 0 0 0 0 
  Number of test functions: 1 
  Number of errors: 0 
  Number of failures: 1 

今回も失敗しますが,エラーメッセージは異なります.これは mysum() が期待される値を返していない,つまり,バグを持っているということです.したがって,このバグをなくさなくてはいけません.

といっても,この場合 mysum() がなにもしていない(ので,返値は NULL)だけですが.

** その 3
mysum() のバグを潰す.具体的には sum() の値を返すことにします.

  ### tests.R
  test.mysum <- function() {
    checkEqualsNumeric(mysum(1:10), 55)
  }
  
  mysum <- function(x) {
    sum(x
  }

そしてテスト.

  > runTestFile("tests.R")
  Error in parse(file, n, text, prompt) : syntax error on line 7
  Number of test functions: 1 
  Number of errors: 1 
  Number of failures: 0 

あれ? エラーになってしまいました.

Number of erros: には関数にエラーがあるということを示しています.一方,Number of filures: は,testEqualsNumeric() などでの評価が失敗していることを意味します.つまり,前者は R のスクリプトとして間違っている,後者はプログラムの挙動として間違っていることを示します.

** その 4
で,今回はそもそもスクリプトが間違っている(sum() の括弧が閉じていない)ので,これを修正します.

  ### tests.R
  test.mysum <- function() {
    checkEqualsNumeric(mysum(1:10), 55)
  }
  
  mysum <- function(x) {
    sum(x)
  }

そしてテストする.

  > runTestFile("tests.R")
  Number of test functions: 1 
  Number of errors: 0 
  Number of failures: 0 

やたっ♪ 成功しました.


*コメント
-すみません.理解不足なコメントかもしれませんが,R に用意されているデバッグ用の関数 (例えば [[ここで紹介されている関数:http://www.okada.jp.org/RWiki/?cmd=read&page=R%A4%CE%B4%D8%BF%F4%C4%EA%B5%C1%A4%CE%B4%F0%CB%DC&word=%A5%C7%A5%D0%A5%C3%A5%B0]] ) の機能が便利になったものだという理解でよろしいのでしょうか? --  &new{2004-07-04 (日) 19:41:17};
- 新しく書いた関数が,正しい処理をしているかどうかは,答えのわかっているデータを与えて,関数が出す答えがそれと一致することを確認することにつきるでしょう。~
 正解と関数が出す答えをどうやって比較するかですが,たとえば答えが数値一個なら,簡単ですね。目で見て比較しても良いし,答えを返す前に stopifnot 関数を使ってチェックするとかでよいでしょう。~
 計算に複数の条件によって答えが違うような場合には,あらゆる条件を通過するような複数のデータセットを用意して,それぞれの条件ごとに正解を比較すればいいわけです。この場合にも関数の値を返す前に自動的に比較してもいいでしょう。~
 答えが複数(たとえば相関係数行列や因子分析の結果のようにたくさんの数値)からなる場合には,全部を目で比較するのはいやでしょうから,ファイルに結果を書き出しておいて,ファイル同士を diff コマンドか何かで比較してやればいいです。しかし,この場合には,正解のファイルを用意するのが大変そう。結局,いくつかの数値を目でみて,確認するのが現実的ではないかと思いますがどうでしょうね。~
 OS のセキュリティホールが後になっていろいろ発見されるのをみてもわかるように,あるプログラムがちゃんと動くかどうかをテストするテストデータを作るだけでも結構大変で,見落としというのも結構あるのです。~
 プログラムをちゃんと設計してから書き始めるとか,細かい関数を作り,正しいことを確認してから,より大きい関数に組み込んでいくというようなボトムアップとか,その逆のトップダウンとか,よりよいプログラムを書く方法というのが議論された時代もありました。R は,役に立つ関数がいろいろ用意されているので,そう言う点ではバグを含むプログラムを書くおそれというのは少ないような気はします。たとえば VBA で何かを書こうとしたら,ほとんど全部自分で書かないといけない(誰かの作ったライブラリを使うというのも限界がある。そもそも,R ほどのたくさんの関数のソースを集めて公開してあるところなんかないだろうし。)。 --  &new{2004-07-04 (日) 20:08:05};
-当該の関数を書く以前にその関数をテストするための関数を用意しておいて,このテストをクリアするように関数をつくっていくという考え方に則ったフレームワークを実行するためのパッケージです.デバッグは関数ができあがってから使われるという点が異なると思います. --  &new{2004-07-06 (火) 13:53:49};
-恐れ入りますが,2 つめのコメントの主旨はなんでしょうか. --  &new{2004-07-06 (火) 13:55:21};
-RUnit の効用は限られているのではないかと言うことでは?
-どのような効用が,どのように限られているのでしょうか. --  &new{2004-07-06 (火) 14:10:48};
-UnitTest のフレームワークが,すでに議論された時代遅れのものである,ということかな --  &new{2004-07-06 (火) 14:14:42};
-あるいは R においては,それほど有効性はないだろうという指摘か --  &new{2004-07-06 (火) 14:15:16};
-複雑な解を返す関数のチェックをするためには大変な思いをしないといけないかもと書いてあるのでしょう。 --  &new{2004-07-06 (火) 15:03:00};
-その場合,複数個のテストを行なえばよいのではないでしょうか --  &new{2004-07-06 (火) 15:38:28};
-件のコメントにも書いてありますが,あるデータセットを因子分析するような例を考えましょう。mva には factanal があるので,それを使えばいいわけだけど,そうでない場合,結構な量の数値が出てきますよね。 --  &new{2004-07-06 (火) 17:07:07};
-複数の返値があるから大変である,ということでしょうか? --  &new{2004-07-06 (火) 19:10:28};
-以下のようにすればよいということでしょうね。
 test.my.oneway.ANOVA <- function()
 {
 # equal=TRUE のときのチェック
 	result <- my.oneway.ANOVA(c(8, 11, 22, 6),
               c(135.83, 160.49, 178.35, 188.06), 
                c(19.59, 12.28, 15.01, 9.81)^2)
 	checkEqualsNumeric(c(13669.396, 3, 4556.4655, 
                          20.82824, 1.737484e-8, 9406.843, 43,
                          218.7638, 99999, 99999, 23076.240, 46, 
                          501.6574, 99999, 99999), as.numeric(t(result)), 
                          tolerance=0.000001)
 
 # equal=FALSE のときのチェック
 	result <- my.oneway.ANOVA(c(8, 11, 22, 6), 
                c(135.83, 160.49, 178.35, 188.06), 
                c(19.59, 12.28, 15.01, 9.81)^2, 
                equal=FALSE)
 	checkEqualsNumeric(c(result$F, result$df1, result$df2, result$P) , 
                      c(17.56461,3,16.50403,2.191921e-05), 
                      tolerance=0.000001)
 }
 
 my.oneway.ANOVA <- function(n, m, u, equal=T)
 {
 #    stopifnot(length(n) == length(m), length(m) == length(u), 
                      n > 1, u > 0, floor(n) == n)
     ng <- length(n)
     if (equal) {    # 分散が等しいと仮定する場合
         nc <- sum(n)
         sw <- sum(u*(n-1))
         sb <- sum(n*(m-sum(n*m)/nc)^2)
         ss <- c(sb, sw, sb+sw)
         df <- c(ng-1, nc-ng, nc-1)
         ms <- ss/df
         f <- p <- rep(99999, 3)
         f[1] <- ms[1]/ms[2]
         p[1] <- pf(f[1], df[1], df[2], lower = F)
         anova.table <- cbind(ss, df, ms, f, p)
         colnames(anova.table) <- c("SS", "d.f.", "MS", "F value", "P value")
         rownames(anova.table) <- c("between class", "within class", "total")
         anova.table
     }
     else {    # 分散が等しいと仮定しない場合
         w <- n/u
         m0 <- sum(w*m)/(sum.w <- sum(w))
         temp <- sum((1-w/sum.w)^2/(n-1))/(ng^2-1)
         f <- sum(w*(m-m0)^2)/((ng-1)*(1+2*(ng-2)*temp))
         p <- pf(f, ng-1, 1/(3*temp), lower=F)
         list(F=f, df1=ng-1, df2=1/(3*temp), P=p)
     }
 }
まだテストしなくてはならない項目があるが,一応。 --  &new{2004-07-06 (火) 19:23:42};
-これ以降 oneway.ANOVA() の中身を最適化などの理由で変更してテストをクリアしなければ,変更した箇所がバグの元だと考えられます. --  &new{2004-07-06 (火) 19:38:36};
-テストデータと,そのときの結果を記録したファイルを残しておけば十分かもしれない。 --  &new{2004-07-06 (火) 23:43:15};
-> テストデータと,そのときの結果を記録したファイルを残しておけば十分かもしれない。~
それをフレームワークにしたものが UnitTest です --  &new{2004-10-15 (金) 22:15:22};
-相当複雑な出力を持つ実用的な関数について,実現してみてください。教科書的な,呪文を唱えられても,現実味が感じられない。 --  &new{2004-10-15 (金) 22:35:43};
-ついでに。グラフィック出力が結果である関数については無力ですか>うnitTえst --  &new{2004-10-15 (金) 22:37:55};
-実用的な関数の内部で用いる純粋な計算を行うような関数の Test を行うことがメインになります.また,複雑な結果を返すもののチェックとしては最低限「どのような名前のオブジェクトが返ってこなければならないか」のチェックはできます. --  &new{2004-10-19 (火) 14:01:31};
-グラフィック出力の場合,プロットの基となるデータの整合性のチェックには使えるのではないでしょうか.あるいは,まったく現実的ではありませんがグラフィックをダンプして検証用のデータを用意し,出力される画像とビット単位で比較すればよいでしょうか. --  &new{2004-10-19 (火) 14:04:11};
-グラフィック出力の場合,プロットの基となるデータの整合性のチェックには使えるのではないでしょうか. --  &new{2004-10-19 (火) 14:04:11};

#comment

トップ   編集 差分 バックアップ 添付 複製 名前変更 リロード   新規 一覧 検索 最終更新   ヘルプ   最終更新のRSS