Pikzie でデータ駆動テストをやってみた

クリアコード さんが開発している Pikzie (ピクジー) という Python のための書きやすさとデバッグのしやすさを重視した Unit Testing Framework があります。ブログでデータ駆動テスト *1 の紹介をされていました。以前、素数を求めるアルゴリズム -エラトステネスの篩(ふるい)-] を書いたので、それを使って実際にやってみました。

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import pikzie

def is_prime(number):
    def hurui(s, p):
        p.append(s.pop(0))
        for i, num in enumerate(s):
            if num % p[-1] == 0:
                s.pop(i)

    _number = int(number)
    prime  = []
    search = range(2, _number)
   
    while not prime or prime[-1] ** 2 < search[-1]:
        hurui(search, prime)
    prime += search
    return _number in prime

@pikzie.data("small", 2)
@pikzie.data("lucky", 7)
@pikzie.data("large", 1013)
def test_true(number):
    assert_true(is_prime(number))

@pikzie.data("negative", -3)
@pikzie.data("zero", 0)
@pikzie.data("unit", 1)
@pikzie.data("square", 4)
@pikzie.data("workload", 15342)
def test_false(number):
    assert_false(is_prime(number))

実行結果。

$ ./test_prime.py 
..EEEFFE

1) Error: __main__.test_false (unit)
  data: 1
./test_prime.py:32: assert_false(is_prime(number))
./test_prime.py:16: hurui(search, prime)
./test_prime.py:8: p.append(s.pop(0))
<type 'exceptions.IndexError'>: pop from empty list

2) Error: __main__.test_false (zero)
  data: 0
./test_prime.py:32: assert_false(is_prime(number))
./test_prime.py:16: hurui(search, prime)
./test_prime.py:8: p.append(s.pop(0))
<type 'exceptions.IndexError'>: pop from empty list

3) Error: __main__.test_false (negative)
  data: -3
./test_prime.py:32: assert_false(is_prime(number))
./test_prime.py:16: hurui(search, prime)
./test_prime.py:8: p.append(s.pop(0))
<type 'exceptions.IndexError'>: pop from empty list

4) Failure: __main__.test_true (large): assert_true(is_prime(number))
  data: 1013
./test_prime.py:24: assert_true(is_prime(number))
expected: <False> is a true value

5) Failure: __main__.test_true (lucky): assert_true(is_prime(number))
  data: 7
./test_prime.py:24: assert_true(is_prime(number))
expected: <False> is a true value

6) Error: __main__.test_true (small)
  data: 2
./test_prime.py:24: assert_true(is_prime(number))
./test_prime.py:16: hurui(search, prime)
./test_prime.py:8: p.append(s.pop(0))
<type 'exceptions.IndexError'>: pop from empty list

Finished in 0.075 seconds

8 test(s), 2 assertion(s), 2 failure(s), 4 error(s), 
           0 pending(s), 0 omission(s), 0 notification(s)


ノーッ!!!

バグだらけだということが発覚してしまいました(T T)

wikipedia:素数 の定義は

1とその数自身以外に正の約数がない(つまり1とその数以外のどんな自然数によっても割り切れない)、1より大きな自然数のこと

なので、1以下の数のチェック処理が抜けていることに気付きました。

--- test_prime.py	2010-08-20 13:59:23.127652865 +0900
+++ test_prime2.py	2010-08-20 13:58:53.877652375 +0900
@@ -11,6 +11,9 @@
                 s.pop(i)
 
     _number = int(number)
+    if _number <= 1:
+        return False
+    
     prime  = []
     search = range(2, _number)

修正後、テストを実行してみます。

$ ./test_prime2.py 
.....FFE

1) Failure: __main__.test_true (large): assert_true(is_prime(number))
  data: 1013
./test_prime2.py:29: assert_true(is_prime(number))
expected: <False> is a true value

2) Failure: __main__.test_true (lucky): assert_true(is_prime(number))
  data: 7
./test_prime2.py:29: assert_true(is_prime(number))
expected: <False> is a true value

3) Error: __main__.test_true (small)
  data: 2
./test_prime2.py:29: assert_true(is_prime(number))
./test_prime2.py:21: hurui(search, prime)
./test_prime2.py:8: p.append(s.pop(0))
<type 'exceptions.IndexError'>: pop from empty list

Finished in 0.076 seconds

8 test(s), 5 assertion(s), 2 failure(s), 1 error(s),
           0 pending(s), 0 omission(s), 0 notification(s)

エラーが減りました。次に 7 と 1013 という素数を与えているのに False と判定されているテストに注目します。おや、探索リスト search の範囲指定にバグがありました(- -#

--- test_prime2.py	2010-08-20 14:06:43.875654037 +0900
+++ test_prime3.py	2010-08-20 14:06:36.699777499 +0900
@@ -15,7 +15,7 @@
         return False
     
     prime  = []
-    search = range(2, _number)
+    search = range(2, _number+1)
    
     while not prime or prime[-1] ** 2 < search[-1]:
         hurui(search, prime)

修正後、テストを実行してみます。

$ ./test_prime3.py 
.......E

1) Error: __main__.test_true (small)
  data: 2
./test_prime3.py:29: assert_true(is_prime(number))
./test_prime3.py:20: while not prime or prime[-1] ** 2 < search[-1]:
<type 'exceptions.IndexError'>: list index out of range

Finished in 0.074 seconds

8 test(s), 7 assertion(s), 0 failure(s), 1 error(s),
           0 pending(s), 0 omission(s), 0 notification(s)

あと1つです。素数 2 を与えた場合、探索リスト search が1つの要素しか持っていないため hurui 関数の中で search から pop() すると空リストになり prime[-1] ** 2 < search[-1] の条件判定で IndexError が発生します。2 は仕様として True を返すように修正することもできますが、せっかくテストケースがあるので while ループの脱出条件を変更してみましょう。

--- test_prime3.py	2010-08-20 14:06:36.699777499 +0900
+++ test_prime4.py	2010-08-20 15:07:52.620777507 +0900
@@ -17,8 +17,11 @@
     prime  = []
     search = range(2, _number+1)
    
-    while not prime or prime[-1] ** 2 < search[-1]:
+    while True:
         hurui(search, prime)
+        if not search or search[-1] <= prime[-1] ** 2:
+            break
+    
     prime += search
     return _number in prime

修正後、テストを実行してみます。

$ ./test_prime4.py
........
Finished in 0.069 seconds

8 test(s), 8 assertion(s), 0 failure(s), 0 error(s),
           0 pending(s), 0 omission(s), 0 notification(s)


やりました!

テストはバグを潰していく過程が目に見えるので楽しいですね。