マルチタスクやっているエンジニアの皆さん、本当に集中してコードを書く時間はどうしても限られてきませんか?
テスト駆動開発を実践すると、一瞬でフロー状態に入れるのでおすすめです。
※フロー状態とは
時間の経過を忘れるくらい作業に没頭している状態
参考:Wikipedia
テスト駆動開発(TDD)とは
テスト駆動開発とは Test Driven Development を日本語に訳した言葉です。
略してTDDと呼ぶこともあります。
TDDのゴールについて、「テスト駆動開発」(著:Kent Beck、訳:和田卓人)という書籍では下記のように述べられています。
「動作するきれいなコード」。Ron Jeffriesのこの簡潔な言葉が、テスト駆動開発(TDD)のゴールだ。動作するきれいなコードはあらゆる意味で価値がある。
TDDの手順
では「動作するきれいなコード」をどのように実現するのか?
手順を図示すると下記のようになります。
文字に起こすならば、
- 次の目標を考え、TODOリストに起こす
- TODOリストから1つ選び、テストコードを書く
- テストを実行して、失敗させる(RED)
- 目的のコードを書く
- 2.で書いたテストコードを成功させる(GREEN)
- テストが通る状態を保ったまま、リファクタリングする(REFACTORING)
- 1~6を繰り返す
となります。
私の実践例
⚠️以降のコードはあくまでテスト駆動開発を説明するためのものであり、そのまま貼り付けても動作しませんのでご了承ください⚠️
最初の一歩
テスト駆動開発の肝はもちろん「RED / GREEN / REFACTORING」のリズムを守ることですが、個人的にはTODOリストを作成するという作業がかなり重要だと思っています。
TODOリストが作成できるということは、すなわち必要十分な設計ができているということだからです。
例として、AzureのBlob Storageにアップロードされている動画・音声ファイルを1つの音声ファイルにまとめる、という処理をPythonで開発することを考えてみましょう。
まず私は下記のTODOリストを作成しました。
- [ ] BLOBにアップロードされたファイルをコンテナにダウンロードする
- [ ] 動画ファイルであれば音声ファイルに変換する
- [ ] 複数ファイルがあれば結合する
- [ ] 作成した音声ファイルをBLOBにアップロードする
- [ ] mainメソッドを呼び出す
- [ ] mainメソッドは引数file_directoryを受け取る
- [ ] file_directoryの下にあるファイル名を全て取得する
- [ ] ファイル名の数だけdownload_blob_fileを呼び出す
- [ ] handle_filesのテストを書く
- [ ] convert_mp4_to_mp3のテストを書く
この中からまず簡単にテスト・実装できそうなものを選びます。
小さいところから始めたいので、まずは「BLOBにアップロードされたファイルをコンテナにダウンロードする」から取り組んでみることにしました。
最初のテストは、関数が呼び出せるかどうかといったレベルで大丈夫です。
class TestBlobStorage(BaseTestCase):
def test_blobダウンロードする(self):
blob_storage_client = BlobStorageClient()
result = blob_storage_client.download_blob_file()
self.assertTrue(result)
関数がない状態だと、もちろんこのテストコードは落ちます。(=REDの状態)
次の段階では実コードを書きます。download_blob_fileという関数を用意し、Trueを返すように書きます。
class BlobStorageClient:
def download_blob_file(self):
return True
これで一旦テストを通すことができます。(=GREENの状態)
あまりにも単純なコードなので、この段階ではテストコード・実コードともにリファクタリングは必要なさそうです。
ここで、RED / GREEN / REFACTORING を細かく回すため、もう少しTODOリストを詳細化したいと考え始めます。
- [ ] BLOBにアップロードされたファイルをコンテナにダウンロードする
// 追加
- [ ] download_blob_fileは引数としてblob_nameを受け取る
- [ ] download_blob_fileはSDKのget_blob_clientを呼び出す
// 追加ここまで
- [ ] 動画ファイルであれば音声ファイルに変換する
- [ ] 複数ファイルがあれば結合する
- [ ] 作成した音声ファイルをBLOBにアップロードする
- [ ] mainメソッドを呼び出す
- [ ] mainメソッドは引数file_directoryを受け取る
- [ ] file_directoryの下にあるファイル名を全て取得する
- [ ] ファイル名の数だけdownload_blob_fileを呼び出す
- [ ] handle_filesのテストを書く
- [ ] convert_mp4_to_mp3のテストを書く
このように、気づいたことがあればTODOリストは常に更新していくと良いです。
次にやるべきことが明確に見えているため、早く次を実装したくてウズウズしませんか?
個人的に、このウズウズがフロー状態に入るきっかけになると感じています。
全体像
このコードはAzureのPython SDKを使うことになります。
SDKのやっている処理内容までテストする必要はないので、想定しているSDKの処理が呼び出されているか、ということをモックを使ってテストします。
なんやかんやあって、最終的に出来上がったテストコードはこんな感じ。
import os
import unittest
from unittest.mock import MagicMock, patch
from utils.blobstorage import BlobStorageClient
class BaseTestCase(unittest.IsolatedAsyncioTestCase):
@patch("utils.blobstorage.settings")
@patch("utils.blobstorage.BlobServiceClient")
def setUp(self, MockBlobServiceClient, mock_settings):
# 環境変数の設定部分をモック化
mock_settings.AZURE_BLOB_CONNECTION_STRING = "fake_connection_string"
mock_settings.AZURE_BLOB_CONTAINER_FILE_UPLOAD = "fake_container_name"
# SDKの処理をモック化
self.mock_blob_service_client = MockBlobServiceClient.return_value
self.mock_container_client = MagicMock()
# モック化したSDKの処理を呼び出すように設定
self.blob_storage_client = BlobStorageClient()
self.blob_storage_client.blob_service_client = self.mock_blob_service_client
self.blob_storage_client.container_client = self.mock_container_client
class TestBlobStorage(BaseTestCase):
async def test_blob名を指定すると内容が一時ファイルに書き込まれパスが返却される(
self,
):
# 準備
## SDKの処理をモック化
self.mock_container_client.get_blob_client.return_value = MagicMock()
self.mock_container_client.get_blob_client.return_value.download_blob.return_value = (
MagicMock()
)
self.mock_container_client.get_blob_client.return_value.download_blob.return_value.readall.return_value = (
b"test_data"
)
# 実行
result = await self.blob_storage_client.download_blob_file(blob_name="blob.txt")
# 検証
## SDKの処理が呼び出されていることを確認
self.mock_container_client.get_blob_client.assert_called_once_with("blob.txt")
self.mock_container_client.get_blob_client.return_value.download_blob.assert_called_once()
## 結果が文字列であることを確認
self.assertIsInstance(result, str)
## tempfileが作成されていることを確認
self.assertTrue(os.path.exists(result))
## tempfileに書き込まれた内容が正しいことを確認
with open(result, "r") as f:
self.assertEqual(f.read(), "test_data")
# 後処理
os.remove(result)
出来上がった実コードはこんな感じ。
import tempfile
from azure.storage.blob import BlobServiceClient
from core.config import settings
class BlobStorageClient:
def __init__(self):
self.blob_service_client = BlobServiceClient.from_connection_string(
settings.AZURE_BLOB_CONNECTION_STRING
)
self.container_client = self.blob_service_client.get_container_client(
settings.AZURE_BLOB_CONTAINER_FILE_UPLOAD
)
async def download_blob_file(self, blob_name):
blob_client = self.container_client.get_blob_client(blob_name)
# 一時ファイルを作成してデータを読み取る
with tempfile.NamedTemporaryFile(delete=False) as temp_file:
# Blobからデータをダウンロードし、一時ファイルに書き込む
download_stream = blob_client.download_blob()
temp_file.write(download_stream.readall())
return temp_file.name
ここまでたどり着くために、
- 次の目標を考え、TODOリストに起こす
- TODOリストから1つ選び、テストコードを書く
- テストを実行して、失敗させる(RED)
- 目的のコードを書く
- 2.で書いたテストコードを成功させる(GREEN)
- テストが通る状態を保ったまま、リファクタリングする(REFACTORING)
という流れを実際に何度も繰り返しています。
私はスーパーエンジニアではないので、一発で上記のテストコードと実コードを書くことはできません。
しかし、テスト駆動開発の手順を踏んでいけば、最終的にここへ到達することができるのです。
テスト駆動開発の使いどころ
よく使われる表現ですが、テスト駆動開発もまた「銀の弾丸」ではありません。
ただし、テスト駆動開発ができるスキルを持っていれば、
- 複雑なタスク
- 気をつけることがたくさんあるタスク
- 複数の機能がある大きめのタスク
に対しても、小さな一歩を積み重ねながら集中して取り組みやすくなります。
自分のスキルやその日の気分に合わせて、テスト駆動開発を使いこなしてみてはいかがでしょうか。