🕌

【技術書アウトプット】(書籍)テスト駆動開発のハンズオンをPythonで実装してみた

に公開

はじめに

こんにちは!もんたです。

今回はKent Beck(著)の『テスト駆動開発』という技術書を読み、テスト駆動開発の基礎を学んだので、アウトプットとして記事にしました。

テスト駆動開発

本書では以下の内容を学ぶことができます!

  1. テスト駆動開発の基礎
  2. Pythonを使ったテスト駆動開発の方法

「テスト駆動開発って何〜」っていう初心者向けの内容となっております。
自分もゴリゴリの初心者エンジニアなので、一緒に学んでいけたらなと思います。

前提

まずは前提を共有します。

この記事は「テスト駆動開発」という本をベースとしています。

テスト駆動開発は第1部〜第3部で構成されています。
特に第1部はJavaを用いてハンズオン形式でテスト駆動開発が学べる内容となっています。

この記事は、第1部のハンズオン形式の内容をもんたがPythonで実装したのでそれを共有しながらテスト駆動の基礎を学んでいこうという内容になっています。


ちなみに、実際のコードは以下のリポジトリで管理しています。

https://github.com/Hiroto0706/test-driven-development-output

環境

今回は以下の環境で開発を行いました。
テストフレームワークはpytestを使っています。

項目 内容
Python バージョン 3.13
テストフレームワーク pytest(バージョン指定なし)

テスト駆動開発とは

テスト駆動開発とは、以下の流れで開発を進める開発手法になります。

  1. まず、テストケースを書く
  2. テストを走らせ、テストが通らないこと(レッド)を確認する
  3. テストケースが通るように、ハードコードでもいいのでコードを書く
  4. テストが通ること(グリーン)を確認する
  5. さらに修正を加える
  6. 最後にリファクタリングをする

大事なのは、まずテストケースを書くということです。

テストケースを書くことで、「このコードはこういう要件を満たす内容にしたい」ということが明確になります。

テスト駆動開発とは

🐶: 「先生、テスト駆動開発って何なんですか?」

👴: 「テスト駆動開発、略してTDDとはの、まず最初にテストコードを書いて、そのテストを通すために必要最低限の実装を行う開発手法なんじゃ。コードを書く前に『こんな動作をしてほしい』というテストを書くんじゃよ。」

🐶: 「えぇ、コードを書く前にテストを書くんですか?」

👴: 「その通りなんじゃ。最初は実装が無いから、当然テストは失敗してしまう。これを『赤(Red)』と呼ぶんじゃよ。」

🐶: 「なるほど、赤になったらどうすればいいんですか?」

👴: 「次は、テストを通すために最小限の実装をするんじゃ。これでテストが成功して『緑(Green)』になる。初めは完璧にする必要は無く、動くことが大事なんじゃよ。」

🐶: 「そして、最後には何かするんですか?」

👴: 「そうじゃ。テストが通った後は、リファクタリングという工程でコードを整理し、読みやすく保守しやすい形に改善するんじゃ。これにより、後からの変更も安心してできるようになるんじゃよ。」

🐶: 「つまり、テストを書いてから実装し、リファクタリングするんですね!」

👴: 「その通りじゃ。『Red→Green→Refactor』のサイクルを繰り返すことで、安心してコードを書き進められるようになるんじゃ。テストがあるおかげで、どこで間違いがあったかもすぐにわかるんじゃよ。」

🐶: 「実際にやってみると、どう実装すればいいかがはっきり見えてくるんですね!」

👴: 「そうじゃ、🐶。テストを書くことで、期待される動作が明確になるんじゃ。最初は戸惑うかもしれんが、実践すればその良さが実感できるはずじゃよ。」

🐶: 「ありがとうございます、先生!これでテスト駆動開発の基本がよくわかりました!」

👴: 「よく理解できたな、🐶。これからも実際に手を動かして、TDDの素晴らしさを体験しておくれじゃよ!」

まぁ、要するに『テストケースを書く→ハードコードでもいいので動かす→リファクタリング』の流れを繰り返すのがテスト駆動開発っていう認識でOKです。

テスト駆動で書くと何がいいの?

テスト駆動開発では、「動作する綺麗なコード」が最も価値あるコードだとみなされています。
動作する綺麗なコードを目指すのがテスト駆動開発の目的であり、最終的なゴールなのです。

その動作する綺麗なコードを目指すための手段というのがテスト駆動開発というわけです。
そして、テスト駆動で開発するとフィードバックがすぐに得ることができるので、「どこを修正すべきか」がすぐにわかります。

そして、最後にリファクタリングをすることで、テスト駆動開発の最終ゴールである『動作する綺麗なコード』を実現することができるのです。

テスト駆動開発は何がいいの?

🐶: 「先生、テスト駆動開発のメリットって何なんですか?テスト駆動で書くと、どんな良いことがあるんでしょうか?」

👴: 「おお、良い質問じゃな、🐶。テスト駆動開発の最大のメリットは、何と言っても『すぐにフィードバックが得られる』ことなんじゃよ。テストを書いてから実装するから、コードの動作を即座に確認できるんじゃ。」

🐶: 「なるほど、すぐに問題点が分かるんですね!」

👴: 「その通りじゃ。さらに、テストがあれば後からコードを変更しても安心じゃ。リファクタリングの際に『これで大丈夫じゃろうか』と不安になることがなくなるんじゃよ。設計も明確になり、何を実装すべきか迷うことが少なくなるんじゃ。」

🐶: 「つまり、テストがあることで設計が固まって、バグも減るんですね!」

👴: 「そうじゃ、🐶。テストは実質的なドキュメントの役割も果たす。どんな挙動を期待しているかが明文化されるから、後でコードを読むときにもすぐ理解できるんじゃよ。これにより、品質の高いコードが書け、メンテナンスもしやすくなるんじゃ。」

🐶: 「わかりました、先生!テスト駆動開発で安心してリファクタリングできるし、設計も明確になるんですね!」

👴: 「その通りじゃ、🐶。これらのメリットが、テスト駆動開発の大きな魅力なんじゃよ。ぜひ実践してみるといいじゃろう!」

また、単純に動作する綺麗なコードを書きやすいこと以外にも、「安心してコードを書くことができる」っていうのがメリットとしてあると思います。

「ここ変更しても大丈夫なのかな」とか「ここ変更して他の箇所に影響出ないかな」と不安になりながらコードを書くことはメンタルに悪影響を及ぼします。

テスト駆動開発を用いることで、コードに変更を加えた結果、「どこに影響があったか」がテストコードを通してわかるので、どこが悪いかがすぐにわかり、安心してコードを書くことができるようになるのです。

いざ、テスト駆動へ

それではここから、テスト駆動開発で私が学んだことをまとめていこうかと思います。

具体的なコードも一緒にお見せしますので、テスト駆動開発のイメージがちょっとでもついたらいいなと思います。

このチャプターで満たしたい要件

念の為、今回のテストコードで満たしたい要件をまとめておきます。

要件 説明
乗算と比較 お金の金額を指定の倍率で掛け算し、計算結果が正しいかどうかを比較できる。 5ドル × 2 = 10ドル、5ドル × 3 = 15ドル
通貨情報の取得 お金のオブジェクトは、自分がどの通貨(USDやCHF)かを示す情報を持っている。 5ドル → 通貨:"USD"、5フラン → 通貨:"CHF"
同じ通貨の加算 同じ種類のお金同士の場合、足し合わせて正しい合計金額を求めることができる。 5ドル + 5ドル = 10ドル
加算結果の管理 お金を足した結果は、その計算結果として「どの金額を足したか」が保持され、さらに別のお金と合算できる。 (5ドル + 5ドル) + 5ドル = 15ドル
異なる通貨の換算 異なる通貨同士を足す場合、あらかじめ決めた為替レートを使って、同じ通貨に換算して合計金額を求める。 5ドル + 10フラン(レート:2 → 10フラン = 5ドル換算) = 10ドル
複合演算 複数の演算(加算や乗算)を連続して行い、最終的な合計金額を求めることができる。 (5ドル + 10フラン) × 2 = 20ドル

①ハードコードでもいいからテストが通るようにコードを書く

テスト駆動は何よりも小さいステップで進めていくことが大切なので、ハードコードでもいいからテストが通るコードを書くっていうのはめちゃくちゃいい進め方なんです。

仮に、以下のように満たしたい要件のテストコードがあったとします。

def test_multiplaication():
    five = Dollar(5)
    ten = five.times(2)
    assert ten.amount == 10

単純に5ドルに2をかけて10になるかをテストするという内容です。

『ハードコードでもいいから通るコードを書け』とは以下のようにamountではなく、直接5を代入してテストを無理やり通るようにするということです。

class Dollar():
    def __init__(self, amount: int):
        self.amount = 5 # ここ5にしたらテスト通るので、5にしちゃう

    def times(self, multiplier: int):
        self.amount *= 2

かなり気持ち悪いコードかもしれないですが、テスト駆動開発においてはこれは大歓迎の方法です。
なぜなら、テスト駆動開発は小さいステップで進めていくことが重要だからです。

後々になって、以下のように2倍だけでなく3倍も足す必要が出てきたら改めてその時、ハードコードではなく、変数を代入するようにすればいいのです。

def test_multiplaication():
    five = Dollar(5)
    ten = five.times(2)
    assert ten.amount == 10

    five = Dollar(5)
    fifteen = five.times(3)
    assert fifteen.amount = 15 # 5を直接代入するとエラーになるので、この時初めてDollarクラスを修正すればいい
class Dollar():
    def __init__(self, amount: int):
        self.amount = amount

    def times(self, multiplier: int):
        self.amount *= 2

まぁ、とりあえずテスト駆動においては「初めのうちは汚いコードは大歓迎だよ」ってことです。
それよりも、小さいステップで始めるってことを覚えておいてください…!!

②TODOリストを作成する

TODOリストを作成するってのはそのままの意味です。
以下のように、TODOコメントを残すでもいいし、別の場所にTODOリストを作成するなどして、「テストケースを満たすためになにをすればいいか」を明確にしておきましょう。

class Dollar:
    def __init__(self, amount: int):
        # TODO: amountをプライベートにする
        self.amount = amount

    def times(self, multiplier: int):
        return Dollar(self.amount * multiplier)

自分は以下の拡張機能を使って自分はTODOリストを管理していました。

https://marketplace.visualstudio.com/items?itemName=wayou.vscode-todo-highlight

③副作用のないコードを意識する

副作用とは、「関数名から思いがけない変更がされている」アンチパターンのことです。

例えば以下のようなコードのことを指します。

class Dollar():
    def __init__(self, amount: int):
        self.amount = amount

    def times(self, multiplier: int):
        self.amount *= 2

def test_multiplication():
    five = Dollar(5)
    five.times(2)
    assert five.amount == 10

    five.times(3)
    assert five.amount == 15

一見すると問題なさそうに見えるコードですが、assert five.amount == 15は失敗します。

なぜなら、five.times(2)の部分で、直接fiveオブジェクトのamountを書き換えており、five.amount(3)をするときにはfiveのamountは10になっているからです。

そのため、five.times(3)は30となり、15と一致せず、エラーになります。

関数名ではtimes(2)なので、fiveを2倍するのかなという予想がつきますが、実際はfiveオブジェクトのamountが直接書き換えられてしまっています。

このようなコードは後続の処理に悪影響を与えます。

なので、こういった副作用をなくすために以下のようにオブジェクトそのものを返すようにしましょう。

class Dollar():
    def __init__(self, amount: int):
        self.amount = amount

    def times(self, multiplier: int):
        return Dollar(self.amount * multiplier) # オブジェクトそのものを返すので、副作用がない

def test_multiplication():
    five = Dollar(5)
    ten = five.times(2)
    assert ten.amount == Dollar(10).amount

    fifteen = five.times(3)
    assert fifteen.amount == Dollar(15).amount

このようなリファクタリング周りの知識は良いコード/悪いコードで学ぶ設計入門がめちゃくちゃ勉強になるのでおすすめです。

④仮実装・三角測量・明白な実装

仮実装・三角測量・明白な実装とは、TDDでとりあえず意識しておくと良いっていう三原則です。

仮実装・三角測量・明白な実装

🐶: 「先生、仮実装、三角測量、そして明白な実装って、具体的にどういう流れなんでしょうか?コード例も交えて教えてください!」

👴: 「うむ、わかったぞ、🐶。まずは『仮実装』についてじゃ。たとえば、足し算の関数 sum(a, b) を作るとするじゃろう。最初のテストケースは『3 + 4 = 7』と確認することじゃ。そこで、実装の最初の段階では、何も考えずにただ return 7 と書いて、テストが通るようにするんじゃ。つまり、要件を満たすかどうかだけを見るための、とりあえずの実装じゃよ。」

🐶: 「つまり、最初は『とにかくテストをグリーンにするために、固定の値を返すだけ』ってことですね!」

👴: 「その通りじゃ。次に『三角測量』じゃ。仮実装のままでは、たまたま3と4の組み合わせだけ通るだけになる。そこで、他のテストケース、たとえば『2 + 3 = 5』や『4 + 5 = 9』も追加してみるんじゃ。これにより、固定値では通らないテストが現れ、実装の一般性を求められるようになるんじゃよ。」

🐶: 「追加のテストが増えることで、『ただの固定値返し』では足りないことが分かるんですね!」

👴: 「そうじゃ。そして、テストケースが十分に揃って、どう実装すれば全てのテストが通るかが明確になったら、『明白な実装』に進むんじゃ。ここでは、足し算という明確な要件に基づいて、正しく return a + b と書くんじゃよ。これが、要件がはっきりしている場合に直接正しいコードを書く、明白な実装という考え方なんじゃ。」

🐶: 「まとめると、まずは『3+4=7』という一つのテストをグリーンにするために固定値を返す仮実装、次に複数のテストケースを追加して実装の一般性を確認する三角測量、そして最終的に『a+b』と正しいロジックで書く明白な実装、という流れですね!」

👴: 「その通りじゃ、🐶。具体的な流れは以下のようになるんじゃ:」

  • 仮実装:

    def sum(a, b):
        return 7  # とりあえずテストを通すための固定値
    

    この段階では、『3 + 4 = 7』というテストケースだけがグリーンになるが、他は通らん。

  • 三角測量:
    テストケースを追加して、例えば次のようにするんじゃ。

    assert sum(3, 4) == 7
    assert sum(2, 3) == 5
    assert sum(4, 5) == 9
    

    これで固定値では通らんことがわかり、実装を一般化せざるを得なくなるんじゃ。

  • 明白な実装:
    要件が明確になったので、正しく計算するコードを書くんじゃ。

    def sum(a, b):
        return a + b  # 正しい実装
    

    これで、全てのテストケースがグリーンになるはずじゃ。

🐶: 「具体例もあって、流れがよく理解できました!まずは簡単にテストを通して、次にテストを増やして要求を明確にし、最終的に正しい実装に落とし込むんですね!」

👴: 「そうじゃ、🐶。この『仮実装 → 三角測量 → 明白な実装』のサイクルを繰り返すことで、安心してコードを書き進められるようになるんじゃ。さあ、これを実際に試してみると良いじゃろう!」

🐶: 「ありがとうございます、先生!これでTDDの流れがバッチリわかりました!」

仮実装

仮実装とは、sum(4,5)という関数をテストしたい場合、return 9とハードコードしてしまい、とりあえずテストが通るように開発を進めることを指します。

def sum(a, b):
    return 9 # とりあえずテストが通る値を入れる

三角測量

三角測量とは、sum(a,b)のテストが通ることを4+5=9というパターンだけでなく、3+7=10というパターンでも通るように実装することを指します。

assert sum(4, 5) == 9
assert sum(3, 7) == 10

明白な実装

明白な実装はめっちゃシンプルで、実装すべき内容が明らかな場合は「仮実装で値のハードコードをせずに、直接書いてもいいよ」っという意味です。

例えば、sum(a,b)を実装したいとします。
単純にa + bの値を返せばいいっていうのは明らかなので、return a + bと書くようにしちゃう。

それだけです。


TDDではこの「仮実装・三角測量・明白な実装」を意識して開発を進めるのが良いよということが書かれてありました。

『とにかく動くコードを書き、それをリファクタし、動くコードを書く…』これがテスト駆動開発の基礎中の基礎です。

最後に

最後までお読みいただきありがとうございました!🐶

テスト駆動開発は始め、かなり難しそうな印象を持つかと思うのですが、やってみるとグリーンバーを見るのがどんどん楽しくなっていきました…!

また、リファクタリングも作業プロセスに組み込まれているので、リファクタリング能力というレベルの高いエンジニアになるには必須の能力も鍛えられるのでかなり良い開発手法だなと感じました。

(初めの段階では)時間がかかるっていうのはネックですが、将来的な時間は100%テスト駆動開発の方が短いので、長期的な目線を持ってテスト駆動開発に取り組もうと思いました!!

ぜひみなさんも楽しいテスト駆動開発ライフを〜!!

https://store.line.me/emojishop/author/5353830/ja

Discussion