LoginSignup
8
8

More than 5 years have passed since last update.

Python doctestを使ってテスト駆動型でFizzBuzzコードを書く。

Last updated at Posted at 2015-03-16

はじめに

標準ライブラリのdoctestを使ってテスト駆動型でFizzBuzzプログラムを書いてみた。

doctestとは(引用)

http://docs.python.jp/2/library/doctest.html
(引用開始)
doctest モジュールは、対話的 Python セッションのように見えるテキストを探し出し、セッションの内容を実行して、そこに書かれている通りに振舞うかを調べます。 doctest は以下のような用途によく使われています。

モジュールの docstring (ドキュメンテーション文字列) 中にある対話実行例のすべてが書かれている通りに動作するか検証することで、 docstring の内容が最新かどうかチェックする。
テストファイルやテストオブジェクト中の対話実行例が期待通りに動作するかを検証することで、回帰テストを実現します。
入出力例を豊富に使ったパッケージのチュートリアルドキュメントが書けます。入出力例と解説文のどちらに注目するかによって、ドキュメントは「読めるテスト」にも「実行できるドキュメント」にもなります。
(引用終了)

doctestは、ドキュメント(コメント)のチェックをしながら、回帰テストにも使える優れたツールな模様である。これは使ってみるしかあるまい。

テスト駆動型開発のやり方(引用)

http://www.atmarkit.co.jp/ait/articles/1403/05/news035.html
(引用開始(見出しのみ))
【1】失敗するテストコードを書く
【2】テストを失敗から成功に変化させるようにテスト対象コードを書く
【3】テストを失敗させるテストコードを追加する
【4】テストを失敗から成功に変化させるようにテスト対象コードを書く
【5】テストを成功状態にしたままリファクタリングする
(引用終了)

今回はこのやり方に則って行うこととする。

今回の関数の仕様

適当に仕様を決めてみた。
関数名: fizzbuzz(n)
機能:
 入力値nが3の倍数であれば'Fizz'という文字列を返し、
入力値nが5の倍数であれば'Buzz'という文字列を返し、
入力値nが15の倍数であれば'FizzBuzz'という文字列を返し、
それ以外は整数nを出力する関数。

nは、整数型、長整数型、浮動点少数型で表される整数である必要がある。
(浮動点少数型の場合、小数部分が0である必要がある。)
少数が入力されると、InputErrorの例外を返す。

【1】失敗するテストコードを書く

まずは仮実装

def fizzbuzz(n):
  '''
  入力値nが3の倍数であれば'Fizz'という文字列を返し、
  入力値nが5の倍数であれば'Buzz'という文字列を返し、
  入力値nが15の倍数であれば'FizzBuzz'という文字列を返し、
  それ以外は整数nを出力する関数。
  例:
  >>> fizzbuzz(3)
  'Fizz'

  nは、整数型、長整数型、浮動点少数型で表される整数である必要がある。
  (浮動点少数型の場合、小数部分が0である必要がある。)
  少数が入力されると、InputErrorが返される。
  '''

  return 'Fizz'

if __name__ == '__main__':
  import doctest
  doctest.testmod()

実行(-vは詳細出力のためのオプション)。
なお、-vを付けないとテストに失敗したときのみ情報が出力されるらしい。

python fizzbuzz_doctest.py -v 

この状態ならokがでる。

テストケースを追加してみる

>>> fizzbuzz(5)
'Buzz'

下記エラーが出力される。

File "fizzbuzz_doctest.py", line12, in __main__.fizzbuzz
Failed example:
    fizzbuzz(5)
Expected:
    'Buzz'
Got:
    'Fizz'
1 items had no tests:
    __main__

【2】テストを失敗から成功に変化させるようにテスト対象コードを書く

下記のように修正する。

  if (n%3) == 0:
    return 'Fizz'
  elif (n%5) == 0:
    return 'Buzz'

【3】テストを失敗させるテストコードを追加する

上記【1】の繰り返し

【4】テストを失敗から成功に変化させるようにテスト対象コードを書く

上記【2】の繰り返し

補足

例外のテストでは、Tracebackから始める必要がある模様である。また、...で途中の行を省略できる。

  >>> fizzbuzz(5.5)
  Traceback (most recent call last):
      ...
  ValueError: n must be integer. n: 5.500000

できたコード(中間段階)

# coding: utf-8

def fizzbuzz(n):
  '''
  入力値nが3の倍数であれば'Fizz'という文字列を返し、
  入力値nが5の倍数であれば'Buzz'という文字列を返し、
  入力値nが15の倍数であれば'FizzBuzz'という文字列を返し、
  それ以外は整数nを出力する関数。
  例:
  >>> fizzbuzz(3)
  'Fizz'

  >>> fizzbuzz(36)
  'Fizz'

  >>> fizzbuzz(5)
  'Buzz'

  >>> fizzbuzz(50)
  'Buzz'

  >>> fizzbuzz(15)
  'FizzBuzz'

  >>> fizzbuzz(45)
  'FizzBuzz'

  >>> fizzbuzz(2)
  2

  >>> fizzbuzz(49)
  49

  nは、整数型、長整数型、浮動点少数型で表される整数である必要がある。
  (浮動点少数型の場合、小数部分が0である必要がある。)
  少数が入力されると、ValueErrorが返される。

  >>> fizzbuzz(1.0)
  1

  >>> fizzbuzz(5.0)
  'Buzz'

  >>> fizzbuzz(5.5)
  Traceback (most recent call last):
      ...
  ValueError: n must be integer. n: 5.500000

  '''
  if n != int(n):
    raise ValueError( 'n must be integer. n: %f' % n )
  n = int(n)

  if (n%15) == 0:
    return 'FizzBuzz'
  elif (n%3) == 0:
    return 'Fizz'
  elif (n%5) == 0:
    return 'Buzz'
  else:
    return n

if __name__ == '__main__':
  import doctest
  doctest.testmod()

【5】テストを成功状態にしたままリファクタリングする

テストにより動作を確保しながらリファクタリングすることにより、最終的なコードを得られるようである。

例えば、次のような変更を加えることもできる。
if文の参照コストを削減するため、事前に1~15のそれぞれの15で割った余りを要素とするリストを作成し、このリストを参照することにより、返す文字列を決める。

  '''
  if (n%15) == 0:
    return 'FizzBuzz'
  elif (n%3) == 0:
    return 'Fizz'
  elif (n%5) == 0:
    return 'Buzz'
  else:
    return n
  '''

  fizzbuzz_list = []
  for i in range(15):
    if (i%15) == 0:
      fizzbuzz_list.append('FizzBuzz')
    elif (i%5) == 0:
      fizzbuzz_list.append('Buzz')
    elif (i%3) == 0:
      fizzbuzz_list.append('Fizz')
    else:
      fizzbuzz_list.append(0)
  r = int(n%15)
  return fizzbuzz_list[r] if fizzbuzz_list[r] else n

新たにテストを書かなくとも、次のコマンドにより回帰テストを行うことができる。

python fizzbuzz_doctest.py -v

さらにアルゴリズムの変更をしてみる。

  '''
  if (n%15) == 0:
    return 'FizzBuzz'
  elif (n%3) == 0:
    return 'Fizz'
  elif (n%5) == 0:
    return 'Buzz'
  else:
    return n


  fizzbuzz_list = []
  for i in range(15):
    if (i%15) == 0:
      fizzbuzz_list.append('FizzBuzz')
    elif (i%5) == 0:
      fizzbuzz_list.append('Buzz')
    elif (i%3) == 0:
      fizzbuzz_list.append('Fizz')
    else:
      fizzbuzz_list.append(0)
  '''

  fizzbuzz_list =  ['FizzBuzz', 0, 0, 'Fizz', 0, 'Buzz', 'Fizz', 0, 0, 'Fizz', 'Buzz', 0, 'Fizz', 0, 0 ]
  r = n%15
  return fizzbuzz_list[r] if fizzbuzz_list[r] else n

前回と同様に以下のコマンドで回帰テストを実行できる。

python fizzbuzz_doctest.py -v

最終的なコード

コードだけでなく、ドキュメントも完成した。

# coding: utf-8

def fizzbuzz(n):
  '''
  入力値nが3の倍数であれば'Fizz'という文字列を返し、
  入力値nが5の倍数であれば'Buzz'という文字列を返し、
  入力値nが15の倍数であれば'FizzBuzz'という文字列を返し、
  それ以外は整数nを出力する関数。
  例:
  >>> fizzbuzz(3)
  'Fizz'

  >>> fizzbuzz(36)
  'Fizz'

  >>> fizzbuzz(5)
  'Buzz'

  >>> fizzbuzz(50)
  'Buzz'

  >>> fizzbuzz(15)
  'FizzBuzz'

  >>> fizzbuzz(45)
  'FizzBuzz'

  >>> fizzbuzz(2)
  2

  >>> fizzbuzz(49)
  49

  nは、整数型、長整数型、浮動点少数型で表される整数である必要がある。
  (浮動点少数型の場合、小数部分が0である必要がある。)
  少数が入力されると、ValueErrorが返される。

  >>> fizzbuzz(1.0)
  1

  >>> fizzbuzz(5.0)
  'Buzz'

  >>> fizzbuzz(5.5)
  Traceback (most recent call last):
      ...
  ValueError: n must be integer. n: 5.500000

  '''
  if n != int(n):
    raise ValueError( 'n must be integer. n: %f' % n )
  n = int(n)

  fizzbuzz_list = ['FizzBuzz', 0, 0, 'Fizz', 0, 'Buzz', 'Fizz', 0, 0, 'Fizz', 'Buzz', 0, 'Fizz', 0, 0 ]
  r = n%15
  return fizzbuzz_list[r] if fizzbuzz_list[r] else n

if __name__ == '__main__':
  import doctest
  doctest.testmod()

結論

テストとドキュメント作成が一気にできるので使い勝手が良さそう。コメントが長くなるのでコードが見づらくなるという指摘もあるけれど、ドキュメントを残しておいたほうがメンテナンス性が上がる点と、回帰テストを実行することで品質も担保できる点の2点の利点の方が大きいような気がする。

8
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
8