📑

自信をもってコードを書く!TDDハンズオン

2021/03/12に公開

1. ハンズオンのゴール

明日から、より自信を持ってコードを書けるようになる

2. こんな悩みがありませんか?

  • コードにバグがない自信がない
  • コードレビューしてもらったら、バグが見つかって手戻りが多い
  • 実装が終わるまで動作確認できないので、うまく動くか心配
  • 使いやすいコードになっているか不安

3. 悩みはTDDで解決します

  • バグが入りづらい(仕様がテストとして記述される)
  • 使いやすい関数やモジュールが作れる(使う側から考える)
  • 小さく実装して動作確認するので短い時間で動くことが確認できる
  • 進捗がわかりやすい(TODOリスト)

4. TDDとは

  • テスト駆動解発
  • プログラム開発手法のひとつ
  • テスト手法ではない!

個人的には、失敗を上手に活用した成功体験を積める開発手法だと思っています。最初に覚えてほしい開発手法です。

5. TDDの流れ

  1. 失敗するテストを書く
  2. テストを実行して失敗を確認
  3. テストを成功させる最低限の実装を書く
  4. テストを実行し成功を確認する
  5. テストが落ちないように、実装をリファクタリングする

6. TDDはサイクル

テストファースト、レッド、グリーン、リファクタリングの繰り返しです。ただし、リファクタリングは必ずしなければならないわけではないです。

各用語については次で説明します。

7. 用語

ここで使う用語を簡単に説明します。

  • テストファースト: 実装する前にテストを先に書くこと
  • レッド: テストが失敗している状態のこと
  • グリーン: テストが成功している状態のこと
  • リファクタリング: 外からみた振る舞いはそのままで、内部構造を改善すること

8. 具体例で想像してみよう

足し算をする add 関数を実装する。

  1. add(1, 2) が3になるテストを書く
  2. テストを実行してadd関数が未実装のためテストが落ちる
  3. add関数が3を返すようにする(ハードコーディング)
  4. テストを実行し、テストがパスすることを確認する
  5. ハードコーディングした実装を引数同士の足し算に書き直してリファクタリングする

9. TDDで重要ポイント

  1. やることリスト(TODOリスト)を用意する
  2. いきなり難しい実装をしない
  3. テストケースを複雑にしない
  4. やることリストは随時増やしていく

これらができると、やるべきことが明らかになり、進捗を感じやすく、自信が持てる

10. セマンティックバージョニング

Semantic Versioning を題材にやっていきます。

ライブラリのバージョンとかでみる、あいつ。たとえば、1.0.0とか0.3.9-alphaとか。
仕様は、 https://semver.org/lang/ja/ にまとめられている

Node.js のライブラリにはセマンティックバージョニングが推奨されている

11. 今回実装するセマンティックバージョニング

すべての仕様を満たすことが目標ではないので簡略化させます。下に書いたリストはTODOリストとしてTDDを行うときに参照されます。

* Semverインスタンスが作成できる
* 比較できる
  * Semver(3, 0, 0) == Semver(3, 0, 0)
  * Semver(1, 0, 0) < Semver(3, 0, 0)
  * Semver(0, 0, 1) < Semver(0, 0, 2)
* バージョンアップできる
* Semverを文字列で表示できる

1.2.3というバージョンは、メジャーバージョン、マイナーバージョン、パッチバージョンの三つに分けることができます。1, 2, 3 がそれぞれメジャー、マイナー、パッチにあたります。

12. 実装スタート

https://github.com/tamanobi/practice-tdd を git-clone する。今回はPythonだが、ほかのプログラミング言語でもOKです。

13. セットアップ

Python 3.8 以上が必要です。次の手順でTDDが行える準備をしましょう。

Python は仮想環境を標準ライブラリ( venv )で作成できます。 python3 で実行しましょう!

$ python3 -m venv .venv  # 仮想環境を作成
$ source .venv/bin/activate  # 仮想環境を使用する
$ pip install pytest black

プロジェクトごとに仮想環境を用意することはエンジニアにとって基本的なテクニックです。仮想環境を作ることで、ライブラリや環境にまつわるトラブルを回避できます。

次のコマンドがエラーにならなければセットアップ完了です。

$ make test  # 自動テスト
$ make format  # 自動コードフォーマット

もし、あなたがPython初心者なら https://www.sejuku.net/blog/64106 を読んでおいてください。Pythonにおける self の理解が不十分だなと思う方にもおすすめです。

14. ファイルの確認

  • semver/core.py: 実装コード
  • semver/test_core.py: テストコード
  • todo.md: TODOリスト

これらのファイルを変更していきます。

15. まずはTODOリストを確認

* Semverインスタンスが作成できる
* 比較できる
  * Semver(3, 0, 0) == Semver(3, 0, 0)
  * Semver(1, 0, 0) < Semver(3, 0, 0)
  * Semver(0, 0, 1) < Semver(0, 0, 2)
* バージョンアップできる
* Semverを文字列で表示できる

16. 手の付けやすいところからやっていく

まずは、Semverインスタンスが作成できるに着手していきましょう。まずはテストケースを書くために test_core.py を編集します。

  import pytest
  from semver.core import Semver

  class TestCore():
      def test_instance(self):
-         pass
+         assert Semver()

テストを実行して失敗すること確認しましょう! 赤いエラーメッセージが出たと思います。

$ make test

17. インスタンスを作成する(グリーン)

テストが失敗してレッドの状態になったので、これをグリーンにすることを目指します。早速 core.py を編集しましょう。

インスタンスが作成できればいいので、最低限の実装を行います。

+class Semver:
+    def __init__(self):
+        pass

本来、Semverにはmajor, minor, patchの変数が必要ですがテストを成功させることを優先します。

テストを実行します。

$ make test

テストが成功しているはずです。

18. インスタンス生成時に引数を渡す(レッド)

Semverインスタンスが生成できることが確認できました。次はリファクタリングですが、このコードはこれ以上リファクタリングできないので、別のテストケースを考えます。

Semverインスタンスは生成できますが、生成時にバージョン指定ができないため、あまり役に立ちません。インスタンス生成時にバージョンを指定できるようにしましょう。

TODOリストを更新します。TODOリストの更新は本人が分かれば好きなように変更してOKです。

ここでは、 markdown 記法のひとつである打ち消し線( ~~ ) を使ってTODOが完了した旨をマークしています。

-* Semverインスタンスが作成できる
+* ~~Semverインスタンスが作成できる~~
+* Semverインスタンス生成時にバージョン指定ができる
 * 比較できる
   * Semver(3, 0, 0) == Semver(3, 0, 0)
   * Semver(1, 0, 0) < Semver(3, 0, 0)
   * Semver(0, 0, 1) < Semver(0, 0, 2)
 * バージョンアップできる
 * Semverを文字列で表示できる

test_core.py を編集しましょう。test_instanceという関数の中身を変更します。

 from semver.core import Semver
 
 class TestCore():
-    def test_instance(self):
-        assert Semver()
+    def test_Semverインスタンス生成時にバージョン指定できる(self):
+        assert Semver(1, 2, 3)

テストを更新したので、テスト実行して失敗すること(レッドであること)を確認しましょう。

$ make test

__init__ の関数のインタフェースが合っていないということで、テストが失敗しますね。レッドの状態になりました。

19. インスタンス生成時に引数を渡す(グリーン)

これをグリーンにするにはどうしたらいいでしょうか? __init__ 関数に変更が必要そうですね。

core.py を編集しましょう

 class Semver:
-    def __init__(self):
+    def __init__(self, major, minor, patch):
         pass

引数を受け付けるようにしただけです。引数を全く使っていませんが、テストが成功する 最小限の実装 ではこれで十分です。

テストを実行しましょう。

$ make test

テストが成功しますね。TODOリストを更新しましょう!

 * ~~Semverインスタンスが作成できる~~
-* Semverインスタンス生成時にバージョン指定ができる
+* ~~Semverインスタンス生成時にバージョン指定ができる~~
 * 比較できる
   * Semver(3, 0, 0) == Semver(3, 0, 0)
   * Semver(1, 0, 0) < Semver(3, 0, 0)

20. Semverインスタンスにバージョンを保持する(レッド)

Semverインスタンス生成時にバージョンを指定することができましたが、バージョンが保持されていません。インスタンス生成後も、参照できるように書き換えましょう。

この変更は、TODOリストに載っていないので、まずはTODOリストの更新です。

 * ~~Semverインスタンスが作成できる~~
 * ~~Semverインスタンス生成時にバージョン指定ができる~~
+* Semverインスタンスはバージョンを保持する
 * 比較できる
   * Semver(3, 0, 0) == Semver(3, 0, 0)
   * Semver(1, 0, 0) < Semver(3, 0, 0)

次に test_core.py を編集しテストケースを追加しましょう。バージョン保持しているというテストはどう書けばいいでしょうか?

 class TestCore():
     def test_Semverインスタンス生成時にバージョン指定できる(self):
         assert Semver(1, 2, 3)
+
+    def test_Semverインスタンスはバージョンを保持する(self):
+        semver = Semver(1, 2, 3)
+        assert semver.major == 1
+        assert semver.minor == 2
+        assert semver.patch == 3

インスタンスのメンバーに直接アクセスすればいいですね。テストを実行してレッドになっていることを確認しましょう。

$ make test

AttributeError: 'Semver' object has no attribute 'major' が出てエラーになります。

21. Semverインスタンスにバージョンを保持する(グリーン)

バージョンを保持する実装を書いていきましょう。core.py の __init__ 関数を編集します。

 class Semver:
     def __init__(self, major, minor, patch):
-        pass
+        self.major = major
+        self.minor = minor
+        self.patch = patch

引数で受け取った値を、selfのメンバーに格納するだけです。よりスマートな書き方としてdataclassを使う手法があります。もし興味があれば調べてみてください。

TDDをやっていると、テストコードに対して素朴な実装を何度も書くことになります。この積み重ねは確実な歩みとなり、遠いところまでたどり着くことができます。

テストを実行してみましょう。

$ make test

グリーンになっていますね。TODOリストも更新しましょう。

 * ~~Semverインスタンスが作成できる~~
 * ~~Semverインスタンス生成時にバージョン指定ができる~~
-* Semverインスタンスはバージョンを保持する
+* ~~Semverインスタンスはバージョンを保持する~~
 * 比較できる
   * Semver(3, 0, 0) == Semver(3, 0, 0)
   * Semver(1, 0, 0) < Semver(3, 0, 0)

22. Semverを文字列で表示できる(レッド)

これまで Semverインスタンスを作ってきましたが、Semverの文字列表現を実装していきましょう。 TODOリストに書かれた最後の項目です。

TODOリストの項目は上からやる必要がありませんし、途中で思いついたTODOは増やしましょう。

まずは、 test_core.py を編集してテストケースを作ります。

         assert semver.major == 1
         assert semver.minor == 2
         assert semver.patch == 3
+
+    def test_Semverを文字列で表示できる(self):
+        assert str(Semver(1, 2, 3)) == "1.2.3"

Python では str 関数によってインスタンスの文字列表現を得ることができます。

ここで気づいてほしいのは、テストを読むだけで使い方がわかるという点です。書いたテストは、動作をそのまま表しているため、チーム開発で意思疎通に役立ちます。また、私がOSSなどのコードを読むときはまずテストコードを読み、関数の使われ方をチェックしています。

そしてテストコードは実行され続けるものですので、誰かが関数の挙動を変えてしまったときにすぐに気づけるのです。

少々脱線しましたが、これでテストコードを書けたのでテストが失敗することを確認しましょう。

$ make test

AssertionError が発生するはずです。

23. Semverを文字列で表示できる(グリーン)

文字列表現を core.py に実装していきましょう。Pythonの場合は、 __str__ というマジックメソッドを変更することで文字列表現を実装できます。

テストを成功させる最小限の実装は、こうなります。

         self.major = major
         self.minor = minor
         self.patch = patch
+
+    def __str__(self):
+        return "1.2.3"

この実装では、汎用性がないことは明らかです。しかしテストは成功します。

$ make test

24. Semverを文字列で表示できる(リファクタリング)

1.2.3バージョンではテストが成功しましたが、ほかの値で成功しないことは明らかです。

TDDでは、グリーンのときにリファクタリングすることができます。リファクタリングがうまくいけば、グリーンのままなので自信を持って挙動が変わっていないことが言えます(現在のテストケースの範囲で)。

もし、レッドのときにリファクタリングすると挙動が変わったのか、変わったとしてどう変わったのか判断が難しくなります。

さっそくリファクタリングしていきましょう。Semverインスタンスがもっているバージョンをそれぞれ結合すれば、文字列表現が実現できそうです。

         self.patch = patch
 
     def __str__(self):
-        return "1.2.3"
+        return f"{self.major}.{self.minor}.{self.patch}"

Python の f文字列 を使いました。

テストが成功するか見てみましょう。

$ make test

成功します。これまで書いてきたテストを守った状態で、実装が汎用的なものになりました。

25. バージョンに負の値を受け付けない(レッド)

バージョンの渡し方、バージョンの文字列表現で、すでに不吉な匂いを感じている人がいるかもしれません。

そうです。バージョンは非負の整数に限る必要があります
現在の実装では -1.-1.-2 というバージョンもあり得そうです。

バージョンに負の値を受け付けないように実装することにしましょう。

TODOリストに追加します。

 * ~~Semverインスタンスが作成できる~~
 * ~~Semverインスタンス生成時にバージョン指定ができる~~
 * ~~Semverインスタンスはバージョンを保持する~~
+* Semverはバージョンに負の値を受け付けない
 * 比較できる
   * Semver(3, 0, 0) == Semver(3, 0, 0)
   * Semver(1, 0, 0) < Semver(3, 0, 0)

次に、レッドの状態を作るためにテストケースを追加します。「バージョンに負の値を受け付けないように」というテストケースを書きます。

     def test_Semverを文字列で表示できる(self):
         assert str(Semver(1, 2, 3)) == "1.2.3"
+
+    def test_バージョンに負の値を受け付けない(self):
+        with pytest.raises(ValueError):
+            Semver(-1, -2, -3)

このケースでは、 負の値をインスタンス生成時に渡すと例外を送出するという挙動を期待しています。

今回は例外送出する挙動にしましたが、負の値を入れた場合、バージョンを0として扱いたい場合もあるでしょう。その都度、ご自身や開発に携わるメンバーと相談して決めてください。

Python を書く方は、 with が使えるようになるとコードをシンプルに保ちやすくなります。公式ドキュメントを参照してみてください。

Python 組み込みの例外はいくつかありますが、今回は不適切な値を受け取った場合に使われる ValueError を採用します。
ValueErrorについては公式ドキュメントを参照してください。

説明が長くなりました。さて、テストを実行してテストが失敗することを確認しましょう。

$ make test

Failed: DID NOT RAISE <class 'ValueError'> となるはずです。

26. バージョンに負の値を受け付けない(グリーン)

負の値がインスタンス生成で受け取ったとき ValueError を返すように core.py を書き換えます。

前回みたいにバージョンの値を決め打ちして -1, -2, -3 が渡されたときは ValueError になるようにしてもいいですが、慣れてきたのでスピードアップしましょう。

if で major, minor, patch がそれぞれ 0未満のときに例外にします。

 class Semver:
     def __init__(self, major, minor, patch):
+        if major < 0 or minor < 0 or patch < 0:
+            raise ValueError
+
         self.major = major
         self.minor = minor
         self.patch = patch

テストを成功させるための実装の大きさや考慮の広さことを、歩幅やステップと言います。歩幅は、実装者の熟練度や、実装の難易度によって変わるものです。

焦らず、自分の実力と実装方針の想像しやすさで歩幅を狭くしたり、広くしたり調整していってください。

さて、テストを実行して成功を確認しましょう。

$ make test

歩幅を広くしてみましたが、無事成功しました。

TODOリストを更新しましょう。

 * ~~Semverインスタンスが作成できる~~
 * ~~Semverインスタンス生成時にバージョン指定ができる~~
 * ~~Semverインスタンスはバージョンを保持する~~
-* Semverはバージョンに負の値を受け付けない
+* ~~Semverはバージョンに負の値を受け付けない~~
 * 比較できる
   * Semver(3, 0, 0) == Semver(3, 0, 0)
   * Semver(1, 0, 0) < Semver(3, 0, 0)

27. TODOリストを見返してみる

ここまで、ひとつひとつ手順を追ってTDDを行ってみました。TODOリストを見返してみましょう。最初のTODOリストとどう変わったでしょうか?

  • Semverインスタンスが作成できる
  • Semverインスタンス生成時にバージョン指定ができる
  • Semverインスタンスはバージョンを保持する
  • Semverはバージョンに負の値を受け付けない
  • 比較できる
    • Semver(3, 0, 0) == Semver(3, 0, 0)
    • Semver(1, 0, 0) < Semver(3, 0, 0)
    • Semver(0, 0, 1) < Semver(0, 0, 2)
  • バージョンアップできる
  • Semverを文字列で表示できる

27.1. TDDでの発見: 挙動が細かく定義できる

最初に書いたTODOよりも項目が増えています。最初に用意したものでは、考慮が不足していたため、付け加えたからですね。つまり、TDDを通じてSemverの挙動が細かく定義できたということです!

(とはいえ、仕様はある程度開発前に決めておくことを推奨します)

最初は曖昧だった挙動が最小限の実装を重ねる中で、少しずつ明らかになります。「どう作ろうか?」と悩みが徐々に「これはこうでしょ!」という自信に変わっていくわけです。

27.2. TDDでの発見: テストコードが残るため、動作が保証される

打消し線が引かれているものは、あなたがこれまで実装したものたちです。実装だけでなく、テストコードも一緒に書きました。

書いたテストコードは、実行できるので実装の動作が保証されています。当然ですが、望まれた動作と実装の動作が一致している保証はないので、過信は禁物です。

ただ「テストコード」が挙動調査の手がかりになるため、コードがどのように動くのか調べやすくなります。特に、コードを書いた本人以外(あるいは数か月後の自分)にとってありがたいことです。

このメリットはとても大きいです。

「自分の環境では動いていた。再現方法は〇〇だが、あなたの環境で再現するかわからない」のようなかっこ悪いことがなくなります。

「テストでは動いている。再現方法はテストコードを読めばわかる」と自信をもって主張することができます。

27.3. TDDでの発見: TODOリストのおかげで進捗が積める

TODOリストに書いておくことで、やるべきことは明確になりましたね。どこまで終わらせたかという振り返りも容易です。

日を跨いだり、週をはさんだりしても、TODOリストを見ればやるべきことが思い出せますね。

TDDは、TODOリストとレッド状態の存在のおかげで、タスク管理の面でも活躍してくれます。作業を中断したあと再開するとき、ダラダラと過ごして時間を浪費した経験があると思います。

次のシンプルなルール:

  1. 作業中断する前にレッド状態にしておく
  2. 再開するときにはグリーン状態にする

を守るだけで、作業再開がとてもスムーズになります。これはツァイガルニク効果を使ったテクニックです。

達成できなかった事柄や中断している事柄のほうを、達成できた事柄よりもよく覚えているという現象
wikipedia より引用

私は個人開発や仕事でのチーム開発などで、このルールを適用して活用しています。

28. 手つかずの部分

TODOリストのうち「比較できる」と「バージョンアップできる」は未着手でした。

この点は、あなたが一人でTDDを活用して着手してみてください。

ここまで、やってこれているなら大丈夫です。TDDは一人でも実践できる、開発力アップのためのテクニックです。気軽にトライして経験値を積んでみてください。

29. まとめ

ざっくりまとめます。

TDDは

  • テスト手法ではなく、開発手法
  • テストファースト、レッド、グリーン、リファクタリングのサイクルを何度も繰り返す
  • テストをパスために、小さく実装する
  • 実装中にTODOリストを増やして一つずつこなしていく

というものでした。

私もTDDを取り入れて日々開発しています。TDDは魅力がたくさんあるので、ぜひ多くの人にTDDを実践してもらいたいと思っています。

私は、TDDで次のようなことを実感しています。

  • 仕様がテストとして記述されるため、バグが入りづらい
  • 実装前に使用例としてテストコードを書くので、使いやすい関数やモジュールが作れる
  • 小さく実装して動作確認するので短い時間で動くことが確認できる
  • TODOリストを使うので、進捗がわかりやすい上、実感しやすい

ぜひ、あなたもこの記事を使ってTDD実践者になってください!

30. 最後に、自信を持ってコードを書きたいあなたへ

あなたが自信を持ってコードを書けるように、この記事を書きました。手法を知るだけでは、自信は持てないので日々の実装に取り入れてみてください。

もしこの記事が、あなたにとって

  • 『参考になった』
  • 『周りのエンジニアも知りたい人がいそうだ!』

というものだったら

ぜひTwitterやLINEなどでシェアをしていただけないでしょうか?

シェアしていただくことで「自信を持ってコードを書くエンジニアが増える」ので、エンジニア界隈にとってプラスになります。

今後、私は自信を持ってコードを書くエンジニアを増やすべく活動していくので応援よろしくお願いします。

ここまでお付き合いいただきまして、お疲れ様でした!

Discussion