0
1

More than 3 years have passed since last update.

モブプロでCyber-dojoでTDDに入門

Last updated at Posted at 2019-12-11

はじめに

いなたつアドカレの十日目の記事です。

今回は前回に続いて、関西でモブプログラミングやってみぃ〜ひん??の参加記です。

今回は、実際に初めてTDDを行った時の流れを事実に基づいて(すこし改変して)、TDDってどんな感じかを書いていきます。

はじめてのてすと

前回の記事と同じようにして、pytest,FizzBuzzを始めます。
スクリーンショット 2019-12-09 23.38.54.png

テストをして、無事に怒られることができたら、次は怒られないように実装しましょう。

スクリーンショット 2019-12-09 23.41.29.png

テストが通りGreenの状態になりましたね。ここまでは前回と同様です。

これで、テストが用件を満たしていると、テストが通ることを確認できました。

fizzbuzzってみる

これで、もうhikerは用済みなので、hiker.pyとtest_hiker.pyを今回の要求に合わせて、fizzbuzzに書き換えましょう。
スクリーンショット 2019-12-10 13.38.00.png

hikerなんてモジュールがねえよ!って怒られましたね。もろもろfizzbuzzに対応させて書き換えましょう。

fizzbuzz.py
class FizzBuzz:

    def answer(self):
        return 6 * 7
test_fizzbuzz.py
import fizzbuzz


def test_life_the_universe_and_everything():
    '''a simple example to start you off'''
    douglas = fizzbuzz.FizzBuzz()
    assert douglas.answer() == 42

としてテストを実行します。
スクリーンショット 2019-12-10 13.40.16.png
テストが成功しましたよ!!これは喜ぶべき世紀の発見です(大袈裟)とりあえず楽しい気分になるためにみんなで喜びます。

状態がRedからGreenになったので次はRefactoringですね。

とりあえず初めからあるコメントとかは消してみやすくしておきましょう。
そしてテストコードのimport周りも少し綺麗にしました。

test_fizzbuzz.py
from fizzbuzz import FizzBuzz


def test_life_the_universe_and_everything():
    douglas = FizzBuzz()
    assert douglas.answer() == 42

すこしこれでプログラムがリファクタされましたね。
プログラムを書き換えたので、テストをします。通ってたテストが通らなくなっては困りますね。
スクリーンショット 2019-12-10 13.46.26.png
無事にテストコードの通過を保てていますね。

FizzBuzz開始!!

TDDの流れがわかったのでこれからFizzBuzzを進めていきます。まずは、FizzBuzzの用件を適切な粒度に切り分けていきましょう。

FizzBuzzの最小のシナリオってなんだ

FizzBuzzの最小のシナリオを考えるためには、まずはFizzBuzzに必要な要素を洗い出します

  • 引数が1のとき返り値が1
  • 引数が2のとき返り値が2
  • 引数が3のとき返り値がFizz
  • 引数が5のとき返り値がBuzz
  • 引数が6のとき返り値がFizz
  • 引数が10のとき返り値がBuzz
  • 引数が15のとき返り値がFizzBuzz
  • 引数が30のとき返り値がFizzBuzz

こんなものですかね。最小のシナリオに今回は一番上の「引数が1のとき返り値が1」としました。

本来はこうやって用件を切り分けて洗い出すべきですが、はじめてだったので僕たちは割愛しました。

引数が1のとき返り値が1のテストと実装

TDDはまずはテストコードを書きます。
fizzbuzzをするメソッドはfizzbuzzで引数に対する返り値をテストしていきましょう。

test_fizzbuzz.py
from fizzbuzz import FizzBuzz


def test_life_the_universe_and_everything():
    douglas = FizzBuzz()
    assert douglas.answer() == 42

def test_引数が1のとき返り値が1():
    douglas = FizzBuzz()
    assert douglas.fizzbuzz(1) == "1"

テストを実行すると、fizzbuzzなんてメソッドないよ!って怒られるよね〜などいいながら怒られることを心待ちにしてテストを押します。

おこられました。予想通りですね。
スクリーンショット 2019-12-10 14.00.23.png

じゃあ、fizzbuzzを実装しましょう。

fizzbuzz.py
class FizzBuzz:

    def answer(self):
        return 6 * 7

    def fizzbuzz(i):
        return "1"

これで、テストは通るでしょう!よし、テストです!!

スクリーンショット 2019-12-10 14.11.33.png

あれ、引数の数がおかしいぞ、ってエラーを吐きましたね。

ぐぐりましょう。

どうやら、自分自身を渡すようにしなくてはいけないようですね。慣習的にselfと名付けるようです。

では、プログラムを修正します。

fizzbuzz.py
class FizzBuzz:

    def answer(self):
        return 6 * 7

    def fizzbuzz(self, i):
        return '1'

よくみてみると、answerの引数にもselfがありましたね。これに倣ってselfを引数として与えました。

スクリーンショット 2019-12-10 14.14.01.png

お、これでテストを通過しました。

「引数が1のとき返り値が1」を無事にクリアできましたね。
リファクタリングは、ダブルコーテーションとシングルコーテーションが混在しているので、シングルコーテーションに統一しておきましょう。

これで無事に最小のシナリオがクリアできたので、納品ですね!!

いやいや、まてと、1の時しかテストしてないから、大丈夫だとは思うけど、2の時のテストもしておこう、では2のときのテストをしてみます。

引数が2のとき返り値が2

先ほどと同様に2の場合のテストをかきます

test_fizzbuzz.py
from fizzbuzz import FizzBuzz


def test_life_the_universe_and_everything():
    douglas = FizzBuzz()
    assert douglas.answer() == 42

def test_引数が1のとき返り値が1():
    douglas = FizzBuzz()
    assert douglas.fizzbuzz(1) == '1'
    
    def test_引数が2のとき返り値が2():
    douglas = FizzBuzz()
    assert douglas.fizzbuzz(2) == '2'

こんな感じですね。2の時に2を返すかのテストを追加しました。
よし、テストしましょう。
スクリーンショット 2019-12-10 14.22.52.png

エラーですね。よかったこのまま納品してなくて、、、

エラー内容は2が欲しいのに1なんか寄越すんじゃねえっておこっていますね。

こまりました。どうしましょう?ここで聡明なプログラマはいいました。「そのまま引数を返してやろう」
タイピストは言われるがまま、コードを書き換えます。

fizzbuzz.py
class FizzBuzz:

    def answer(self):
        return 6 * 7

    def fizzbuzz(self, i):
        return i

スクリーンショット 2019-12-10 14.26.17.png
おぉっと?エラーが2つでてしまいました。
文字列と数字の一致を確認していたので、とりあえず引数を文字列に変換して返してみます。

fizzbuzz.py
class FizzBuzz:

    def answer(self):
        return 6 * 7

    def fizzbuzz(self, i):
        return str(i)

これでテストしてみます。

スクリーンショット 2019-12-10 15.43.12.png
テスト成功ですね。大いに喜びましょう。

  • 引数が1のとき返り値が1
  • 引数が2のとき返り値が2

のふたつの項目がクリアできました。

リファクタリングですね。

fizzbuzz.py
class FizzBuzz:

    def answer(self):
        return 6 * 7

    def fizzbuzz(self, arg):
        return str(arg)

引数iはわかりにくいのでargとでもしておきましょうか

引数が3のとき返り値がFizz

そろそろ、テストを書いてエラーを確認する必要はないでしょう。

fizzbuzz.py
class FizzBuzz:

    def answer(self):
        return 6 * 7

    def fizzbuzz(self, arg):
        if (arg == 3):
            return 'Fizz'
        return str(arg)
test_fizzbuzz.py
from fizzbuzz import FizzBuzz


def test_life_the_universe_and_everything():
    douglas = FizzBuzz()
    assert douglas.answer() == 42

def test_引数が1のとき返り値が1():
    douglas = FizzBuzz()
    assert douglas.fizzbuzz(1) == '1'
    
def test_引数が2のとき返り値が2():
    douglas = FizzBuzz()
    assert douglas.fizzbuzz(2) == '2'
    
def test_引数が3のとき返り値がFizz():
    douglas = FizzBuzz()
    assert douglas.fizzbuzz(3) == 'Fizz'

3のケースではこれでうまくいきましたね。
スクリーンショット 2019-12-10 16.59.37.png

では、ここで仕様を確認してみましょう。

readme.txt
Write a program that prints the numbers from 1 to 100.
But for multiples of three print "Fizz" instead of the
number and for the multiples of five print "Buzz". For
numbers which are multiples of both three and five
print "FizzBuzz".

Sample output:

1
2
Fizz
4
Buzz
Fizz
7
8
Fizz
Buzz
11
Fizz
13
14
FizzBuzz
16
17
Fizz
19
Buzz
... etc up to 100

6の場合もFizzと表示されると仕様にはかいてありますね。

引数が6のとき返り値がFizzの場合のテストを作成して、テストしてみましょう。
スクリーンショット 2019-12-10 18.29.37.png

6が返り値になってますね。だめです。

仕様を考えてみましょう。

Fizzと表示したいのは、どうやら3で割り切れる時であることがわかりました。

そしてモブは、「3の剰余が0のときにFizzと表示するようにif文を書き換えてください」といいます。
タイピストは機械のように実装をします。

fizzbuzz.py
class FizzBuzz:

    def answer(self):
        return 6 * 7

    def fizzbuzz(self, arg):
        if (arg % 3 == 0):
            return 'Fizz'
        return str(arg)

剰余を条件に書き換えれていますね。

スクリーンショット 2019-12-10 18.40.36.png

無事に3の倍数の時にfizzと表示することができました。

現在のテストコードをみてみます。

test_fizzbuzz.py
from fizzbuzz import FizzBuzz


def test_life_the_universe_and_everything():
    douglas = FizzBuzz()
    assert douglas.answer() == 42

def test_引数が1のとき返り値が1():
    douglas = FizzBuzz()
    assert douglas.fizzbuzz(1) == '1'
    
def test_引数が2のとき返り値が2():
    douglas = FizzBuzz()
    assert douglas.fizzbuzz(2) == '2'
    
def test_引数が3のとき返り値がFizz():
    douglas = FizzBuzz()
    assert douglas.fizzbuzz(3) == 'Fizz'
    
def test_引数が6のとき返り値がFizz():
    douglas = FizzBuzz()
    assert douglas.fizzbuzz(6) == 'Fizz'

テストはこんなに必要なのでしょうか?
今回のリファクタリングでは、不要なテストを削除していきます。

test_fizzbuzz.py
from fizzbuzz import FizzBuzz

    
def test_引数が2のとき返り値が2():
    douglas = FizzBuzz()
    assert douglas.fizzbuzz(2) == '2'

def test_引数が6のとき返り値がFizz():
    douglas = FizzBuzz()
    assert douglas.fizzbuzz(6) == 'Fizz'

この二つさえのこしていれば、固定値ではなく倍数で処理ができていることがかくにんできますね

不要なテストケースを削除することも立派なリファクタリングです。

引数が5のとき返り値がBuzz

さきほど、仕様を確認したので、これも5の倍数であることは自明なので5と10の場合のテストコードを書いて、実装をします。

fizzbuzz.py
class FizzBuzz:

    def answer(self):
        return 6 * 7

    def fizzbuzz(self, arg):
        if (arg % 3 == 0):
            return 'Fizz'
        if (arg % 5 == 0):
            return 'Buzz'
        return str(arg)
test_fizzbuzz.py
from fizzbuzz import FizzBuzz

    
def test_引数が2のとき返り値が2():
    douglas = FizzBuzz()
    assert douglas.fizzbuzz(2) == '2'

def test_引数が6のとき返り値がFizz():
    douglas = FizzBuzz()
    assert douglas.fizzbuzz(6) == 'Fizz'
   
def test_引数が5のとき返り値がBuzz():
    douglas = FizzBuzz()
    assert douglas.fizzbuzz(5) == 'Buzz'
    
def test_引数が10のとき返り値がBuzz():
    douglas = FizzBuzz()
    assert douglas.fizzbuzz(10) == 'buzz'

スクリーンショット 2019-12-10 18.52.02.png

おっとエラーです、これはテストケースが間違っていますね。修正しましょう。

不要なテストコードhじゃ消しておきましょう。5のケースを削除します。

引数が15のとき返り値がFizzBuzz

さきほど、仕様を確認したので、これも15の倍数であることは自明なので15と30の場合のテストコードを書いて、実装をします。

fizzbuzz.py
class FizzBuzz:

    def answer(self):
        return 6 * 7

    def fizzbuzz(self, arg):
        if (arg % 3 == 0):
            return 'Fizz'
        if (arg % 5 == 0):
            return 'Buzz'
        if (arg % 15 == 0):
            return 'FizzBuzz'
        return str(arg)
test_fizzbuzz.py
from fizzbuzz import FizzBuzz

    
def test_引数が2のとき返り値が2():
    douglas = FizzBuzz()
    assert douglas.fizzbuzz(2) == '2'

def test_引数が6のとき返り値がFizz():
    douglas = FizzBuzz()
    assert douglas.fizzbuzz(6) == 'Fizz'
    
def test_引数が10のとき返り値がBuzz():
    douglas = FizzBuzz()
    assert douglas.fizzbuzz(10) == 'Buzz'
    
def test_引数が15のとき返り値がFizzBuzz():
    douglas = FizzBuzz()
    assert douglas.fizzbuzz(15) == 'FizzBuzz'
    
def test_引数が30のとき返り値がFizzBuzz():
    douglas = FizzBuzz()
    assert douglas.fizzbuzz(30) == 'FizzBuzz'
    

スクリーンショット 2019-12-10 19.01.46.png

Fizzと表示されているようですね。3の倍数が先に反応してしまっているようです。
それもそうです、3の倍数の確認5の倍数の確認、15の倍数の確認の順番で確認していますからね。

では順番を入れ替えてみましょう。

fizzbuzz.py
class FizzBuzz:

    def answer(self):
        return 6 * 7

    def fizzbuzz(self, arg):
        if (arg % 15 == 0):
            return 'FizzBuzz'
        if (arg % 3 == 0):
            return 'Fizz'
        if (arg % 5 == 0):
            return 'Buzz'

        return str(arg)

スクリーンショット 2019-12-10 19.05.21.png

うまくいきましたね、これで納品できます。現場は大歓声ですね。

では最後のリファクタリングです。

if文が並んでいるのは気持ち悪いので、elseなどをしっかりとつかいましょう。

fizzbuzz.py
class FizzBuzz:

    def answer(self):
        return 6 * 7

    def fizzbuzz(self, arg):
        if (arg % 15 == 0):
            return 'FizzBuzz'
        else if (arg % 3 == 0):
            return 'Fizz'
        else if (arg % 5 == 0):
            return 'Buzz'

        return str(arg)

スクリーンショット 2019-12-10 19.10.07.png

どうやらelse if なんてのはだめみたいです、pythonはelifで条件分岐するみたいです。
ついでに数字を出力するケースはelseに入れておきましょう

fizzbuzz.py
class FizzBuzz:

    def answer(self):
        return 6 * 7

    def fizzbuzz(self, arg):
        if (arg % 15 == 0):
            return 'FizzBuzz'
        elif (arg % 3 == 0):
            return 'Fizz'
        elif (arg % 5 == 0):
            return 'Buzz'
        else
            return str(arg)

テストしてみます。これで、、、おわ、、、、

スクリーンショット 2019-12-10 19.12.44.png

りませんでした。。。。

すっかりコロンをわすれていましたね。

fizzbuzz.py

class FizzBuzz:

    def answer(self):
        return 6 * 7

    def fizzbuzz(self, arg):
        if (arg % 15 == 0):
            return 'FizzBuzz'
        elif (arg % 3 == 0):
            return 'Fizz'
        elif (arg % 5 == 0):
            return 'Buzz'
        else:
            return str(arg)

今度こそこれで完了です。
スクリーンショット 2019-12-10 19.13.52.png

やっと納品ですね!拍手喝采今日は飲み会でしょう。飲み会はしてませんが。

終わりに

モブプロで初めてTDDをやってみたときの全体的な流れや、失敗内容をそのまま(ってほどそのままでもない)おとどけしました。

こんな感じで、

  1. テストを書く
  2. 実装する
  3. リファクタリング

のサイクルを回して、TDDをおこなっていくのですが、それをモブプロですることによって、少し違った感覚で行えてとても面白かったです。
テストの結果がわかりやすく、とりあえず実行してみて、怒られたらググってみようみたいな雰囲気がいつもググってからコードをかいてるのでとても新鮮でした。

モブプロ、やろうぜ

おまけ

最後の最後のリファクタリング

とある方はいいました。「僕がつかってる言語なら三項演算子ってのがあるのですが、pythonはどうでしょう?」

fizzbuzz.py
class FizzBuzz:

    def answer(self):
        return 6 * 7

    def fizzbuzz_bu(self, arg):
        if (arg % 15 == 0):
            return 'FizzBuzz'
        elif (arg % 3 == 0):
            return 'Fizz'
        elif (arg % 5 == 0):
            return 'Buzz'
        else:
            return str(arg)
            
    def fizzbuzz (self, arg):
        if (arg % 15 == 0):
            return 'FizzBuzz'
        elif (arg % 3 == 0):
            return 'Fizz'
        elif (arg % 5 == 0):
            return 'Buzz'
        else:
            return str(arg)

とりあえず、完成したプログラムを残しつつ、これから三項演算子バージョンに書き換えてみます。

まずは、5の倍数の場合のみを三項演算子に置き換える指令がでました。

fizzbuzz.py
class FizzBuzz:

    def answer(self):
        return 6 * 7

    def fizzbuzz_bu(self, arg):
        if (arg % 15 == 0):
            return 'FizzBuzz'
        elif (arg % 3 == 0):
            return 'Fizz'
        elif (arg % 5 == 0):
            return 'Buzz'
        else:
            return str(arg)
            
    def fizzbuzz (self, arg):
        if (arg % 15 == 0):
            return 'FizzBuzz'
        elif (arg % 3 == 0):
            return 'Fizz'
        arg = 'Buzz' if arg % 5 == 0 else arg

        return str(arg)

スクリーンショット 2019-12-10 19.24.42.png

テストが通ってることが確認できるので、リファクタリング成功です。
では、別のケースも置き換えましょう、

fizzbuzz.py
class FizzBuzz:

    def answer(self):
        return 6 * 7

    def fizzbuzz_bu(self, arg):
        if (arg % 15 == 0):
            return 'FizzBuzz'
        elif (arg % 3 == 0):
            return 'Fizz'
        elif (arg % 5 == 0):
            return 'Buzz'
        else:
            return str(arg)
            
    def fizzbuzz (self, arg):
        arg = 'FizzBuzz' if arg % 15 == 0 else arg
        arg = 'Fizz' if arg % 3 == 0 else arg
        arg = 'Buzz' if arg % 5 == 0 else arg

        return str(arg)

スクリーンショット 2019-12-10 19.26.54.png

エラーが大量にでてしまいました。それも当然です。文字列の剰余を取ろうとしたりしているのですから。

ではひとつの三項演算子にしてはどうでしょうか?との声があったので、elseの中身と、代入を消してみました。

fizzbuzz.py
class FizzBuzz:

    def answer(self):
        return 6 * 7

    def fizzbuzz_bu(self, arg):
        if (arg % 15 == 0):
            return 'FizzBuzz'
        elif (arg % 3 == 0):
            return 'Fizz'
        elif (arg % 5 == 0):
            return 'Buzz'
        else:
            return str(arg)
            
    def fizzbuzz (self, arg):
        arg = 'FizzBuzz' if arg % 15 == 0 else 
        'Fizz' if arg % 3 == 0 else 
        'Buzz' if arg % 5 == 0 else 

        return str(arg)

「これでいけるでしょう!!!!!」といいながらテストをします。

スクリーンショット 2019-12-10 19.32.49.png

だめです。

改行がダメなのでしょう、どうやって長い文のプログラムを複数行に分けるかをぐぐると、バックスラッシュで行けるとの声があったので、つけくわわえてみます。

fizzbuzz.py
class FizzBuzz:

    def answer(self):
        return 6 * 7

    def fizzbuzz_bu(self, arg):
        if (arg % 15 == 0):
            return 'FizzBuzz'
        elif (arg % 3 == 0):
            return 'Fizz'
        elif (arg % 5 == 0):
            return 'Buzz'
        else:
            return str(arg)
            
    def fizzbuzz (self, arg):
        arg = 'FizzBuzz' if arg % 15 == 0 else \
        'Fizz' if arg % 3 == 0 else \
        'Buzz' if arg % 5 == 0 else \
        arg
        return str(arg)

スクリーンショット 2019-12-10 19.33.35.png

やった!成功です!!完成と同時に歓声が上がります。
三項演算子バージョンができたので、バックアップは消して、さぁ、納品です!!!!!!!

fizzbuzz.py
class FizzBuzz:

    def answer(self):
        return 6 * 7

    def fizzbuzz (self, arg):
        arg = 'FizzBuzz' if arg % 15 == 0 else 
        'Fizz' if arg % 3 == 0 else 
        'Buzz' if arg % 5 == 0 else 

        return str(arg)
0
1
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
0
1