Share
Embed
TDDのフローの本質と恩恵を引き出すための考え方
TDD(テスト駆動開発)のフローの本質と恩恵を引き出すための考え方(スライド50枚+α)
※社内勉強のために作ったベース版を少し手直しした物
3年位、成長期フェーズの日々の開発(既存改修)の中で、薄く向き合い続け
気付けば形になっていた実践Tipsのようなものをまとめました。
「質とスピードの両立」をある程度実現するベースとなっているものがTDDの考え方なため、
TDDをどう捉え、普段の開発(既存改修のシーン)にどう落とし込んでいるのか、何を意識しているのか+αな内容
SYM
September 06, 2024
Transcript
55 slides
-
目的/背景 • 前提として「TDDやりましょう」と言うためのものではない ◦ 自身がTDDの通りにやってないのでTDDやりましょうなんて口が裂けても言えない笑 2 • その一助になればと思い、自分自身という1具体ケースの紹介 ◦ 自身がTDDをどう捉え、普段の開発(既存改修のシーン)に
どう落とし込んでいるのか、何を意識しているのか+αな内容 ▪ 2~3年位かけて普段の開発の中で薄く向き合い続けているうちに 気付けば形になっていた実践Tipsのような物 • が、TDDの考え方を理解し、実践することができれば 「質とスピードの両立」を実現する土台を築く近道になるはず ◦ 自身がこれをある程度実現するためのベースとなっているのがTDDの考え方のため -
Contents 1. TDDとは ※ Raccoon Tech Connect #4 (3分LT) の内容改訂版 ◦
TDDのフローの説明とその本質(自分なりの捉え方) ◦ 「振る舞い」と「動作」について 2. 補足:TDDのフローがもたらす恩恵と与える機会を考えてみた 3. 実践のために必要なユニットテストに対する考え方 ◦ どんなテストを意識するのが良いか ◦ 最たる例 4. 普段の開発(既存改修)でTDDの恩恵を引き出すための考え方 ◦ 「既存改修」でTDDがやりづらいと感じる理由 ◦ 「既存改修」の修正は大きく2種類に分類できる ▪ それぞれへのアプローチ ▪ TDDがやりづらいへの克服の糸口 ▪ 実践の型(自己流) ▪ 「テストから書く」に拘らなくて良い理由 ◦ テストファーストの本質 5. おまけ:アーキテクチャから見る「振る舞い」「動作」の位置づけ 3 -
TDD(テスト駆動開発)とは 4 t-wadaのブログ【翻訳】テスト駆動開発の定義 https://t-wada.hatenablog.jp/entry/canon-tdd-by-kent-beck (尺の都合等で)本当は 削らない方が良さそうな ものも削っているため、 ご一読いただければ理解 深まるかと思います ※
「リスト ➡ Red ➡ Green ➡ リファクタ」のフローの説明の内容 は 2024/3/8 に投稿された以下の記事をリスペクト しています -
TDD(テスト駆動開発)とは 5 リスト ➡ Red ➡ Green ➡ リファクタ 1.
テストリスト(Todoリスト)を書く 2. テストを1つ書く(Red) 3. テストを成功させる(Green) ◦ ※ 新しいテストの必要性に気づいたらリストに追加 4. 必要に応じてリファクタリングを行う 5. テストリストが空になるまでステップ2に戻って繰り返す -
TDDの要素 -「リスト」 6 • (テスト) リストの位置づけ t-wadaさんの記事では… ◦ 『あるシステムに振る舞いの変更が望まれているとき 新しい振る舞いにおいて期待される動作をリストアップ する』
▪ 振る舞いの分析(変更後の振る舞いが満たすべき様々な動作を 網羅的に考える) • 大きなタスク (ゴール) を多数の小さなタスク (ゴール) に分解 • ゴール(振る舞いの実現)への道筋を定めていく • 1つずつ順番に取り組む (目の前の1つに集中する) 準備 タスク 最終 ゴール タスク タスク タスク タスク タスク タスク -
TDDの要素 -「リスト」: 振る舞い、動作 is 何? 7 (外部)仕様 = 何をするか(要件) •
ユーザや外部システムから見たシステム の全体の動き 振る舞い = (要件を)どう実現するか • 特定の条件や入力に対するシステム の反応、開発者視点での動き • (特定のケースにてシステムが) 〇〇の入力に対して ✕✕を出力する 動作 • システム内部の処理やアクション、 実行されるロジック • 具体イメージ:小さな関数/メソッド 外部仕様 動作1 動作2 動作3 振る舞い 動作1 動作2 動作3 振る舞い 動作1 動作2 動作3 振る舞い システム -
TDDの要素 -「リスト」: 振る舞い、動作 is 何? 8 動作: • 指定された条件に合致する データをCSV形式で返す
振る舞い: 1. 指定された条件に基づいて データを取得する 2. データをフォーマットする 3. ファイルデータに変換する 要望:CSV形式でデータをエクス ポートしたい public class ExportService { public byte[] exportData(ExportRequest request) { List<Data> data = fetchData(request); String formattedData = formatData(data); byte[] fileData = convertFile(formattedData); return fileData; } List<Data> fetchData(ExportRequest req) { // 動作1: 指定された条件に基づいてデータを取得 } String formatData(List<Data> data) { // 動作2: データをフォーマットする(CSV形式) } byte[] convertFile(String formattedData) { // 動作3: ファイルデータ(バイト列)に変換 } } イメージ例 -
TDDの要素 -「Red➡Green」 • Red ➡ Green の位置づけ t-wadaさんの記事では… ◦ テストを「ひとつだけ」書く ➡
テストを成功させる ▪ テストを成功させる過程にリファクタを混ぜ込まない ▪ テストコードを書いている途中に設計判断が始まるが、 それは主にインターフェイスの設計判断 ※ まずは動かし、それから正しくする。このやり方が結局は脳にも優しい 9 • 1つの小さな「動作」の実装(1タスク)のみに集中する ◦ ※「動作」 = 関数(メソッド) ◦ インターフェースの設計判断 ≒ 関数の引数/戻り値の判断 ◦ テストから書く=1タスク (ゴール) の達成条件を明確化 -
TDDの要素 -「Red➡Green」 10 • 各動作に対して、テスト をしながら進めていく • 段階的に振る舞いを組み 上げつつテストする public
class ExportService { public byte[] exportData(ExportRequest request) { List<Data> data = fetchData(request); String formattedData = formatData(data); byte[] fileData = convertData(formattedData); return fileData; } List<Data> fetchData(ExportRequest req) { // 動作1: 指定された条件に基づいてデータを取得 } String formatData(List<Data> data) { // 動作2: データをフォーマット(CSV) } byte[] convertData(String formattedData) { // 動作3: データをバイト配列に変換 } } testConvertData() testFormattedData() testFetchData() testExportData() -
TDDの要素 -「リファクタ」 11 • リファクタの位置づけ t-wada さんの記事では… ◦ 『ここまで来て、ようやく実装の設計判断を行える 』 ▪
『内部実装がどうあるべきかの判断は後から確保できる』 → 内部の設計を(行えるようになったら)行う工程 ※ この段階で必要以上にリファクタリングしてしまわないこと • (材料が揃ってから) 全体を見直し (設計を) 洗練する機会 ◦ 目の前の小さなゴールに集中する = 視野が狭くなりがち → 視野を(全体に)広げ直す = 過集中防止 ◦ 「動作の実装」と 「(内部)設計の洗練」を分け、適度に行う ▪ ※設計の洗練に取り組む際、テストが元の動作を担保する -
リスト ➡ Red ➡ Green ➡ リファクタ TDDのフローの本質 12 最終ゴールへの
道筋を整理/定義 単なる「動作」の 実装に集中 必要な分だけの設計の 洗練のみに集中 • 小さなタスク (動作の実装 / 設計の洗練) に分解し 1つずつ順番に集中して取り組む • 設計のフィードバックサイクルを構築して回す 本質 気付きがあればリスト更新 -
TDDのフローの本質 13 PLAN CHECK DO ACTION PDCA cycle Todoリスト作成/更新 ・振る舞い分析
・動作を列挙 ≒タスク分解/整理 1つのタスクに着手 ・1つのゴール(動作 の実装)のみに集中 (内部)設計を見直す ・設計の洗練/改善 が行えるか考える ・Todoリスト見直し (内部)設計の洗練 ・リファクタリング =内部の質を改善 Feedback 設計の洗練 -
TDDのフローの本質 14 PLAN CHECK DO ACTION PDCA cycle Todoリスト作成/更新 ・振る舞い分析
・動作を列挙 ≒タスク分解/整理 1つのタスクに着手 ・1つのゴール(動作 の実装)のみに集中 (内部)設計を見直す ・設計の洗練/改善 が行えるか考える ・Todoリスト見直し (内部)設計の洗練 ・リファクタリング =内部の質を改善 Feedback 設計の洗練 GOAL テスト -
◦ (計画立て) 1つの大きなゴールを多数の小さなゴールに分解 ◦ (動作の実装) 目の前の小さな1つのゴールに集中 ◦ (設計の洗練) 材料が揃ったら全体を見直し改善に集中 ▪
リファクタ = 内部の質を上げる(テストが元の動作を担保 ) • テスト = ツール (手段) ※テストは目的になりえない ◦ 最終ゴールに向けて自らが敷いていく「レール」というツール ◦ 内部(設計やコード)の質を洗練するための「手段」 TDDのフローの本質まとめ(お堅め版) 15 本質: 小さく分解して1歩ずつ順番に取り組み、 設計の洗練 の (Feedback) サイクル を回すこと ただ、TDDは気軽さ(敷居を下げ、柔軟に取り組むこと)が大事だと思う… -
テストは 「開発の進行方向を示し、コードの品質を保証する ための指針」になる(指針≒レール) • 小さいステップに分けて • コードが汚くてもいいからさっさと作ってテストで動す (小さな動くものを作る) • これを繰り返して、少しずつ動く物を増やしていき
• ある程度形になったらテストを担保にコードを手直しして 綺麗に整える。 「TDDとは」をざっくり言い換えると 16 テストのメリット: 作ったコードを手軽に動かせる。何度でも -
TDDの本質の裏側にある意図 18 • フロー状態(没頭)に入りやすく、かつ持続するための手引き • (Feedback)サイクル = 設計の経験知を地道に積み上げる機会 • 小さなタスク
(動作の実装 / 設計の洗練) に分解し 1つずつ順番に集中して取り組む • 設計のフィードバックサイクルを構築して回す 本質 なぜ「小さく分解」するのか? -
TDDの本質の裏側にある意図 19 小さなタスク ( 動作の実装 / 設計の洗練 ) に分解 大きな泥団子に立ち向かう
https://speakerdeck.com/masuda220/big-ball-of-mud?slide=13 より引用 -
TDDの本質の裏側にある意図 • いきなり大きな物は倒せない • 小さく容易な物から倒していけ ばいずれ倒せる 20 大 き な
ゴ | ル 小さく分解する ドミノ倒し的な理論: -
小さく分解することで繋がるもの ⇒ フロー状態 • 明確な目標と即時のフィードバック • 特定のタスクへの高度な集中 • スキルとチャレンジのバランス •
反射的自意識の喪失 • 時間のゆがみや時間感覚の変化 • タスクに対して自分でコントロールしている感覚と主体性 • 行動と意識の統合 • 自己目的的な体験(フロー状態に本質的なやりがいがある) https://asana.com/ja/resources/flow-state-work より引用 21 フロー状態 に入るための重要な8要素: -
22 1. 目標設定と フィード バック 明確な目標とフィードバック 各テストが明確な短期目標となり、テストが成功 したかの即時フィードバックが得られる タスクに対して自分でコント ロールしている感覚と主体性
自分が開発の進行を完全にコントロールしている 感覚が得られる 2. 集中と没頭 特定のタスクへの高度な集中 1つのテストに都度集中することで、他のことに 気を散らさずに作業に没頭できる 反射的自意識の喪失 反復的なプロセスに没頭することで自己批判や不 安から解放される 3. 行動と意識 の融合 行動と意識の統合 テストを書いてそれを通すという明確な行動と、 その行動に対する意識が一体化する 時間のゆがみや時間感覚の変化 作業に没頭するあまり、時間の経過を忘れる 4. 成長と達成 感 スキルとチャレンジのバランス テストを通して徐々に難易度を上げながら進める ため、スキルに適したチャレンジを提供 自己目的的な体験 (フロー状態 に本質的なやりがいがある) テストが成功する度に得られる達成感が、作業を 自己目的化する。その過程を楽しめる フ ロ | 状 態 と T D D の 関 連 by ChatGPT -
チャレンジ(挑戦)の機会=リファクタリング • リファクタリング (分解して組み上げ直す) ◦ = 挑戦 (Try & Error
による経験知の積み重ね) ◦ ※設計力の向上には経験知が必須(本を読むだけでは身に付かない) 23 TDDのフローに沿う = 経験知を積み重ねる機会を明確に設ける ▪ (何度でも)失敗できる状態にしてから取り組む • 失敗したら大きな手戻りや負債が残るリスクがある • 失敗しても容易にリトライできるよう準備して取り組む ◦ テストを書く(+バックアップ取っておくと最初からやり直せる) -
価値のあるユニットテストとは? ※ここでの「価値」の定義: • プロダクトコードを修正しても壊れにくいテスト • 不良の発見にも繋がりやすいテスト • ドキュメントとしても機能するテスト 25 ブラックボックステスト視点のユニットテスト
➡ 〇〇の入力に対して、✕✕の出力が得られる(IN/OUT) -
ブラックボックステスト視点のユニットテスト • ブラックボックステスト ◦ 入力と出力に基づいてシステムの動作を検証する方法 ※ 入出力は、内部状態更新や副作用(DB更新等)も含む広義の方 26 モジュール (テスト対象)
IN OUT (テスト対象の)内部構造を変更 してもテストは壊れにくい テストが仕様(IN/OUT)を表す = ドキュメントとして機能 仕様(IN/OUT)を意識することで 不良が摘出しやすい -
TDDのサイクルの中でのテスト 27 • BAD: ◦ 内部ロジックに依存するテスト(ホワイトボックステスト) ⇒ 頻繁にテストが壊れる • GOOD:
◦ 入出力 を確認するテスト ⇒ 壊れにくい ◦ テストがよりレールとして機能する ▪ 動作を実装する際、リファクタリングする際に、 テストが仕様を守るためのレール(指針)となる 振る舞い:〇〇の入力に対して、✕✕の出力をする 動作:振る舞いを複数の入出力の段階に分けたもの -
不足を後からホワイトボックステストで補完 • 内部の詳細なロジックやアルゴリズム ◦ コードカバレッジ: ▪ 分岐やループを網羅的にテスト • C0(命令網羅)、C1(分岐網羅)、C2(条件網羅) ◦
ロジックの正当性: ▪ 複雑なアルゴリズムや内部ロジックの詳細動作確認 28 内部設計が安定した段階で行い テストを壊すことなく、細部の品質まで担保できる ブラックボックステストではカバーできない部分 -
入出力に着目するテストの具体例 • データ駆動テスト(Data-Driven Test) ◦ Java: JUnit5 の @ParameterizedTest ◦
PHP: PHPUnit の @dataProvidor ◦ Python: pytest の @pytest.mark.parametrize ◦ Golang: TableDrivenTests (テーブル駆動テスト) etc. 29 • 補足:AAAパターン ※テストのフォーマット ◦ Arrange:準備、 Act:実行、 Assert:検証 テストをよりドキュメントとして機能させる -
30 import java.time.Duration; import java.time.ZonedDateTime; public class DateTimeRange { private
final ZonedDateTime startTime; private final ZonedDateTime endTime; DateTimeRange(ZonedDateTime startTime, ZonedDateTime endTime) { if (startTime.isAfter(endTime)) { throw new IllegalArgumentException("~"); } Duration duration = Duration.between(startTime, endTime); if (duration.toDays() >= 1) { throw new IllegalArgumentException("~"); } this.startTime = startTime; this.endTime = endTime; } } 時間幅(開始/終了時刻) を持つクラス ・開始時刻 < 終了時刻 ・1日未満までの期間を指定可能 -
31 import static org.assertj.core.api.Assertions.assertThat; class DateTimeRangeTest { @ParameterizedTest @CsvSource({ "2024-01-01T00:00:00Z,
2024-01-01T12:00:00Z" , "2024-01-01T00:00:00Z, 2024-01-01T00:00:01Z" , "2024-01-01T00:00:00Z, 2024-01-01T23:59:59Z" , "2024-01-01T12:00:00Z, 2024-01-02T11:59:59Z" }) void testDateTimeRange _valid(String start, String end) { // Arrange (準備) ZonedDateTime startTime = ZonedDateTime.parse(start); ZonedDateTime endTime = ZonedDateTime.parse(end); // Act ( 実行) DateTimeRange timeRange = new DateTimeRange(startTime, endTime); // Assert ( 検証) assertThat(timeRange.getStartTime()).isEqualTo(startTime); assertThat(timeRange.getEndTime()).isEqualTo(endTime); } -
32 @ParameterizedTest @CsvSource({ "2024-01-01T12:00:00Z, 2024-01-01T11:00:00Z" , "2024-01-01T12:00:00Z, 2024-01-01T11:59:59Z" , "2024-01-01T00:00:00Z,
2024-01-01T00:00:00Z" "2024-01-01T00:00:00Z, 2024-01-02T00:00:00Z" }) void testDateTimeRange _invalid(String start, String end) { ZonedDateTime startTime = ZonedDateTime.parse(start); ZonedDateTime endTime = ZonedDateTime.parse(end); assertThatThrownBy (() -> new DateTimeRange(startTime, endTime)) . isInstanceOf(IllegalArgumentException .class) } } • なにを確認したいテストなのかを意識することが大事 ◦ どんな入力データがありえるのかを押さえてテストを書く -
TDDなんだか難しい?(実体験) • いざやってみようとした時にぶつかった壁 ◦ テストから書く、なんだかやりづらい? ◦ (普段の開発が既存改修中心だったので) 「既存改修」のシーンではどうやればいいか分からない 34 •
度々ぶつかるかも ◦ 小さな関数(動作)へどう分解するのが適切か分からない ※ 適切に行うにはある程度の経験知が必要で、経験を積み重ねるしかない が、その過程で自分なりの考え方やアプローチを持つことがきっと重要 既存改修のシーンで難しいと感じる理由と自己流の分解のアプローチを紹介 -
「既存改修」ではTDDなんだか難しい? • TDDは「新規開発」を想定して考えられている 35 ➡ 「既存改修」でその機会は多くはない 成長期フェーズの普段の開発:「既存改修」が主 既存改修は? • 既存設計との整合性を考慮しながら手を加える必要があるため
◦ 既存の設計が複雑(クラスの依存関係が複雑)または制約が強い(設計の 柔軟性が制約される≒新規クラスを切りづらい等)とより一層難しさが増す 新規開発に比べて考える事が増える 新しい機能や大きめのまとまった処理の追加であれば、 その機能/処理の実装の範囲ならTDDやれるかもしれない…が -
「既存改修」ではTDDなんだか難しい? 36 既存改修の分類 1. ちょっとした変更を入れるような修正 ◦ 数行程度の修正(ちょっとした条件分岐追加や処理の差込みなど) 2. まとまった処理(小~大)を追加するような修正 ◦
10行位の小さなもの~ 3. 1 と 2 のハイブリット ◦ 既存改修の多くがこのパターン ▪ 具体例:小さな新規の処理を追加する、 それを差し込むために既存に数行程度の修正を入れる ➡ アプローチを少し工夫する必要がある -
既存改修のイメージ 37 XXXXXXXX XXXXXXXX XXXXXXXX XXXXXXXX XXXXXXXX XXXXXXXX XXXXXXXX XXXXXXXX
全体としてどうなるべきか を捉えた上で まとまった処理を追加 ソースコード群 ちょっとした変更を入れる この2つを分けて捉える (改修部分のリストアップ ) 1つ1つの「まとまった処理」 を段階的に組み上げていく ➡ 必要な所に差し込む -
ちょっとした変更を入れるような修正 • 既存の処理をテストで担保してから修正(デグレ防止) ◦ テストを書くのが難しい場合は、手を入れる周辺を (意味のある単位で)関数に外出しテスト可能にする ◦ 小さくインクリメンタルに変更を加える、を繰り返す 38 •
補足:バグ(プログラム不良)修正の場合 ◦ バグ = 考慮漏れ・予期しない入力であることが多い ◦ 最初にバグを再現するテストを追加(あるべき状態を意識でき その失敗ケースをテストとして残すことができる) ▪ = 知識(入力として〇〇がありえる)として残る 「テストから書く」が非常に有効 -
新規開発=まっさらな状態 まとまった処理を追加するような修正 • 分解して組み上げていく ◦ ただ、ここでTDDやりづらい問題にぶつかる(かもしれない) ▪ 既にあるコードの中で、この作業をどこでどう行えば良いのか… 39 XXXXXX
XXXXXX XXXXXX XXXXXX XXXXXX どっちが やりやすい? 何かを考えるの に使っていいと 渡された紙 • 自分が自由にできる領域を確保し、そこに必要な処理を実装する ◦ 新しいクラスを切る ◦ 既存クラスの最下部などに(改行を入れて)スペースを作る TDDやりづらいへの克服の糸口 まっさらな領域を確保 して前提条件を揃える -
実践の型(自己流) • 水平分解 ◦ まとまった処理を動作(小さい関数)へ 分解(動作を1つずつ実装)することで 段階的に1つの処理を作り上げる 40 ➡ どちらもテストから書いてません
• 垂直分解 ◦ まとまった処理に対して正常系・異常系 などのケースごとに実装を進めることで 段階的に作り上げる (後から動作へ分解) ※勝手に命名 -
前置き:テストから書くがやりにくいと感じる理由 • テストを書くためにはテスト対象の関数(メソッド)の定義が必須 ◦ 一発で関数のインターフェース決めきれるか問題 ◦ さっさと動くものを作るのが大事 ▪ (小規模ならできなくもないが) 一発で決めるの難しい
▪ 実装を進める中で気付くことはままある • そこに悩んで足踏みするのは勿体ない • 悩む位ならコードを書いて考えた方が速い • 関数のインターフェースを変えるとテストも直さないといけない ◦ テストとソースを行き来(作業のスイッチング) ▪ 集中力が削がれる、最悪何を考えてたか飛ぶ • 頭の中にあるイメージ(コード)をさっさと書き出したい 41 (個人の感想) 「テストから書く」が難しいシーンは存在する。柔軟性が大事 -
コードから書いて、後からテストでも問題ない 42 TDDの フロー • ゴール:テストが成功する ◦ 順序:テストを書く ➡ 関数を実装する
◦ 達成証明:テストが成功すること (期待される振る舞い/動作が正しく実現できているか 確認) 自己流 • ゴール:関数を実装する ※関数の列挙をTodoリストに代用 ◦ 順序:関数を実装する ➡ テストを書く ◦ 達成証明:テストが成功すること (期待される振る舞い/動作が正しく実現できているか 確認) 何を確認するかがブレなければ同等の効果は引き出せる そのために意識していること:入出力(どんなケースがありえるか) ➡ 振る舞い/動作の適切な把握に繋がり、設計を洗練することに繋がる -
戻りまして、実践の型(自己流) • 水平分解(動作に分解) ◦ どう分解するのが適切か 最初から見える範囲までは こちら 43 public class
ExportService { public byte[] exportData( ExportRequest request) { fetchData(); formatData(); convertData(); } void fetchData() { } void formatData() { } void convertData() { } } ※関数の列挙をTodoリストに代用 まとまった処理へのアプローチ: • 垂直分解(ケースで分解) ◦ どう分解するのが適切か 分からない部分はこちら ◦ 個々の動作にどんなケース があるかの把握はこちら 水平分解+垂直分解の合わせ技 -
水平分解 ※関数分解により、設計の自由度をある程度確保 しつつ、テスト可能な単位で開発を進める考え方 44 関数1 関数2 関数3 処 理 入力
出力 大 元 の 関 数 入力 出力 入力 出力 入力 出力 小さいパーツを関数として列挙 • 1つの処理を小さな関数に分解 • 1つの小さな関数の実装&テスト(*) ◦ これを繰り返す • 最後に大元の関数の実装&テスト 迷う位なら A で行い、最後に不要なテストは (その役目を終えたと考え)容赦なく捨てる ※ただし、仕様理解促進に繋がるなら残す A. 可視性を上げて個別にテスト B. 大元の関数でまとめてテスト (*)個別のテストはどちらか判断して実施 大きな入出力を複数の入出力の段階に分ける -
水平分解の前提:適切に分解できること • 分解できるところまでで良い ◦ 分解できたところまでで、そのまま実装してみる ※ 下手に分解すると手戻りの元になりかねないため ◦ 悩んで手が止まるのが一番勿体ない ※
自分の中の敷居を少しでも低くして手を動かすことが大事 45 どう分解すればいいか分からない時は? • できるところまで分解して実装を進める際 ◦ 軸となる物(例:正常系ワンパス)を用意し、 それを元に他のケースを考えながら実装していく (テストで入出力を押さえながら) -
垂直分解 46 異 常 ① 異 常 ② 正 常
① 正 常 ② 正 常 ③ • 他にどんな入力がありえるか考える ➜ 具体の軸があるため考えやすい • 他の正常系を作る • テストを書く (正常②③…) • (繰り返す) 処 理 関数内をケース毎に分けて段階的実装 • どんな入力の場合はエラーとなる 必要があるか考える • 異常系を実装する • テストを書く (異常①②…) • (繰り返す) • 正常系ワンパス作る ➜ 1つの具体的な軸ができる • テストを書く (正常①) P h a s e 1 P h a s e 2 P h a s e 3 -
• 仕上がったら、最初に分解できなかった塊を具体的なコードを 見ながらどう分解すれば良いかを考える(水平分解) ◦ テストを担保にリファクタリング(関数/メソッド分割のみ) • その後に再度垂直分解を行い、どんなケースがあるかを把握する 垂直分解のあとに水平分解 47 ◦
振る舞いの分析(振る舞いを構成する動作の洗い出し) ◦ 設計の洗練に繋がる ▪ (小さく分解するから)より整理され、理解しやすく、 保守しやすい構造に組み上げられる この 分解過程が 必要なら 繰り返す • (分解したものの動作を保証もしてくれるが) • 分解を手助けしてくれる • 分解したものへの理解を補助してくれる テストは -
既存改修の全体イメージまとめ • 修正後の姿を捉え、修正箇所を特定 • まとまった処理の実装 ◦ 水平分解+垂直分解 ◦ テストを用いて段階的実装 •
ちょっとした修正 ◦ まとまった処理の追加 ◦ テスト(場合にもよるが周辺 をテスト可能な単位に外だし) 48 どのタイミングでテストを行う かを見通しながら進めていく XXXXXXXX XXXXXXXX XXXXXXXX XXXXXXXX XXXXXXXX XXXXXXXX XXXXXXXX XXXXXXXX 段階的に実装&テストしながら設計 を洗練して期待される物を作る -
いつ、何を、どうテストするかをベースに考える テストコードにはテストの意図を込めよう #vstat https://speakerdeck.com/nihonbuson/tesutokodonihatesutofalseyi-tu-woip-meyou?slide=40 より部分引用 49 テスト 分析 テスト 設計 テスト 実装
テスト 実行 何をテスト するか どうテスト するか テスト 分析 テスト 設計 いつやるか テストを軸に考える (これがきっとテストファーストの真意) -
まとめ(TDDの恩恵を引き出す考え方) 50 『TDDは分析技法であり、設計技法であり、実際には開発のすべてのアクティビティを 構造化する技法』 by Kent Beck • 段階的に小さく要素分解していく ◦
テストがこれを補助してくれる ◦ 意識するのは入出力 (どんなケースがありえるか) ※ データ駆動テストも適宜活用 • 分解して始めて集約や一元化(リファクタリング)ができる • 「テストから書く」に捕らわれない 分解しながら、いつ/何を/どう テストするか見通しながら進める • 振る舞いの分析 • 設計の洗練 に繋がる -
レイヤードアーキテクチャ(DDDベース) 52 ユースケースを実現する場所 • ユースケースとは ◦ ユーザが実現して欲しいシナリオ ◦ 外部仕様の中核となるもの UserInterface
層 Application 層 (ユースケース層) Domain 層 Infrastructure 層 ビ ジ ネ ス ロ ジ ッ ク 層 クライアントとの入出力をする層 データベースとの入出力をする層 ルールや制約などのドメイン知識の 実現をする層 -
ユースケース と 振る舞い の関係 53 システム 振る舞い ◦ 外部から見た動き 1:Nの関係
「動作」はきっと二分できる ・ 業務領域に関するもの ・ それ以外(技術的な要素等) 例:データの変換 外部 仕様 ユ | ス ケ | ス ユ | ス ケ | ス 振る舞い 動作 動作 動作 振る舞い 動作 動作 動作 振る舞い 動作 動作 動作 ユ | ス ケ | ス ユースケース ◦ (ユーザ等の)利用シナリオ -
レイヤードアーキテクチャ(DDDベース) 54 UserInterface 層 Application 層 (Use Case 層) Domain
層 Infrastructure 層 ビ ジ ネ ス ロ ジ ッ ク 層 適切な配置をするためにも「動作」 の単位に適切に分解する事が必要 ユースケース ユースケース 振る舞い 動作 動作 振る舞い 動作 動作 動作 動作 振る舞い 動作 動作 動作 動作 (表側) (裏側) -
3層アーキテクチャ(ドメイン層不要) 55 例:(単純な)データ操作中心のアプリ 動作:1つの具体的な処理 ・データのバリデーション/変換 ・(あれば)特定のビジネスルールに基づく処理 UserInterface 層 Business Logic
層 ※複雑になりがち ⇒ 肥大化/神クラス Infrastructure 層 ユースケース ユースケース 振る舞い 振る舞い 動作 動作 動作 振る舞い (表側) 動 作 動作 動作 動作 動作 動作 (裏側) 振る舞い:ユースケースを実現するため に必要な一連の動作の集合? 動作(必要なら振る舞い)をクラス化