<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Sociable Unit Tests on Tarragon</title><link>https://tarrragon.github.io/blog/tags/sociable-unit-tests/</link><description>Recent content in Sociable Unit Tests on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Wed, 04 Mar 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/sociable-unit-tests/index.xml" rel="self" type="application/rss+xml"/><item><title>行為優先的TDD方法論 - Sociable Unit Tests實踐指南</title><link>https://tarrragon.github.io/blog/record/%E8%A1%8C%E7%82%BA%E5%84%AA%E5%85%88%E7%9A%84tdd%E6%96%B9%E6%B3%95%E8%AB%96-sociable-unit-tests%E5%AF%A6%E8%B8%90%E6%8C%87%E5%8D%97/</link><pubDate>Wed, 04 Mar 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/record/%E8%A1%8C%E7%82%BA%E5%84%AA%E5%85%88%E7%9A%84tdd%E6%96%B9%E6%B3%95%E8%AB%96-sociable-unit-tests%E5%AF%A6%E8%B8%90%E6%8C%87%E5%8D%97/</guid><description>&lt;p>曾經有一段時間，我們團隊對TDD又愛又恨。「寫測試讓我們更有信心」，但「重構時要改一堆測試，還不如不寫」。這種矛盾讓我們反覆懷疑：TDD到底有沒有用？&lt;/p>
&lt;p>深入研究Kent Beck的原著和Valentina Jemuović的演講後，才發現問題出在我們誤解了「測試單元」是什麼。&lt;/p></description><content:encoded><![CDATA[<p>曾經有一段時間，我們團隊對TDD又愛又恨。「寫測試讓我們更有信心」，但「重構時要改一堆測試，還不如不寫」。這種矛盾讓我們反覆懷疑：TDD到底有沒有用？</p>
<p>深入研究Kent Beck的原著和Valentina Jemuović的演講後，才發現問題出在我們誤解了「測試單元」是什麼。</p>
<h2 id="痛苦的根本原因">痛苦的根本原因</h2>
<p>許多團隊學TDD時，都被教導「每個class寫一個test class，每個method寫一個test method」。這個看似合理的原則，埋下了長期的痛苦。</p>
<p>問題在於，這樣的測試耦合到了程式的<strong>結構</strong>，而非<strong>行為</strong>。只要重構——把一個class拆成兩個、把方法提取到新類別——測試就跟著破裂。維護測試的時間甚至超過寫功能本身。</p>
<p>Kent Beck在《Test Driven Development By Example》第一頁就寫道：</p>
<blockquote>
<p>&ldquo;Programmer tests should be sensitive to behavior changes and insensitive to structure changes.&rdquo;</p></blockquote>
<p>測試應該對行為的改變敏感，對結構的改變不敏感。如果重構時測試跟著爆炸，原因就在這裡。</p>
<h2 id="測試是可執行的需求規格">測試是可執行的需求規格</h2>
<p>需要先轉換一個根本認知：測試不是「驗證實作正確的工具」，而是<strong>用程式碼表達的需求規格書</strong>。</p>
<p>需求定義系統「應該做什麼」，實作是「怎麼做」的一種方式。需求應該保持穩定，實作可以隨時改變。Martin Fowler在《Refactoring》中說：</p>
<blockquote>
<p>&ldquo;Refactoring is a way of restructuring an existing body of code, altering its internal structure without changing its external behavior.&rdquo;</p></blockquote>
<p>重構改變內部結構，不改變外部行為。耦合到行為的測試，在重構時自然保持穩定。</p>
<h2 id="sociable-unit-tests把module當作測試單元">Sociable Unit Tests：把Module當作測試單元</h2>
<p>TDD有兩種截然不同的流派。</p>
<p><strong>Classical TDD</strong>（Kent Beck、Martin Fowler的做法）把Unit定義為Module——一個或多個協同工作的類別組合，對外提供清晰的Public API。測試只透過這個Public API互動，不知道Module內部有哪些類別、它們如何協作。唯一需要Mock的是真正的外部依賴：資料庫、檔案系統、外部服務。這種風格稱為<strong>Sociable Unit Tests</strong>。</p>
<p><strong>Mockist TDD</strong>（London School）把Unit定義為單一Class，Mock所有協作者。這種風格稱為<strong>Solitary Unit Tests</strong>。</p>
<p>核心差異在耦合對象：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">Sociable: Test → [Module API] → Module Implementation（黑盒）
</span></span><span class="line"><span class="ln">2</span><span class="cl">Solitary: Test → Mock(B) → Class A → Class B
</span></span><span class="line"><span class="ln">3</span><span class="cl">                 Mock(C)           → Class C</span></span></code></pre></div><p>Sociable只有一條耦合線，Solitary有多條。每一條耦合線都是日後的維護成本。</p>
<h2 id="重構安全性的驗證">重構安全性的驗證</h2>
<p>判斷自己的測試是Sociable還是Solitary，有個簡單的驗證方法：</p>
<p>改變Module的內部邏輯、調整類別結構、重新命名內部方法。如果所有測試依然通過，不需要修改，那你寫的是Sociable（正確）。如果任何測試需要跟著改，那你寫的是Solitary（需要重新設計）。</p>
<p>以一個訂單提交的例子來說，Sociable測試看起來像這樣：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="n">test</span><span class="p">(</span><span class="s1">&#39;使用者提交訂單成功&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="kd">async</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  <span class="c1">// Given: Mock外部依賴（只Mock Repository）
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="c1"></span>  <span class="n">when</span><span class="p">(</span><span class="n">mockRepository</span><span class="p">.</span><span class="n">save</span><span class="p">(</span><span class="n">any</span><span class="p">))</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">      <span class="p">.</span><span class="n">thenAnswer</span><span class="p">((</span><span class="n">_</span><span class="p">)</span> <span class="kd">async</span> <span class="o">=&gt;</span> <span class="n">SaveResult</span><span class="p">.</span><span class="n">success</span><span class="p">(</span><span class="s1">&#39;order-123&#39;</span><span class="p">));</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">  <span class="c1">// When: 透過Use Case API提交訂單
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="c1"></span>  <span class="kd">final</span> <span class="n">result</span> <span class="o">=</span> <span class="kd">await</span> <span class="n">submitOrderUseCase</span><span class="p">.</span><span class="n">execute</span><span class="p">(</span><span class="n">order</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">  <span class="c1">// Then: 驗證可觀察的行為結果
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1"></span>  <span class="n">expect</span><span class="p">(</span><span class="n">result</span><span class="p">.</span><span class="n">isSuccess</span><span class="p">,</span> <span class="kc">true</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">  <span class="n">expect</span><span class="p">(</span><span class="n">result</span><span class="p">.</span><span class="n">orderId</span><span class="p">,</span> <span class="s1">&#39;order-123&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">  <span class="c1">// 測試不知道Order內部如何計算、驗證
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="c1"></span>  <span class="c1">// 測試使用真實的Domain Entities
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="c1"></span><span class="p">});</span></span></span></code></pre></div><p>而Solitary測試會是：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dart" data-lang="dart"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="n">test</span><span class="p">(</span><span class="s1">&#39;OrderService.submitOrder calls Repository.save&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="kd">async</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  <span class="c1">// Given: Mock所有協作者
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="c1"></span>  <span class="kd">final</span> <span class="n">mockOrder</span> <span class="o">=</span> <span class="n">MockOrder</span><span class="p">();</span>          <span class="c1">// 連Order也Mock了
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="c1"></span>  <span class="kd">final</span> <span class="n">mockValidator</span> <span class="o">=</span> <span class="n">MockOrderValidator</span><span class="p">();</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">  <span class="kd">final</span> <span class="n">mockCalculator</span> <span class="o">=</span> <span class="n">MockPriceCalculator</span><span class="p">();</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">  <span class="n">when</span><span class="p">(</span><span class="n">mockValidator</span><span class="p">.</span><span class="n">validate</span><span class="p">(</span><span class="n">mockOrder</span><span class="p">)).</span><span class="n">thenReturn</span><span class="p">(</span><span class="kc">true</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">  <span class="n">when</span><span class="p">(</span><span class="n">mockCalculator</span><span class="p">.</span><span class="n">calculate</span><span class="p">(</span><span class="n">mockOrder</span><span class="p">)).</span><span class="n">thenReturn</span><span class="p">(</span><span class="m">100</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">  <span class="n">when</span><span class="p">(</span><span class="n">mockRepository</span><span class="p">.</span><span class="n">save</span><span class="p">(</span><span class="n">mockOrder</span><span class="p">))</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">      <span class="p">.</span><span class="n">thenAnswer</span><span class="p">((</span><span class="n">_</span><span class="p">)</span> <span class="kd">async</span> <span class="o">=&gt;</span> <span class="n">SaveResult</span><span class="p">.</span><span class="n">success</span><span class="p">(</span><span class="s1">&#39;order-123&#39;</span><span class="p">));</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">
</span></span><span class="line"><span class="ln">12</span><span class="cl">  <span class="c1">// Then: 驗證方法呼叫次數（實作細節）
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="c1"></span>  <span class="n">verify</span><span class="p">(</span><span class="n">mockRepository</span><span class="p">.</span><span class="n">save</span><span class="p">(</span><span class="n">mockOrder</span><span class="p">)).</span><span class="n">called</span><span class="p">(</span><span class="m">1</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">  <span class="c1">// 這個測試一旦重構OrderService的內部邏輯就會破裂
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="c1"></span><span class="p">});</span></span></span></code></pre></div><h2 id="test-first的速度優勢">Test-First的速度優勢</h2>
<p>Test-First（先寫測試）比Test-Last（先寫程式再補測試）快，原因是問題被發現的時間點更早。</p>
<p>Test-First的Red-Green-Refactor循環強迫你在寫實作之前先思考介面：「這個功能怎麼用？」、「測試容不容易寫？」介面設計問題在寫測試時（最早期）就暴露，修復成本最低。</p>
<p>Test-Last則是程式寫完了才發現難以測試，這時通常意味著設計有問題，要改動的範圍更大。Kent Beck說TDD更快，指的正是這個。</p>
<h2 id="bdd不是新方法是修正命名">BDD不是新方法，是修正命名</h2>
<p>Dan North在2006年創造「BDD」，目的是修正TDD命名造成的混淆。</p>
<p>他發現「Test」這個詞讓開發人員誤以為要測試每個類別和方法，於是用「Behavior」取代，讓意圖更清楚：測試的是行為，不是程式結構。這和Kent Beck 2003年說的完全一致，只是換了個能讓人更直覺理解的詞。</p>
<p>Google在《Software Engineering at Google》中也驗證同樣的結論：「Don&rsquo;t write a test for each method. Write a test for each behavior.」</p>
<h2 id="與clean-architecture的結合">與Clean Architecture的結合</h2>
<p>Sociable Unit Tests和Clean Architecture是天然的組合，因為建立在相同原則上：業務邏輯獨立於外部世界。</p>
<p>在Clean Architecture中，Use Cases層是業務邏輯的進入點，對外提供清晰的API，對內只使用Domain Entities和透過介面隔離的外部依賴（Repository、Gateway等）。這個結構天然對應Sociable的需求：Use Cases的Public API就是測試邊界，Domain Entities用真實物件，只有Repository需要Mock。</p>
<p>更重要的是，對Use Cases的Unit Test同時就是業務驗收測試。一個寫著「使用者提交訂單成功」的案例，不需要啟動UI也不需要真實資料庫，但驗證了完整的業務流程。Alistair Cockburn在提出Hexagonal Architecture時說：「Tests are another user of the system.」</p>
<p>並非所有情況都適合Sociable。數學演算法、加密系統這類需要細粒度驗證的場景，精確定位到具體類別比重構穩定性更重要，用Solitary合理。但大多數商業應用不是這類。</p>
<h2 id="結論">結論</h2>
<p>我們曾以為TDD很痛苦，但那是因為我們測試的是程式<strong>長什麼樣子</strong>，而不是它<strong>做什麼</strong>。</p>
<p>正確的做法只有一句話：測試透過Module的Public API互動，只Mock真正的外部依賴，使用真實的Domain Entities。</p>
<p>這樣的測試在重構時保持穩定，在功能改變時精準報警。Kent Beck、Dan North、Martin Fowler在不同年代說的是同一件事：<strong>測試行為，而非結構</strong>。</p>
<hr>
<p>參考資料：</p>
<ul>
<li>Kent Beck，《Test Driven Development By Example》，2003</li>
<li>Martin Fowler，《Refactoring: Improving the Design of Existing Code》，1999</li>
<li>Dan North，《Introducing BDD》，2006</li>
<li>Google，《Software Engineering at Google》，2020</li>
<li>Valentina (Cupać) Jemuović，<a href="https://www.youtube.com/watch?v=3wxiQB2-m2k">TDD and Clean Architecture - Driven by Behaviour</a></li>
</ul>]]></content:encoded></item></channel></rss>