pytestでFizzBuzz問題をテスト駆動開発（TDD）

## 分かること
**・テスト駆動開発（TDD）はどの順序で開発すべきか？**
**・テスト駆動開発（TDD）はなぜ失敗をわざと行うのか？**
**・pytestはどう使うべきか？**

## 目次
**1. テスト駆動開発（TDD）について**
**2. Python, Pytestでテスト駆動開発**（[FizzBuzz問題](https://wa3.i-3-i.info/word18535.html)）

#1. テスト駆動開発（TDD）について
**テスト駆動開発は以下3種のサイクルである。**
（ToDoリストを作成し、簡単な処理から手を付けると良い）

**１. <font color="Red">RED（テスト失敗）</font>**
テストコードを作成してテストする。（実装コードを作成してないので失敗する）
**2. <font color="LimeGreen">GREEN（テスト成功）</font>**
成功するように必要最小限の実装コードを書く。（正しさや綺麗さは考えない）
**3. <font color="LimeGreen">リファクタリング</font>**
成功を維持したままコードを綺麗にする
<img width="500" alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/479337/3978fb6b-a414-456d-9b22-da22172e5c6c.png">
(画像：[50 分でわかるテスト駆動開発 ](https://www.slideshare.net/decode2017/do03-50)より)


## なぜ失敗を工程に入れるのか？
テストコードの正しさを確認（テストコードのテスト）する為。
誤りのある実装コードのテストが予想通りのエラーならば、正しい実装コードでもテストが正しく機能されると期待できる。



#2. Pytestでテスト駆動開発（[FizzBuzz問題](https://wa3.i-3-i.info/word18535.html)）
[FizzBuzz問題](https://wa3.i-3-i.info/word18535.html)をテスト駆動開発で実装し、テスト駆動開発の工程を確認する。

>[FizzBuzz問題](https://wa3.i-3-i.info/word18535.html)
1から100までの数をプリントするプログラムを書け。
ただし
3の倍数のときは数の代わりに｢Fizz｣とプリントし、
5の倍数のときは｢Buzz｣とプリントし、
3と5両方の倍数の場合には｢FizzBuzz｣とプリントすること。


##0. [準備] ToDoリストを作成する
簡単な処理から優先順位を高くすると良い。

```
- [ ] 数を文字列にして返す

- [ ] ３の倍数のときは数の代わりに文字列'Fizz文字列'と返す
- [ ] ５の倍数のときは文字列'Buzz'と返す
- [ ] ３と５の倍数の場合には文字列'FizzBuzz'と返す

- [ ] 1から100までの数
- [ ] プリントする
```

##1. ToDoリストから１つをピックアップしてサイクルを回す

```
- [ ] 数を文字列にして返す
    - [ ] 1を渡したら文字列"1"を返す  ← next
    - [ ] 2を渡したら文字列"2"を返す
```
必要ならToDoの深堀りをする。

###1-1. <font color="Red">RED（テスト失敗）</font>
テストコードを作成してテストでわざと失敗する
######1-1-1. テストコードを作成(test_fizz_buzz.py)
「1を渡したら文字列"1"を返す」テストを作成する

```python:test_fizz_buzz.py
import pytest

class TestStringConvert:
    # 前準備
    @pytest.fixture
    def target(self):
        from fizz_buzz import string_convert        
        return string_convert

    # 検証
    def test_return_value(self, target):
        assert target(1) == '1'
    
```
この時点で既に、
・実装ファイルとテストコードのファイル名（fizz_buzz.py, test_fizz_buzz.py)
・数を文字列にして返す関数名
・数を文字列にして返す処理をテストするクラスと関数（TestStringConvert, test_return_value）
と考えることが多い為、ToDoは簡単な処理から始めるのがおすすめ。

"@pytest.fixture"はテストの前処理(実装コードで使う関数の呼び出し)を行うpytestの機能。

######1-1-2. テストコードをテスト(test_fizz_buzz.py)
実装コードを作成してない（実装モジュールを作成してない）ので失敗する

```powershell
pytest test_fizz_buzz.py
============================= test session starts =============================
-
collected 1 item

test_fizz_buzz.py E                                                      [100%]

=================================== ERRORS ====================================
____________ ERROR at setup of TestStringConvert.test_return_value ____________

self = <test_fizz_buzz.TestStringConvert object at 0x000001EF5648A190>

    @pytest.fixture
    def target(self):
>       from fizz_buzz import string_convert
E       ModuleNotFoundError: No module named 'fizz_buzz'

test_fizz_buzz.py:7: ModuleNotFoundError
=========================== short test summary info ===========================
ERROR test_fizz_buzz.py::TestStringConvert::test_return_value - ModuleNotFoun...
========================= 1 error in 0.18s =========================
```
予想（狙い）通り、fizz_buzzモジュールがないエラーなら成功。

```powershell
E       ModuleNotFoundError: No module named 'fizz_buzz'
```
このエラーが出なければ、テストコードのどこかに誤りがあると分かる。

### 1-2. <font color="LimeGreen">GREEN（テスト成功）</font>
テストコードが成功することだけを優先して必要最小限の実装コードを書く。
（この時点で正しさや綺麗さは考えなくて良い）

#### 1-2-1. 必要最小限の実装コードを書く(fizz_buzz.py)
```python:fizz_buzz.py
def string_convert(x):
    return '1'

```

#### 1-2-2. テストコードをテスト(test_fizz_buzz.py)
```powershell
pytest test_fizz_buzz.py
============================= test session starts =============================
-
collected 1 item

test_fizz_buzz.py .                                                      [100%]
======================== 1 passed in 0.02s =========================
```
成功。

###（1-3. <font color="LimeGreen">リファクタリング</font>）
今回はそのまま次のサイクルへ向かう。

##2. 次のサイクルを回す
```
- [ ] 数を文字列にして返す　　　　　　　　 
    - [x] 1を渡したら文字列"1"を返す    
    - [ ] 2を渡したら文字列"2"を返す　  ← next
```

###2-1. <font color="Red">RED（テスト失敗）</font>
#### 2-1-1. テストコードを拡張(test_fizz_buzz.py)
「2を渡したら文字列"2"を返す」テストを追加する

```diff_python:test_fizz_buzz.py
 import pytest

 class TestStringConvert:
     # 前準備
     @pytest.fixture
     def target(self):
         from fizz_buzz import string_convert        
         return string_convert

+    check_value_data = [
+        (1, '1'),  # 1を渡したら文字列"1"を返す
+        (2, '2')   # 2を渡したら文字列"2"を返す
+    ]
    
     # 検証
-    def test_return_value(self, target):      
-       assert target(x) == '1'
+    @pytest.mark.parametrize('x, expected_return_value', check_value_data)
+    def test_return_value(self, target, x, expected_return_value):      
+        assert target(x) == expected_return_value

```
@pytest.mark.parametrizeで以下２パターン（計２回）のテストを定義する。
>１．x = 1, expected_return_value= '1'
２．x = 2, expected_return_value= '2'

##### →１つのテストに複数のassert文を記述するのは避ける。
以下のような記述は避ける。 
（assert1でエラーとなったとき、assert2のテスト結果が分からない為。）

```python

    # 検証（避けたい記述方法）
    def test_return_value(self, target):      
        assert target(1) == '1'  # assert1 
        assert target(2) == '2'  # assert2（assert1がエラーだと実行されない）

```
#### 2-1-2. テストコードをテスト(test_fizz_buzz.py)
現在の実装コード（fizz_buzz.py）はどんな値を入れても文字列1を返す為、エラーが出る。

```powershell
pytest test_fizz_buzz.py
============================= test session starts =============================
-
collected 2 items

test_fizz_buzz.py .F                                                     [100%]

================================== FAILURES ===================================
__________________ TestStringConvert.test_return_value[2-2] ___________________

self = <test_fizz_buzz.TestStringConvert object at 0x0000013B7BC5C4F0>
target = <function string_convert at 0x0000013B7BC32DC0>, x = 2
expected_return_value = '2'

    @pytest.mark.parametrize('num, expected_return_value', check_value_data)
    def test_return_value(self, target, x, expected_return_value):
>       assert target(x) == expected_return_value
E       AssertionError: assert '1' == '2'
E         - 2
E         + 1

test_fizz_buzz.py:18: AssertionError
=========================== short test summary info ===========================
FAILED test_fizz_buzz.py::TestStringConvert::test_return_value[2-2] - Asserti...
=================== 1 failed, 1 passed in 0.19s ====================

```
### 2-2. <font color="LimeGreen">GREEN（テスト成功）</font>
テストの（狙い通りの）失敗を確認後、テストが成功するように実装コードを訂正する。

```diff_python:fizz_buzz.py
  def string_convert(x):
-     return '1'
+     return str(x)
    
```

```powershell
pytest test_fizz_buzz.py
============================= test session starts =============================
platform win32 -- Python 3.8.5, pytest-6.1.1, py-1.9.0, pluggy-0.13.1
rootdir: C:\Users\spell\Documents\GIT\fizzbuzz
collected 2 items

test_fizz_buzz.py ..                                                     [100%]
======================== 2 passed in 0.02s =========================
```
2つのテストが実行されたことも分かる。

```powershell
collected 2 items
```
### 2-3. <font color="LimeGreen">リファクタリング</font>）
成功を維持したままコードを綺麗にする

```diff_python:test_fizz_buzz.py
 import pytest

 class TestStringConvert:
     @pytest.fixture
     def target(self):
         from fizz_buzz import string_convert        
         return string_convert

     check_value_data = [
         (1, '1'),
         (2, '2')
     ]
    
-    @pytest.mark.parametrize('x, expected_return_value', check_value_data)
-    def test_return_value(self, target, x, expected_return_value):      
-        assert target(x) == expected_return_value
+    @pytest.mark.parametrize('num, expected_return_value', check_value_data)
+    def test_return_value(self, target, num, expected_return_value):      
+        assert target(num) == expected_return_value
```

```diff_python:fizz_buzz.py
-  def string_convert(x):
-      return str(x)
+  def string_convert(num):
+      return str(num)
    
```

##3. 次のサイクルを回す
```
- [X] 数を文字列にして返す                          ← finish
    - [X] 1を渡したら文字列'1'を返す
    - [X] 2を渡したら文字列'2'を返す

- [ ] 3の倍数のときは数の代わりに文字列'Fizz'と返す   
    - [ ] 3を渡したら文字列'Fizz'を返す             ← next
- [ ] 5の倍数のときは文字列'Buzz'と返す
- [ ] 3と5の倍数の場合には文字列'FizzBuzz'と返す

- [ ] 1から100までの数
- [ ] プリントする
```
###3-1. <font color="Red">RED（テスト失敗）</font>
#### 3-1-1. テストコードを拡張(test_fizz_buzz.py)
「3を渡したら文字列"Fizz"を返す」テストを追加する

```python:test_fizz_buzz.py
import pytest

class TestStringConvert:
    @pytest.fixture
    def target(self):
        from fizz_buzz import string_convert        
        return string_convert

    check_value_data = [
        (1, '1'),
        (2, '2'),
        (3, 'Fizz')
    ]
    
    @pytest.mark.parametrize('num, expected_return_value', check_value_data)
    def test_return_value(self, target, num, expected_return_value):      
        assert target(num) == expected_return_value

```
### 3-2. <font color="LimeGreen">GREEN（テスト成功）</font>
### 3-2-1. 成功するように必要最小限のコードを書く（倍数関係なく3ならFizzを返す）

```diff_python:fizz_buzz.py
 def string_convert(num):
+    if num == 3:
+        return 'Fizz'
    
     return str(num)

```

### 3-2-2. 成功したら正しい処理（3の倍数ならFizzを返す）をするよう訂正

```diff_python:fizz_buzz.py
 def string_convert(num):
-    if num == 3:
+    if num % 3 == 0:
         return 'Fizz'
    
     return str(num)

```
###3-3. <font color="LimeGreen">リファクタリング</font>
・テストコードから仕様が読み取れない（個別の具体的な動きしかない）為、テストコードだけで仕様を読み取れるようにする。
・また、テストのパターン数に対称性を持たせる。
（メンテナンスを減らす為にも場合分けは減らして対称性を持たせる）

```diff_python:test_fizz_buzz.py
 import pytest

 class TestStringConvert:
     @pytest.fixture
     def target(self):
         from fizz_buzz import string_convert        
         return string_convert

     check_value_data = [
-        (1, '1'),
-        (2, '2'),
+        # 3の倍数のとき
         (3, 'Fizz'),
+        # その他の数のとき
+        (1, '1')
     ]
    
     @pytest.mark.parametrize('num, expected_return_value', check_value_data)
     def test_return_value(self, target, num, expected_return_value):      
         assert target(num) == expected_return_value
```

今回はコメントアウトで説明したが、以下の手法でも効果的。
>・テスト関数名を長くして仕様を説明する。
・パターンごとにテストクラス分けを行う。


以上を繰り返すのがテスト駆動開発（TDD）である。


参考動画：[50 分でわかるテスト駆動開発](https://speakerdeck.com/twada/tdd-live-in-50-minutes)（[@t_wadaさん](https://twitter.com/t_wada)によるライブコーディング）