nikkie-ftnextの日記

イベントレポートや読書メモを発信

読書ログ | 『Clean Craftsmanship』 第2章より、Uncle Bobのテスト駆動開発は驚くほど小さなステップの積み重ね!

はじめに

小さいは正義。nikkieです。

『Clean Craftsmanship』の読書ログです。
Uncle Bobのテスト駆動開発、書籍とビデオで体感して非常に衝撃を受けました。

目次

『Clean Craftsmanship』第2章

https://www.tumblr.com/asciidwango/693992928727760896/clean-craftsmanship

第2章は「テスト駆動開発」。
テスト駆動開発とはどんなものかというのが示された後1、複数の例でテスト駆動開発を体験します。
書籍だけでなくビデオも用意されており、Uncle Bobのテスト駆動開発のリズム2も体験できます。

リズムを体験して衝撃的だったことが1つ。

Uncle Bobに見せてもらったTDDは、1つのテストを通す上で、RedとGreenを何回も往復します!

この本を読むまで、私はTDDというのは、1つのテストを全部書き切って、実行したら落ちるので実装を全部書き切るものとして実践していました。
1つのテストがRed -> Greenとなるまでに1往復しかしません。

ところがUncle Bobは1つのテストを通すまでに、テストも実装も少しずつ書き進めていったんですね。
その結果、上で述べたように1つのテストと実装を終えるまでに何回もテストと実装を往復します!
この様子は後述します。

Uncle Bobによるテスト駆動開発の説明

感想として、非常にstrictです(strict TDD!)

以下の3つの法則により説明されます

  1. テストを書くまでは本番コードを書いてはならない。本番コードがないためにテストは失敗する。(Kindle版 pp.52-53)

  2. 失敗するテストやコンパイルできないテストを必要以上に書いてはならない。失敗を解決するには本番コードを書く。(Kindle版 p.53)

  3. 失敗しているテストを解決する本番コードを必要以上に書いてはならない。テストがパスしたら、追加のテストを書く。(Kindle版 p.53)

これらにより、小さなステップを積み重ねることになります(例で示します)。
テストが落ちる🟥まで書き、それを通す実装🟩を書き、またテストを落ちる🟥まで書くという繰り返しです。
テストが落ちるには実行してfailするほか、コンパイルエラーも含みます。

なお、4つ目の法則もあり、それはリファクタリングです。
Red -> Green -> Refactorのサイクルが非常に小さい単位で繰り返されるので、リファクタリングが本当に絶えず行われる形になります。

例:整数のスタック

書籍の例を参照して、驚くほど小さなステップの積み重ねのTDDを見ていきましょう。
なお、Python(unittest)で写経したコードがこちらにあります:

何もしないテスト

始まりは何もしないテストです。
通ることを確認し、環境構築できているという判断をします

from unittest import TestCase


class StackTestCase(TestCase):
    def test_nothing(self):
        ...

スタックを作れるテスト

スタックが作れることを検証していきます。

テストでMyStackクラスをインスタンス化しようとします。

class StackTestCase(TestCase):
    def test_can_create_stack(self):
        stack = MyStack()

MyStackクラスはまだありません(し、テストの中でimportもされていません)からテストは落ちます。
Pythonにはコンパイルエラーはないので、テストを実行してNameErrorとなりますね。

strictなテスト駆動開発の第1の法則を満たすので、第2の法則により実装してこれを解決します。

class MyStack:
    ...
+from stack import MyStack
+
+
class StackTestCase(TestCase):
    def test_can_create_stack(self):
        stack = MyStack()

テスト中でインスタンス化できるようになりました!
この段階でRed -> Greenまで進んだことになります(テストも実装もどちらも途上ですが)

第3の法則により、次はテストコードの追加となります。
テストコードに着手する前にRefactorでMyStackクラスをStackクラスにrenameしました。

スタックを作れるテストで、インスタンス化の後の検証を実装します。
インスタンス化したら空のスタックである」ことを検証します。
スタックのis_emptyメソッドがTrueを返すことを確認しましょう。

class StackTestCase(TestCase):
    def test_can_create_stack(self):
        stack = Stack()
        self.assertTrue(stack.is_empty())

ここでis_emptyメソッドがないので、テストは落ちます(AttributeError)。

実装を書くターンです。
最小限の実装を書くと、Falseを返すメソッドとなります。

class Stack:
    def is_empty(self) -> bool:
        return False

AttributeErrorは送出されなくなりますが、期待値と一致しません。
そこで実装を修正します。

class Stack:
    def is_empty(self) -> bool:
        return True

これでスタックを作れるテストケースは通りました!

スタックにpushできるテスト

続いてはスタックに要素をpushできるようにしていきます。
インスタンス化した空のスタックに要素を1つ入れたら、空ではないですよね。
このテストを書いていきます。

まずスタックに要素を入れるメソッドがないので、それをテストで使ってテストを落とし(Red)

class StackTestCase(TestCase):
    def test_can_push(self):
        stack = Stack()
        stack.push(0)

何もしないpushメソッドを実装します(Green)。

class Stack:
    def push(self, element: int) -> None:
        ...

要素をpushした後は、空ではないことをテストコードで表します。

class StackTestCase(TestCase):
    def test_can_push(self):
        stack = Stack()
        stack.push(0)
+        self.assertFalse(stack.is_empty())

これを通すように実装します(以降は書籍をどうぞ!)

どうですか? strictな3原則に沿って、1つのテストと実装を完了するまでに、RedとGreenを何回も往復しますよね。
コンパイルエラー(PythonだとNameErrorやAttributeError)もRedなので、それだけを解決する短い実装でGreenにして、Refactor、そして次のサイクルへと回していっています

終わりに

『Clean Craftsmanship』第2章で知って驚いた、テスト駆動開発を取り上げました。

  • strictなRed -> Green -> Refactorのサイクル
    • テストと実装をどちらも少しずつ書いていく
  • コンパイルエラーもRedとし、コンパイルエラーを通す実装を最小限書く(これを何度も繰り返す)

テスト駆動開発という文字は同じでしたが、実践していたものとは全くの別物でした。
このやり方を参考に、テストを長時間どーん!と書き、それを通す実装をこれまたどーん!と書くというかつてのやり方を、小さな往復を繰り返すように練習しています。
一気に書き上げられないもどかしさはありますが、着実に進んでいる安心感(間違えようのない小ささ)もまた感じています。
引き続き練習するぞ

P.S. XP祭り 2023の「小さなテクニカルプラクティスのワークショップ」準備中です

最近ミリアニがお気に入りなのですが、ワークショップ準備の状況もミリアニの某エピソードのような感じです(そこまで再現しなくても😅)
やばい〜


  1. Cleanシリーズで見かける複式簿記のたとえも登場します
  2. 書籍の写経では学べないリズムもサポートされているのはとてもありがたいです。ref:読書ログ | gihyo.jp の「和田卓人の“テスト駆動開発”講座」から講義編の記事を読みました🦁 - nikkie-ftnextの日記