2012年8月12日日曜日

Haskell で平面ルービックキューブ (2)

今年の正月に、Haskell で 平面ルービックキューブを書いてみた。「Haskell で平面ルービックキューブ

この時は、Haskell の GUI コーディングはどんなものかと、Gtk2Hs を調べるついでに書いただけなので、シャッフルくらいできるようにしても良いなと思いつつ、放置していた。それを今日は書き足してみた。

====

シャッフルというのは、Esc キーを押すと、6面揃った状態から指定の回数分だけ、ランダムな面と方向で90度回転させるというもの。回数は起動時パラメータで指定する。

下の画像は、Escキーを押した直後。起動時に 3を渡しているので、3回かき回している。

これをよく見て、青を反時計回り(B)、赤を時計回り(r)、白を時計回り(w)と、3回操作すると 6面が揃う。

ソースはこんな感じになる(ブログに貼るにはチト長いが…)

module Main where

import Graphics.UI.Gtk
import Graphics.UI.Gtk.Glade
import Graphics.UI.Gtk.Gdk.GC
import Graphics.UI.Gtk.Gdk.Drawable
import Graphics.UI.Gtk.Gdk.Events as Evt
import Data.IORef
import Control.Monad
import System
import System.Random

--図形と色
data SquareColor = G|R|W|Y|O|B deriving Show
color G = Color 0x0000 0xE000 0x0000
color R = Color 0xE000 0x0000 0x0000
color W = Color 0xFFFF 0xFFFF 0xFFFF
color Y = Color 0xFFFF 0xFFFF 0x0000
color O = Color 0xF000 0x5000 0x0000
color B = Color 0x0000 0x0000 0xD000

initialSurface = map (replicate 9) [G,R,W,Y,O,B]

v60    = (18, -10)
v120   = (18,  10)
v180   = (0,   21)
angles = [[(-108,-30), v120,v180],
          [(-108,-30), v60, v120],
          [(-54,   0), v60, v180],
          [(0,   -30), v60, v180],
          [(0,    33), v60, v120],
          [(54,  -60), v120,v180]]
allParas = concatMap createParas angles
createParas v = map toPara $
    map (\n -> dupV v n) [0..2] >>= (\x -> map (\n ->dupH x n) [0..2])
toPara = (\[(ox,oy), (x1,y1), (x2,y2)] ->
  [(ox,oy), (ox+x1-1, oy+y1-1),
  (ox+x1+x2-2, oy+y1+y2-2),(ox+x2-1, oy+y2-1)])
dupH = (\[(ox,oy), v1@(x1,y1), v2] n -> [(ox+x1*n, oy+y1*n), v1, v2])
dupV = (\[(ox,oy), v1, v2@(x2,y2)] n -> [(ox+x2*n, oy+y2*n), v1, v2])

centering _ [] = []
centering size (angle: angles) =
  (map (centeringPoint size) angle): (centering size angles)

centeringPoint (w, h) (x, y) = (x + (div w 2), y + (div h 2))

--main とウィンドウ操作
main = do
    currentSurfaceIORef <- newIORef initialSurface
    initialSurfaceIORef <- newIORef initialSurface

    initGUI
    Just xml     <- xmlNew "rcube.glade"
    window       <- xmlGetWidget xml castToWindow "window1"
    entry        <- xmlGetWidget xml castToEntry "entry1"
    drawingArea  <- xmlGetWidget xml castToDrawingArea "drawingarea1"

    onDestroy window mainQuit
    onExposeRect drawingArea (const $ do
        dw       <- widgetGetDrawWindow drawingArea
        gc       <- gcNew dw
        surface  <- readIORef currentSurfaceIORef
        size     <- widgetGetSize drawingArea
        drawCube dw gc size surface)
    onEditableChanged entry $ do
        seq      <- get entry entryText
        surface  <- readIORef initialSurfaceIORef
        writeIORef currentSurfaceIORef $ rotateAll surface seq
        widgetQueueDraw drawingArea
    onKeyPress entry (\event -> do
        if 65307 == Evt.eventKeyVal event
        then do
            args <- getArgs
            s <- shuffle $ read $ args!!0
            let shuffled = rotateAll initialSurface s
            writeIORef initialSurfaceIORef shuffled
            writeIORef currentSurfaceIORef shuffled
            widgetQueueDraw drawingArea
            entrySetText entry ""
            return True
        else return False)

    widgetShowAll window
    mainGUI
  where
    drawCube d g size x = do
      drawParas d g (foldl (++) [] x) $ centering size allParas
    drawParas d g _ [] = return ()
    drawParas d g (x:xs) (p: ps) = do
      gcSetValues g $ newGCValues { foreground = color x }
      drawPolygon d g True p
      drawParas d g xs ps
    shuffle n = replicateM n
              $ getStdRandom (randomR (0,11)) >>= (return.(!!) "rgbywoRGBYWO")

-- 回転操作
rotateAll p [] = p
rotateAll p (x:xs) = rotateAll (rotate p x) xs
g2r [g,r,w,y,o,b] = [r6 o,r3 g,r3 w,r3 r,y,   r9 b]
r2g [o,g,w,r,y,b] = [r9 g,r9 r,r9 w,y,   r6 o,r3 b]
w2r [g,r,w,y,o,b] = [r9 g,w,   r3 o,r3 y,r3 b,r6 r]
r2w [g,w,o,y,b,r] = [r3 g,r6 r,w,   r9 y,r9 o,r9 b]
y2r [g,r,w,y,o,b] = [r9 w,y,   o,   r3 b,r3 g,r9 r]
r2y [w,y,o,b,g,r] = [r9 g,r3 r,r3 w,y,   o,   r9 b]
o2r [g,r,w,y,o,b] = [r6 b,r6 o,r6 y,r6 w,r6 r,r6 g]
r2o [b,o,y,w,r,g] = [r6 g,r6 r,r6 w,r6 y,r6 o,r6 b]
b2r [g,r,w,y,o,b] = [r9 y,b,   r9 o,r3 g,r3 w,r]
r2b [y,b,o,g,w,r] = [r9 g,r,   r9 w,r3 y,r3 o,b]
rotate [(g1:g2:g3:gs), [r1,r2,r3,r4,r5,r6,r7,r8,r9],
        (w1:w2:w3:ws), (y1:y2:y3:ys),
        o,              (b1:b2:b3:bs)] 'r' =
       [(w1:w2:w3:gs), [r7,r4,r1,r8,r5,r2,r9,r6,r3],
        (y1:y2:y3:ws), (b1:b2:b3:ys),
        o,              (g1:g2:g3:bs)]
rotate [(g1:g2:g3:gs), [r1,r2,r3,r4,r5,r6,r7,r8,r9],
        (w1:w2:w3:ws), (y1:y2:y3:ys),
        o,             (b1:b2:b3:bs)] 'R' =
       [(b1:b2:b3:gs), [r3,r6,r9,r2,r5,r8,r1,r4,r7],
        (g1:g2:g3:ws), (w1:w2:w3:ys),
        o,             (y1:y2:y3:bs)]
rotate p 'g' =r2g (rotate (g2r p) 'r')
rotate p 'G' =r2g (rotate (g2r p) 'R')
rotate p 'w' =r2w (rotate (w2r p) 'r')
rotate p 'W' =r2w (rotate (w2r p) 'R')
rotate p 'y' =r2y (rotate (y2r p) 'r')
rotate p 'Y' =r2y (rotate (y2r p) 'R')
rotate p 'o' =r2o (rotate (o2r p) 'r')
rotate p 'O' =r2o (rotate (o2r p) 'R')
rotate p 'b' =r2b (rotate (b2r p) 'r')
rotate p 'B' =r2b (rotate (b2r p) 'R')
rotate p _ = p
r3 [s1,s2,s3,s4,s5,s6,s7,s8,s9] = [s7,s4,s1,s8,s5,s2,s9,s6,s3]
r6 x = r3 $ r3 x
r9 x = r6 $ r3 x
onKeyPress の辺りを書き足した。Hackage がダウンしていてドキュメントが見られないというアクシデントがあったが、割と簡単に直感で書けてしまう。

2012年8月7日火曜日

FizzBuzz をテストファーストで書いてみよう

なんとなく FizzBuzz をテストファーストで書いてみることにした。

====

Eclipse 上で JMockit を使ってやってみる。

仕様は、3の倍数なら Fizz、5の倍数なら Buzz、3と5の公倍数なら FizzBuzz、それ以外ならその整数自体を標準出力に書き出すというもの。範囲は 1 から 100 までにしよう。main() から実行する。

まずテストクラスから

public class FizzBuzzTest {
  @Test public void main() {
    FizzBuzz.main(new String[0]);
  }
}
クラス FizzBuzz が存在しないので、エディタ上で赤い下線が引かれている。Ctrl+1 でクラス生成ダイアログを開いて、main() メソッド付きのクラスを自動作成する。
public class FizzBuzz {
  /**
   * @param args
   */
  public static void main(String[] args) {
    // TODO Auto-generated method stub
  }
}
コンパイルが通ったら一応 JUnit を実行してグリーンになるのを確認。


次に、とりあえず一番最初に"1" が表示されることを確認したい。テストコードの方を以下のように書き換える。

public class FizzBuzzTest {
  @Mocked("println(String)") PrintStream mock;
  @Test public void main() {
    new Expectations() {{
      mock.println("1");
    }};
    FizzBuzz.main(new String[0]);
  }
}
PrintStream をモックして、System.out.println()の呼び出しを確かめている。

これを実行すると、パラメータ"1"で呼ばれるはずの println() が呼ばれなかったという事で、になる。本体コードを以下のように直して、再実行。今度はグリーンになるので、JMockit を含めた疎通確認ができた。
  public static void main(String[] args) {
    System.out.println("1");
  }


さて、与えられた仕様をどうテストするか。まず 100個の数字ってとこからやってみる。実装としては、println() が呼ばれる度に、パラメータを List に溜め込んでおいて、後で調べる方式にしてみよう。こんなコードになる。

  @Test public void main() {
    final List<String> texts = new ArrayList<>();
    new NonStrictExpectations() {{
      mock.println(anyString); result = new Delegate<PrintStream>() {
        void println(String x) { texts.add(x); }
      }; 
    }};
    FizzBuzz.main(new String[0]);
    assertEquals(100, texts.size());
}
Delegate ってのを使って、PrintStream#println(String) を書き換えて、受け取った文字列をローカルのリスト texts に溜め込むようにした。

実行すると赤くなるから、本体コードも以下のように直して、グリーンにしておく。
  public static void main(String[] args) {
    for (int i = 1; i <= 100; i++)
      System.out.println("" + i);
  }


次に、数字と文字列の対応付けのあたりをやる。以下をチェックする。

始点の1・・・1 ⇨ "1"
3とその前後・・・2 ⇨ "2", 3 ⇨ "Fizz", 4 ⇨ "4"
5とその前後・・・4 ⇨ "4", 5 ⇨ "Buzz", 6 ⇨ "Fizz"
15とその前後・・・14 ⇨ "14", 15 ⇨ "FizzBuzz", 4 ⇨ "16"
終点の100・・・100 ⇨ "Buzz"
テストコードはこんな感じにしてみる。
@SuppressWarnings("unused")
public class FizzBuzzTest {
  @Mocked("println(String)") PrintStream mock;
  @Test public void main() {
    final List<String> texts = new ArrayList<>();
    new NonStrictExpectations() {{
      mock.println(anyString); result = new Delegate<PrintStream>() {
        void println(String x) { texts.add(x); }
      }; 
    }};
    FizzBuzz.main(new String[0]);
    assertEquals(100, texts.size());

    FizzBuzzAssertion a = new FizzBuzzAssertion(texts);
    a.assertIt("1",      1);

    a.assertIt("2",      2);
    a.assertIt("Fizz",   3);
    a.assertIt("4",      4);
    a.assertIt("Buzz",   5);
    a.assertIt("Fizz",   6);
  
    a.assertIt("14",     14);
    a.assertIt("FizzBuzz",15);
    a.assertIt("16",     16);

    a.assertIt("Buzz",   100);
  }
  static class FizzBuzzAssertion {
    final List<String> actuals;
    FizzBuzzAssertion(List<String> actuals) { this.actuals = actuals; }
    void assertIt(String expected, int number) {
      assertEquals(expected, actuals.get(number - 1));
    }
  }
}
FizzBuzzAssertion クラスはコード重複を避けるために導入した。そうしないと、以下のようなコード重複が生じる。また、リストの数字と文字列の対応付けもずれて読みにくいので、これも解消した。
assertEquals("1", texts.get(0));
assertEquals("2", texts.get(1));
assertEquals("Fizz", texts.get(2));
assertEquals("4", texts.get(3));
テストを実行するとになるので、本体コードを以下のように書き換える。
  public static void main(String[] args) {
    for (int i = 1; i <= 100; i++) {
      System.out.println(
          0 == i % 15 ? "FizzBuzz":
          0 == i % 3 ?  "Fizz":
          0 == i % 5 ?  "Buzz":
          Integer.toString(i));
    }
  }
再実行してグリーンになるのを確認。これでテストコードと本体コードの TDD 終わり。

一応機能テストとして、本体コードの main を実行して、コンソールを目視確認しておく。問題なし。

====

こんな感じで、FizzBuzz 自体は屁だけど、テストファーストでやるとモッキングのテクが、若干必要になってくる。

2012年8月6日月曜日

システム例外を扱うテストコードを JMockit で書いてみる

外部のライブラリで発生した例外のハンドリングを、どうやってテストファーストで書くか。これを示してみる。

====

お題は、以下のようなもの。

java.io の API を使って、テキストファイルから最初の 1行目を読み込むメソッド loadText() があり、クラス ExceptionalFugafuga で定義されている。またテストコードも既に書いてある。ただし IOException 発生時のコードは未定のままになっている。

本体コード。
public class ExceptionalFugafuga {
   public String loadText() {
      try (BufferedReader reader = new BufferedReader(new FileReader("fuga.txt"))) {
         return reader.readLine();
      } catch (IOException exep) {
         throw new AssertionError(); // ★ 後回しの暫定コード
      } 
  }
}
こっちはテストコード。
public class ExceptionalFugafugaTest {
   @Mocked BufferedReader br = null;
   @Mocked FileReader reader= null;
   @Test public void loadText() throws Exception {
      new NonStrictExpectations() {{
         br.readLine(); result="the first line";
      }};
      assertEquals("the first line", new ExceptionalFugafuga().loadText());
   }
}
ここで、IOException をキャッチしたときの暫定コードを、"例外: "+例外メッセージという文字列を返すように修正したい。これをテストファーストでやるとどうなるか。

まずテストコードから。
こんなテストメソッドを追加する。

@Test public void loadText_IOException() throws Exception {
  new Expectations() {{
    new FileReader("fuga.txt"); result = new IOException("はあこりゃこりゃ");
  }};
  assertEquals("例外: はあこりゃこりゃ", new ExceptionalFugafuga().loadText());
}
本体コードがまだそのままなので、テストが失敗して赤くなる。

で、AssertionError() を上げているところを "return "例外: " + exep.getMessage();"に変える。

再度実行し、今度はグリーンになる事を確認する。以上。

====

昔なら、テストメソッド loadText() の @Before とか @After で、実ファイル "fuga.txt" を作ったり消したりしていたところだけど、見ての通り、モックツールを使うとそういうのはいらなくなる。

例外に関しても同じようなことで、例外を発生させる状況を作りだすのではなく、単に例外を投げ上げる振る舞いに差し替えればいい。「Unit」としてテストするとは、本来はこうやって環境から隔離して実施するのものではなかったかと思う。同じやり方は DB例外 でも WebService 例外でも、なんでも応用が効く。

2012年8月5日日曜日

現在時刻に依存する振る舞いのテスト・コーディング

現在時刻が 17時台から 22時台までなら夕方(evening)とする」。

そんなメソッドを、Eclipse と JMockit を使ったテストファーストで書いてみる(こういった実行時に依存する条件を含むコードは、昔はテスト・コーディングが難しかったけど、ツールが進歩した最近はそうでもない)。

====

対象メソッドが CurrentTimeHogehoge クラスの isEvening() メソッドだとすると、テストクラスはこんな感じになるはず

public class CurrentTimeHogehogeTest {
  @Test public void isEvening() {
    assertTrue(CurrentTimeHogehoge.isEvening());
  }
}
この時点では CurrentTimeHogehoge クラスがまだ存在しないから、Eclipse のエディタ上で赤い下線が引かれている。その行で Ctrl+1 を押してクラス生成を選ぶとダイアログが開くから、ソースフォルダを適当に直して Finish。

エディタに戻ると、今度は isEvening() に赤い下線が移っているので、これも Ctrl+1 で 生成する(いちいちタイピングしない)。

コンパイルが通ったところで、こんなコードになっているはず。

public class CurrentTimeHogehoge {
  public static boolean isEvening() {
    // TODO Auto-generated method stub
    return false;
  }
}
試しにテスト実行してみると失敗して赤くなるので、"return false" を "return true" に書き換えて、一旦グリーンにしておく。

続いて isEvening() の実装方針だけど、現在時刻を Calendar の get で取るって事で当たりを付ける。そうすると、テストコードは以下のような感じになる。

public class CurrentTimeHogehogeTest
  @Mocked("get") final Calendar cal = null;
  @Test public void isEvening() {
    new Expectations() {{
      Calendar mock = Calendar.getInstance(); 
      mock.get(Calendar.HOUR_OF_DAY); result = 17;
    }};
    assertTrue(CurrentTimeHogehoge.isEvening());
  }
}
cal を宣言する一行は、意味的にはフィールドの宣言と言うより、Calendar クラスの get メソッドを差し替えるという宣言になる。差し替える振る舞いは Expectations の中に記述していて、ここでは固定値17を返している。

ここでテスト実行してみて、になることをひとまず確認。呼ばれるはずのメソッド、Calendar#get() が 呼ばれなかったと、トレースされているはず。

本体コードの isEvening() は以下のように書き換える。とりあえず時間範囲の下側だけ判別するようにしている。

  public static boolean isEvening() {
    Calendar cal = Calendar.getInstance();
    int hour = cal.get(Calendar.HOUR_OF_DAY);
    return 17 <= hour;
  }
実行するとテストがグリーンになる。

続いて、境界値である、16→偽, 17→真, 22→真, 23→偽 についてテスト・コーディングするわけだけど、コードが重複するのが明らかなので、あらかじめ重複部分を以下のようなメソッドとして切り出しておく。

  private void validateIsEvening (final int hour, final boolean result) {
    new Expectations() {{
      Calendar mock = Calendar.getInstance(); 
      mock.get(Calendar.HOUR_OF_DAY); result = hour;
    }};
    assertEquals(result, CurrentTimeHogehoge.isEvening());
  }
これを、パラメータを変えてテストメソッドから呼び出す。以下のようになる。
@Test public void isEvening() {
    validateIsEvening(16, false);
    validateIsEvening(17, true);
    validateIsEvening(22, true);
    validateIsEvening(23, false);
  }

実行するとテストがになるから、isEvening の最終行を "return 17 <= hour && hour <= 22"に直す。これでテストが成功する。

====

演習だから、テストコードと本体コードの切り替えがかなり小刻みだけど、実戦だったらもうちょいざっくりした感じになると思う

JMockit で Hello World をテストファーストしてみよう

main() メソッドから System.out.println("Hello, World!") してるだけのコードをテストファーストで書くとどうなるか?

これを Eclipse と JMockit でやってみる。

====

まず、こんな感じで普通の JUnitコードを書く。

public class HelloWorldTest {
   @Test public void test() {
      HelloWorld.main(new String [0]);
   }
}
この時点では、まだ HelloWorld クラスがないので、Eclipse のエディタ上では、HelloWorld の下に赤い下線が引かれている。そこにカーソルを移動して Ctrl+1 を押下してクラスの生成を選ぶとダイアログが開くので、main 生成にチェックをいれて Finish。こんなクラスが生成される。
public class HelloWorld {
   /**
    * @param args
    */
   public static void main(String[] args) {
      // TODO Auto-generated method stub
   }
}
Eclipse での作業中はショートカットを活用すると無駄なタイピングをかなり削減できる。

コンパイルが通ったところで、一応、テスト実行してグリーンになることを確認しておく。


次に System.out の println メソッドに "Hello, World!"が渡される事を確認する。

これは JMockit を使うので、pom.xml に以下のように追記する。ただし、インストゥルメンテーションの都合上、<dependencies>の中で junit の前に書かれていなくてはならない。

  <dependency>
   <groupId>com.googlecode.jmockit</groupId>
   <artifactId>jmockit</artifactId>
   <version>0.999.15</version>
  </dependency>

テストコードには以下のように追記する。

public class HelloWorldTest {
   @Mocked PrintStream mock;
   @Test public void test() throws Exception {
      new Expectations() {{
         mock.println("Hello, World!");
      }};
      HelloWorld.main(new String[0]);
   }
}

実行してみるとテストが失敗し、以下のような結果がトレースされる。

mockit.internal.MissingInvocation: Missing invocation of:
java.io.PrintStream#println(String x)
with arguments: "Hello, World!"
on mock instance: java.io.PrintStream@15ebf57
・・・
呼ばれるはずのメソッドが呼ばれていないと、JMockit に指摘されている。

ここで main を以下のように書き換えて、再度テスト実行してみる。

   public static void main(String[] args) {
      System.out.println("test");
   }
失敗して、こんなメッセージがトレースされるが、さっきとメッセージが変わっている。
mockit.internal.UnexpectedInvocation: Parameter "x" of java.io.PrintStream#println(String x) expected "Hello, World!", got "test"
・・・
今度は、"Hello, World!" を期待していたのに"test"が渡されたと言っている。

というわけで、おもむろに"test"を"Hello, World!"に書き換えて、テスト再実行。今度はグリーンになる。

実際のテスト・コーディングよりも、敢えてちょっと回りくどい感じでやってみた。