You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@dolphinscheduler.apache.org by gi...@apache.org on 2021/10/13 12:08:58 UTC

[dolphinscheduler-website] branch asf-site updated: Automated deployment: af81b360287536e7469c1476c13b448493877d84

This is an automated email from the ASF dual-hosted git repository.

github-bot pushed a commit to branch asf-site
in repository https://gitbox.apache.org/repos/asf/dolphinscheduler-website.git


The following commit(s) were added to refs/heads/asf-site by this push:
     new 3bd4975  Automated deployment: af81b360287536e7469c1476c13b448493877d84
3bd4975 is described below

commit 3bd497549ff34797a1af247d1f639656fe577494
Author: github-actions[bot] <gi...@users.noreply.github.com>
AuthorDate: Wed Oct 13 12:08:53 2021 +0000

    Automated deployment: af81b360287536e7469c1476c13b448493877d84
---
 img/unit-test/ut-communication-based-style.png | Bin 0 -> 35613 bytes
 img/unit-test/ut-controller-architecture.png   | Bin 0 -> 28429 bytes
 img/unit-test/ut-nested-3a.png                 | Bin 0 -> 16759 bytes
 img/unit-test/ut-output-based-style.png        | Bin 0 -> 34419 bytes
 img/unit-test/ut-state-based-style.png         | Bin 0 -> 38705 bytes
 zh-cn/blog/ut-guideline.html                   | 411 +++++++++++++++++++++++++
 zh-cn/blog/ut-guideline.json                   |   6 +
 zh-cn/blog/ut-template.html                    |   4 +-
 zh-cn/blog/ut-template.json                    |   2 +-
 9 files changed, 420 insertions(+), 3 deletions(-)

diff --git a/img/unit-test/ut-communication-based-style.png b/img/unit-test/ut-communication-based-style.png
new file mode 100644
index 0000000..43f847b
Binary files /dev/null and b/img/unit-test/ut-communication-based-style.png differ
diff --git a/img/unit-test/ut-controller-architecture.png b/img/unit-test/ut-controller-architecture.png
new file mode 100644
index 0000000..2c22291
Binary files /dev/null and b/img/unit-test/ut-controller-architecture.png differ
diff --git a/img/unit-test/ut-nested-3a.png b/img/unit-test/ut-nested-3a.png
new file mode 100644
index 0000000..32b5ec7
Binary files /dev/null and b/img/unit-test/ut-nested-3a.png differ
diff --git a/img/unit-test/ut-output-based-style.png b/img/unit-test/ut-output-based-style.png
new file mode 100644
index 0000000..7ac0cb4
Binary files /dev/null and b/img/unit-test/ut-output-based-style.png differ
diff --git a/img/unit-test/ut-state-based-style.png b/img/unit-test/ut-state-based-style.png
new file mode 100644
index 0000000..d9a1861
Binary files /dev/null and b/img/unit-test/ut-state-based-style.png differ
diff --git a/zh-cn/blog/ut-guideline.html b/zh-cn/blog/ut-guideline.html
new file mode 100644
index 0000000..16fdb48
--- /dev/null
+++ b/zh-cn/blog/ut-guideline.html
@@ -0,0 +1,411 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+  <meta charset="UTF-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
+  <meta name="keywords" content="ut-guideline">
+  <meta name="description" content="ut-guideline">
+  <title>ut-guideline</title>
+  <link rel="shortcut icon" href="/img/favicon.ico">
+  <link rel="stylesheet" href="/build/vendor.e328afe.css">
+  <link rel="stylesheet" href="/build/blog.md.fd8b187.css">
+</head>
+<body>
+  <div id="root"><div class="blog-detail-page" data-reactroot=""><header class="header-container header-container-dark"><div class="header-body"><a href="/zh-cn/index.html"><img class="logo" src="/img/hlogo_white.svg"/></a><div class="search search-dark"><span class="icon-search"></span></div><span class="language-switch language-switch-dark">En</span><div class="header-menu"><img class="header-menu-toggle" src="/img/system/menu_white.png"/><div><ul class="ant-menu whiteClass ant-menu-li [...]
+<blockquote>
+<p>部分内容参考自《Unit Testing Principles, Practices, and Patterns》</p>
+</blockquote>
+<h2>1. 单元测试架构</h2>
+<h3>1.1 如何构建单元测试?</h3>
+<blockquote>
+<p>本节将展示如何通过 3A 原则来构建单元测试,应当避免哪些缺陷,以及如何使测试更具有可读性。</p>
+</blockquote>
+<h4>1.1.1 3A 原则</h4>
+<p>3A 原则简单易懂,它为套件中的所有测试提供了统一的结构,这种统一的结构是其最大的优势之一:一旦习惯了这种模式,就可以更轻松地阅读和理解测试,这反过来又降低了整个测试套件的维护成本。</p>
+<ul>
+<li>Arrange:初始化测试数据。</li>
+<li>Act:调用被测方法,传入依赖参数并获取返回值。</li>
+<li>Assert:断言,对返回值做出断言。</li>
+</ul>
+<p>示例:</p>
+<pre><code class="language-java"><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Calculator</span> </span>{
+    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">long</span> <span class="hljs-title">sum</span><span class="hljs-params">(<span class="hljs-keyword">long</span> a, <span class="hljs-keyword">long</span> b)</span> </span>{
+        <span class="hljs-keyword">return</span> a + b;
+    }
+}
+</code></pre>
+<pre><code class="language-java"><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">CalculatorTest</span> </span>{
+    <span class="hljs-meta">@Test</span>
+    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">sum</span><span class="hljs-params">()</span> </span>{
+        <span class="hljs-comment">// Arrange</span>
+        <span class="hljs-keyword">long</span> a = <span class="hljs-number">1L</span>, b = <span class="hljs-number">2L</span>;
+        Calculator calculator = <span class="hljs-keyword">new</span> Calculator();
+        <span class="hljs-comment">// Act</span>
+        <span class="hljs-keyword">long</span> actual = calculator.sum(a, b);
+        <span class="hljs-comment">// Assert</span>
+        <span class="hljs-keyword">long</span> expected = <span class="hljs-number">3L</span>;
+        assertEquals(expected, actual);
+    }
+}
+</code></pre>
+<p>从 <code>arrange</code> 或 <code>assert</code> 开始编写单元测试都是可行的。当采用 TDD(Test-Driven Development,测试驱动开发)思想进行开发时,由于是在开发特性之前编写单元测试,因此对特性的细节并没有完全了解,此时应当首先理清期望从中得到的结果是什么,进而再思考如何满足这些期望,此时更加推荐从 <code>assert</code> 开始编写单元测试。当然,如果并没有遵循 TDD 思想,也就是在编写单元测试之前业务代码已经实现,那么这时候更推荐从 <code>arrange</code> 开始编写单元测试。</p>
+<h4>1.1.2 避免多重 Arrage, Act, Assert 嵌套</h4>
+<p><img src="/img/unit-test/ut-nested-3a.png" alt="nested-3a"></p>
+<p>多重 Arrange, Act, Assert 意味着该测试正在验证多个行为单元,那么它就不再是单元测试,而是集成测试了。此时应当将这种测试分解成多个测试,使单元测试更加简单、快速且易于理解。</p>
+<h4>1.1.3 避免使用 <code>if</code> 条件语句</h4>
+<p>无论是单元测试还是集成测试,都应该是一个没有分支的、简单的步骤序列,而 <code>if</code> 语句表明在一次测试中验证了多种情况,因此,同样应该将这种测试分解成多个测试。</p>
+<h4>1.1.4 删除 arrange, act, assert 的注释</h4>
+<p>区分 arrange、act、assert 三部分可以提高可读性。可以通过以下两种方式进行区分:</p>
+<ol>
+<li>
+<p>在每部分的开始添加注释来表明是哪一部分。</p>
+<pre><code class="language-java"><span class="hljs-comment">// arrange</span>
+Calculator calculator = <span class="hljs-keyword">new</span> Calculator();
+<span class="hljs-comment">// act</span>
+<span class="hljs-keyword">long</span> actual = calculator.sum(<span class="hljs-number">1L</span>, <span class="hljs-number">2L</span>);
+<span class="hljs-comment">// assert</span>
+assertEquals(<span class="hljs-number">3L</span>, actual);
+</code></pre>
+</li>
+<li>
+<p>在各部分之间添加空行。</p>
+</li>
+</ol>
+<h3>1.2 单元测试命名</h3>
+<p>首先,单元测试的名称是否具有表达性是很重要的,合适的名称可以让人们快速理解这个测试的目的和结果。</p>
+<p>推荐使用下列命名方式:</p>
+<pre><code>[MethodUnderTest]_[Scenario]_[ExpectedResult]
+</code></pre>
+<ul>
+<li><code>MethodUnderTest</code>:被测试方法名。</li>
+<li><code>Scenario</code>:测试方法的条件。</li>
+<li><code>ExpectedResult</code>:在上述条件下所期望的结果。</li>
+</ul>
+<p>如测试 <code>isAdult()</code> 方法:</p>
+<pre><code class="language-java"><span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">boolean</span> <span class="hljs-title">isAdult</span><span class="hljs-params">(<span class="hljs-keyword">int</span> age)</span> </span>{
+    <span class="hljs-keyword">if</span> (age &lt; <span class="hljs-number">0</span> || age &gt; <span class="hljs-number">120</span>) {
+        <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> IllegalArgumentException(<span class="hljs-string">&quot;The age is illegal&quot;</span>);
+    }
+    <span class="hljs-keyword">return</span> age &gt;= <span class="hljs-number">18</span>;
+}
+</code></pre>
+<p><code>isAdult</code> 测试方法命名:</p>
+<pre><code>isAdult_MoreThan18_ReturnTrue	// 大于 18 岁 =&gt; 是成年人
+isAdult_LessThan0_ThrowIllegalArgumentException	// 小于 0 岁 =&gt; 非法输入
+</code></pre>
+<p>此外,还需注意一下几点:</p>
+<ol>
+<li>不要遵循严格的命名方式。因为有时很难在方法名中描述复杂的情况。</li>
+<li>用下划线分隔单词。 这样做有助于提高可读性。</li>
+</ol>
+<h2>2. 单元测试风格</h2>
+<p>三种单元测试风格:</p>
+<ul>
+<li><code>output-based style</code>,基于输出结果的测试风格</li>
+<li><code>state-based style</code>,基于状态的测试风格</li>
+<li><code>communication-based style</code>,基于通信的测试风格</li>
+</ul>
+<h3>2.1 output-based style</h3>
+<p>该测试风格如下图所示,在输入参数之后对业务代码的输出结果进行验证。该测试风格只适用于测试不会改变全局或内部状态的业务代码,因此只需验证其返回值即可。</p>
+<p><img src="/img/unit-test/ut-output-based-style.png" alt="output-based-style"></p>
+<p>示例:</p>
+<p><code>calculateDiscount()</code> 方法用于计算一组物品的折扣。</p>
+<pre><code class="language-java"><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">PriceEngine</span> </span>{
+    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">double</span> <span class="hljs-title">calculateDiscount</span><span class="hljs-params">(Product[] products)</span> </span>{
+        <span class="hljs-keyword">double</span> discount = products.length * <span class="hljs-number">0.01</span>;
+        <span class="hljs-keyword">return</span> Math.min(discount, <span class="hljs-number">0.02</span>);
+    }
+}
+</code></pre>
+<p><code>output-based</code> 风格的单元测试:</p>
+<pre><code class="language-java"><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">PriceEngineTest</span> </span>{
+    <span class="hljs-meta">@Test</span>
+    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">calculateDiscount_MinimumDiscount_ReturnMinimumDiscount</span><span class="hljs-params">()</span> </span>{
+        <span class="hljs-comment">// arrange</span>
+		Product product1 = <span class="hljs-keyword">new</span> Product(<span class="hljs-string">&quot;Hand wash&quot;</span>);
+        Product product2 = <span class="hljs-keyword">new</span> Product(<span class="hljs-string">&quot;Shampoo&quot;</span>);
+        PriceEngine priceEngine = <span class="hljs-keyword">new</span> PriceEngine();
+        Product[] products = <span class="hljs-keyword">new</span> Product[]{product1, product2};
+        <span class="hljs-comment">// act</span>
+        <span class="hljs-keyword">double</span> discount = priceEngine.calculateDiscount(products);
+        <span class="hljs-comment">// assert</span>
+        Assert.assertEquals(<span class="hljs-number">0.02</span>, discount);
+    }
+}
+</code></pre>
+<h3>2.2 state-based style</h3>
+<p>该测试风格如下图所示,在操作完成后验证系统的最终状态。其中,“状态”一词可以指单元测试本身,也可以指数据库、文件系统等外部依赖。</p>
+<p><img src="/img/unit-test/ut-state-based-style.png" alt="state-based-style"></p>
+<p>示例:</p>
+<p><code>addProduct()</code> 方法用于向订单中添加一个物品。</p>
+<pre><code class="language-java"><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Order</span> </span>{
+    <span class="hljs-keyword">public</span> List&lt;Product&gt; products = <span class="hljs-keyword">new</span> ArrayList&lt;&gt;();
+
+    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">addProduct</span><span class="hljs-params">(Product product)</span> </span>{
+        products.add(product);
+    }
+}
+</code></pre>
+<p><code>state-based</code> 风格的单元测试:</p>
+<pre><code class="language-java"><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">OrderTest</span> </span>{
+    <span class="hljs-meta">@Test</span>
+    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">addProduct_AddAProduct</span><span class="hljs-params">()</span> </span>{
+        <span class="hljs-comment">// arrange</span>
+        Product product = <span class="hljs-keyword">new</span> Product(<span class="hljs-string">&quot;Hand wash&quot;</span>);
+        Order order = <span class="hljs-keyword">new</span> Order();
+        <span class="hljs-comment">// act</span>
+        order.addProduct(product);
+        <span class="hljs-comment">// assert</span>
+        Assert.assertEquals(<span class="hljs-number">1</span>, order.products.size());
+        Assert.assertEquals(product, order.products.get(<span class="hljs-number">0</span>));
+    }
+}
+</code></pre>
+<blockquote>
+<p>与 <code>output-based style</code> 不同,<code>addProduct()</code> 方法的结果会导致订单状态的更改。</p>
+</blockquote>
+<h3>2.3 communication-based style</h3>
+<p>该测试风格如下图所示,使用 mock 验证待测部分是否能够正确调用其他模块。</p>
+<p><img src="/img/unit-test/ut-communication-based-style.png" alt="ut-communication-based-style"></p>
+<p>示例:</p>
+<p><code>greetUser()</code> 方法用于发送问候邮件。</p>
+<pre><code class="language-java"><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">EmailSender</span> </span>{
+    
+    <span class="hljs-keyword">private</span> SendService service;
+    
+    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-title">EmailSender</span><span class="hljs-params">(SendService service)</span> </span>{
+		<span class="hljs-keyword">this</span>.service = service;
+    }
+
+    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">boolean</span> <span class="hljs-title">greetUser</span><span class="hljs-params">(String email)</span> </span>{
+        String message = <span class="hljs-string">&quot;Hello!&quot;</span>;
+        <span class="hljs-keyword">return</span> service.send(email, message);
+    }
+}
+</code></pre>
+<p><code>communication-based</code> 风格的单元测试:</p>
+<pre><code class="language-java"><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">EmailSenderTest</span> </span>{
+    <span class="hljs-meta">@Test</span>
+    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">greetUser</span><span class="hljs-params">()</span> </span>{
+        <span class="hljs-comment">// arrange</span>
+        SendService service = Mockito.mock(SendService.class);
+        EmailSender sender = <span class="hljs-keyword">new</span> EmailSender(service);
+        String email = <span class="hljs-string">&quot;user@email.com&quot;</span>;
+        when(service.send(email, Mockito.anyString())).thenReturn(<span class="hljs-keyword">true</span>);
+        <span class="hljs-comment">// act</span>
+        <span class="hljs-keyword">boolean</span> actual = sender.greetUser(email);
+        <span class="hljs-comment">// assert</span>
+        Assert.assertEquals(<span class="hljs-keyword">true</span>, actual);
+    }
+}
+</code></pre>
+<h3>2.4 对比</h3>
+<p>一个好的单元测试通常基于以下四种属性进行考量:</p>
+<ul>
+<li>
+<p>防止回归</p>
+</li>
+<li>
+<p>重构成本</p>
+</li>
+<li>
+<p>快速反馈</p>
+</li>
+<li>
+<p>可维护性</p>
+</li>
+</ul>
+<table>
+<thead>
+<tr>
+<th style="text-align:center"></th>
+<th style="text-align:center">output-based style</th>
+<th style="text-align:center">state-based style</th>
+<th style="text-align:center">communication-based style</th>
+</tr>
+</thead>
+<tbody>
+<tr>
+<td style="text-align:center">重构成本</td>
+<td style="text-align:center">低</td>
+<td style="text-align:center">中</td>
+<td style="text-align:center">中</td>
+</tr>
+<tr>
+<td style="text-align:center">维护成本</td>
+<td style="text-align:center">低</td>
+<td style="text-align:center">中</td>
+<td style="text-align:center">高</td>
+</tr>
+</tbody>
+</table>
+<blockquote>
+<p>三种风格在防止回归和快速反馈方面表现相同。</p>
+</blockquote>
+<p>经过对比,<code>output-based</code> 风格是最合适的。基于该风格的单元测试很少与实现细节耦合在一起,因此重构成本较小。同时由于该风格所具有的简洁、不依赖于外部环境等特性,因此更具有可维护性。</p>
+<p><code>state-based</code> 和 <code>communication-based</code> 风格的单元测试与实现细节的耦合度更高,因此更难以重构。并且它们代码量往往更大,从而导致更高的维护成本。</p>
+<p>综上,更推荐基于 <code>output-based</code> 风格编写单元测试。</p>
+<h2>3. 反例</h2>
+<h3>3.1 不要为了测试而更改 private 状态</h3>
+<p>原则上应当只针对 public API 进行测试。通常 private 方法只完成很小一部分功能,它是短小精悍的,虽然可以通过反射等技术实现对 private 方法的测试,但是这将会使测试变的繁琐而且更难维护,因此大多时候无需单独测试  private 方法。</p>
+<p>若 private 方法确实需要进行直接测试,也不要为了测试而更改 private 状态。可以使用下面两种方式实现:</p>
+<ol>
+<li>
+<p>设计测试用例,通过测试 public API 间接测试 private 方法。</p>
+</li>
+<li>
+<p>将 private 方法重构成工具类的 public 方法。</p>
+<blockquote>
+<p>这么做是为了改善设计,而不是帮助测试。</p>
+</blockquote>
+</li>
+</ol>
+<p>以第二种方式为例:</p>
+<pre><code class="language-java"><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Order</span> </span>{
+    <span class="hljs-keyword">private</span> Customer customer;
+    <span class="hljs-keyword">private</span> List&lt;Product&gt; products;
+    
+    <span class="hljs-function"><span class="hljs-keyword">public</span> String <span class="hljs-title">generateDescription</span><span class="hljs-params">()</span> </span>{
+		<span class="hljs-keyword">return</span> <span class="hljs-string">&quot;Customer name: &quot;</span> + customer.getName() + <span class="hljs-string">&quot;, total price: &quot;</span> + getPrice();
+    }
+    
+    <span class="hljs-function"><span class="hljs-keyword">private</span> <span class="hljs-keyword">double</span> <span class="hljs-title">getPrice</span><span class="hljs-params">()</span> </span>{
+        <span class="hljs-keyword">double</span> basePrice;	<span class="hljs-comment">// 基于 products 计算</span>
+        <span class="hljs-keyword">double</span> discounts;	<span class="hljs-comment">// 基于 customer 计算</span>
+        <span class="hljs-keyword">double</span> taxes;	<span class="hljs-comment">// 基于 products 计算</span>
+        <span class="hljs-comment">// do some calculation</span>
+        <span class="hljs-keyword">return</span> basePrice - discounts + taxes;
+    }
+}
+</code></pre>
+<p>其中,公有的 <code>generateDescription()</code> 方法非常简单,只是返回一个订单的信息,但是它所调用的私有的 <code>getPrice()</code> 方法却非常复杂。<code>getPrice()</code> 方法包含了重要的业务逻辑,因此需要进行全面测试。</p>
+<p>为了测试 <code>getPrice()</code> 方法,应当将其重构到单独一个类中。</p>
+<pre><code class="language-java"><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Order</span> </span>{
+    <span class="hljs-keyword">private</span> Customer customer;
+    <span class="hljs-keyword">private</span> List&lt;Product&gt; products;
+    
+    <span class="hljs-function"><span class="hljs-keyword">public</span> String <span class="hljs-title">generateDescription</span><span class="hljs-params">()</span> </span>{
+        PriceCalculator calc = <span class="hljs-keyword">new</span> PriceCalculator();
+		<span class="hljs-keyword">return</span> <span class="hljs-string">&quot;Customer name: &quot;</span> + customer.getName() +
+            <span class="hljs-string">&quot;, total price: &quot;</span> + calc.getPrice(customer, products);
+    }
+}
+
+<span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">PriceCalculator</span> </span>{
+    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">double</span> <span class="hljs-title">getPrice</span><span class="hljs-params">(Customer customer, List&lt;Product&gt; products)</span> </span>{
+        <span class="hljs-keyword">double</span> basePrice;	<span class="hljs-comment">// 基于 products 计算</span>
+        <span class="hljs-keyword">double</span> discounts;	<span class="hljs-comment">// 基于 customer 计算</span>
+        <span class="hljs-keyword">double</span> taxes;	<span class="hljs-comment">// 基于 products 计算</span>
+        <span class="hljs-comment">// do some calculation</span>
+        <span class="hljs-keyword">return</span> basePrice - discounts + taxes;
+    }
+}
+</code></pre>
+<p>这样,就可以不依赖于 <code>Order</code> 类实现 <code>PriceCalculator</code> 类的测试了。另外,由于 <code>getPrice()</code> 方法并没有改变任何状态,因此可以基于 <code>output-based</code> 风格编写该方法的单元测试。</p>
+<h3>3.2 测试中不要涉及业务代码逻辑</h3>
+<blockquote>
+<p>这种情况通常发生在涉及复杂算法的测试中。</p>
+</blockquote>
+<p>以一个简单的例子说明这种情况:</p>
+<pre><code class="language-java"><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">CalculatorTest</span> </span>{
+    <span class="hljs-meta">@Test</span>
+    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">sum</span><span class="hljs-params">()</span> </span>{
+        <span class="hljs-comment">// arrange</span>
+        <span class="hljs-keyword">long</span> a = <span class="hljs-number">1L</span>, b = <span class="hljs-number">2L</span>;
+        Calculator calculator = <span class="hljs-keyword">new</span> Calculator();
+        <span class="hljs-comment">// act</span>
+        <span class="hljs-keyword">long</span> actual = calculator.sum(a, b);
+        <span class="hljs-comment">// assert</span>
+        <span class="hljs-keyword">long</span> expected = a + b;	<span class="hljs-comment">// 反例</span>
+        <span class="hljs-keyword">long</span> expected = <span class="hljs-number">3L</span>;	<span class="hljs-comment">// 正例</span>
+        assertEquals(expected, actual);
+    }
+}
+</code></pre>
+<p>在编写单元测试时,应当将待测方法看成一个黑盒。</p>
+<h3>3.3 代码污染</h3>
+<p>代码污染是指添加仅用于测试的生产代码。它将测试代码和业务代码混合在一起,增加了维护成本。</p>
+<p>示例:</p>
+<pre><code class="language-java"><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Logger</span> </span>{
+    <span class="hljs-keyword">private</span> <span class="hljs-keyword">boolean</span> isTestEnvironment;
+    
+    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-title">Logger</span><span class="hljs-params">(<span class="hljs-keyword">boolean</span> isTestEnvironment)</span> </span>{
+        <span class="hljs-keyword">this</span>.isTestEnvironment = isTestEnvironment;
+    }
+    
+    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">log</span><span class="hljs-params">(String text)</span> </span>{
+        <span class="hljs-keyword">if</span> (!isTestEnvironment) {
+            <span class="hljs-comment">// log the text</span>
+        }
+    }
+}
+
+<span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">Controller</span> </span>{
+    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">someMethod</span><span class="hljs-params">(Logger logger)</span> </span>{
+        logger.log(<span class="hljs-string">&quot;someMethod is called&quot;</span>);
+    }
+}
+</code></pre>
+<p>在 <code>Logger</code> 中,参数 <code>isTestEnvironment</code> 变量表明当前是否运行在测试环境中,并通过构造函数传入。通过该变量可以在测试中灵活的控制日志打印,如下:</p>
+<pre><code class="language-java"><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ControllerTest</span> </span>{
+    <span class="hljs-meta">@Test</span>
+    <span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> someMethod_LogText_logNothing {
+        Logger logger = <span class="hljs-keyword">new</span> Logger(<span class="hljs-keyword">true</span>);	<span class="hljs-comment">// 设置为 true,表明当前处于测试环境</span>
+        Controller controller = <span class="hljs-keyword">new</span> Controller();
+        
+        controller.someMethod(logger);
+        
+        <span class="hljs-comment">// assert it won&#x27;t log nothing</span>
+    }
+}
+</code></pre>
+<p>示例中,首先创建了 <code>Logger</code> 类,并传入参数表明当前处于测试环境。但是如果 <code>Logger</code> 类的构造方法或参数一旦跟随业务变化而发生变动,所有涉及 <code>Logger</code> 类的单元测试都需要修改,从而增加了维护成本。</p>
+<p>可以通过接口划分为两类 Logger 来解决这个问题。</p>
+<pre><code class="language-java"><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">interface</span> <span class="hljs-title">Logger</span> </span>{
+    <span class="hljs-function"><span class="hljs-keyword">void</span> <span class="hljs-title">log</span><span class="hljs-params">(String text)</span></span>;
+}
+
+<span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">LoggerImpl</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">Logger</span> </span>{
+    <span class="hljs-meta">@Override</span>
+    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">log</span><span class="hljs-params">(String text)</span> </span>{
+        <span class="hljs-comment">// log the text</span>
+    }
+}
+
+<span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">FakeLoggerImpl</span> <span class="hljs-keyword">implements</span> <span class="hljs-title">Logger</span> </span>{
+    <span class="hljs-meta">@Override</span>
+    <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">log</span><span class="hljs-params">(String text)</span> </span>{
+        <span class="hljs-comment">// do nothing</span>
+    }
+}
+</code></pre>
+<p>这样,在测试时只需创建一个 <code>FakeLoggerImpl</code> 对象即可,即使打印日志的逻辑发生变化,也无需修改测试代码。</p>
+<pre><code class="language-java"><span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ControllerTest</span> </span>{
+    <span class="hljs-meta">@Test</span>
+    <span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> someMethod_LogText_logNothing {
+        Logger logger = <span class="hljs-keyword">new</span> FakeLoggerImpl();	<span class="hljs-comment">// 创建一个专门用于单元测试的 Logger 对象</span>
+        Controller controller = <span class="hljs-keyword">new</span> Controller();
+        
+        controller.someMethod(logger);
+        
+        <span class="hljs-comment">// assert it won&#x27;t log nothing</span>
+    }
+}
+</code></pre>
+</section><footer class="footer-container"><div class="footer-body"><div><h3>联系我们</h3><h4>有问题需要反馈?请通过以下方式联系我们。</h4></div><div class="contact-container"><ul><li><img class="img-base" src="/img/emailgray.png"/><img class="img-change" src="/img/emailblue.png"/><a href="/zh-cn/community/development/subscribe.html"><p>邮件列表</p></a></li><li><img class="img-base" src="/img/twittergray.png"/><img class="img-change" src="/img/twitterblue.png"/><a href="https://twitter.com/dolphinschedule"><p>Twitt [...]
+  <script src="//cdn.jsdelivr.net/npm/react@15.6.2/dist/react-with-addons.min.js"></script>
+  <script src="//cdn.jsdelivr.net/npm/react-dom@15.6.2/dist/react-dom.min.js"></script>
+  <script>window.rootPath = '';</script>
+  <script src="/build/vendor.68040ed.js"></script>
+  <script src="/build/blog.md.79dd3d0.js"></script>
+  <script>
+    var _hmt = _hmt || [];
+    (function() {
+      var hm = document.createElement("script");
+      hm.src = "https://hm.baidu.com/hm.js?4e7b4b400dd31fa015018a435c64d06f";
+      var s = document.getElementsByTagName("script")[0];
+      s.parentNode.insertBefore(hm, s);
+    })();
+  </script>
+</body>
+</html>
\ No newline at end of file
diff --git a/zh-cn/blog/ut-guideline.json b/zh-cn/blog/ut-guideline.json
new file mode 100644
index 0000000..1a6c450
--- /dev/null
+++ b/zh-cn/blog/ut-guideline.json
@@ -0,0 +1,6 @@
+{
+  "filename": "ut-guideline.md",
+  "__html": "<h1>Unit Test Guideline</h1>\n<blockquote>\n<p>部分内容参考自《Unit Testing Principles, Practices, and Patterns》</p>\n</blockquote>\n<h2>1. 单元测试架构</h2>\n<h3>1.1 如何构建单元测试?</h3>\n<blockquote>\n<p>本节将展示如何通过 3A 原则来构建单元测试,应当避免哪些缺陷,以及如何使测试更具有可读性。</p>\n</blockquote>\n<h4>1.1.1 3A 原则</h4>\n<p>3A 原则简单易懂,它为套件中的所有测试提供了统一的结构,这种统一的结构是其最大的优势之一:一旦习惯了这种模式,就可以更轻松地阅读和理解测试,这反过来又降低了整个测试套件的维护成本。</p>\n<ul>\n<li>Arrange:初始化测试数据。</li>\n<li>Act:调用被测方法,传入依赖参数并获取返回值。</li>\n<li>Assert:断言,对返回值做出断言。</li>\n</ul>\ [...]
+  "link": "/dist/zh-cn/blog/ut-guideline.html",
+  "meta": {}
+}
\ No newline at end of file
diff --git a/zh-cn/blog/ut-template.html b/zh-cn/blog/ut-template.html
index 185a918..280c41f 100644
--- a/zh-cn/blog/ut-template.html
+++ b/zh-cn/blog/ut-template.html
@@ -65,7 +65,7 @@
 </ol>
 <p>在任何环境、任何时间,多次执行后的结果一致,且可以重复执行。</p>
 <ol start="3">
-<li><strong>轻量型</strong></li>
+<li><strong>轻量性</strong></li>
 </ol>
 <p>测试应当是秒级甚至是毫秒级的,不应占用过多时间。</p>
 <blockquote>
@@ -215,7 +215,7 @@
 </ol>
 <h3>4.2 编写示例</h3>
 <p>Controller UT 架构如下:</p>
-<p><img src="/home/shenke/Desktop/%E5%BC%80%E6%BA%90%E4%B9%8B%E5%A4%8F/dolphinscheduler/210290131/img/12.png" alt=""></p>
+<p><img src="/img/unit-test/ut-controller-architecture.png" alt="ut-controller-architecture"></p>
 <p>对于新增的 controller,其测试代码编写步骤如下:</p>
 <ol>
 <li>继承 <code>RestControllerTest</code> 或 <code>NormalControllerTest</code></li>
diff --git a/zh-cn/blog/ut-template.json b/zh-cn/blog/ut-template.json
index 5f237af..a926128 100644
--- a/zh-cn/blog/ut-template.json
+++ b/zh-cn/blog/ut-template.json
@@ -1,6 +1,6 @@
 {
   "filename": "ut-template.md",
-  "__html": "<h1>DolphinScheduler 单元测试模版</h1>\n<h2>1. 单元测试原则</h2>\n<ol>\n<li>\n<p><strong>3A 原则</strong></p>\n<p>3A 原则简单易懂,它为套件中的所有测试提供了统一的结构,这种统一的结构是其最大的优势之一:一旦习惯了这种模式,就可以更轻松地阅读和理解测试,这反过来又降低了整个测试套件的维护成本。</p>\n<ul>\n<li>Arrange:初始化测试数据。</li>\n<li>Act:调用被测方法,传入依赖参数并获取返回值。</li>\n<li>Assert:断言,对返回值做出断言。</li>\n</ul>\n<p>示例:</p>\n<pre><code class=\"language-java\"><span class=\"hljs-keyword\">public</span> <span class=\"hljs-class\"><span class=\"hljs-keyword\">class</span> <span class=\"hljs [...]
+  "__html": "<h1>DolphinScheduler 单元测试模版</h1>\n<h2>1. 单元测试原则</h2>\n<ol>\n<li>\n<p><strong>3A 原则</strong></p>\n<p>3A 原则简单易懂,它为套件中的所有测试提供了统一的结构,这种统一的结构是其最大的优势之一:一旦习惯了这种模式,就可以更轻松地阅读和理解测试,这反过来又降低了整个测试套件的维护成本。</p>\n<ul>\n<li>Arrange:初始化测试数据。</li>\n<li>Act:调用被测方法,传入依赖参数并获取返回值。</li>\n<li>Assert:断言,对返回值做出断言。</li>\n</ul>\n<p>示例:</p>\n<pre><code class=\"language-java\"><span class=\"hljs-keyword\">public</span> <span class=\"hljs-class\"><span class=\"hljs-keyword\">class</span> <span class=\"hljs [...]
   "link": "/dist/zh-cn/blog/ut-template.html",
   "meta": {}
 }
\ No newline at end of file