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)
やりました!
テストはバグを潰していく過程が目に見えるので楽しいですね。