<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
    <id>https://brick-next.js.org/zh/blog</id>
    <title>Brick Next Blog</title>
    <updated>2019-06-15T00:00:00.000Z</updated>
    <generator>https://github.com/jpmonette/feed</generator>
    <link rel="alternate" href="https://brick-next.js.org/zh/blog"/>
    <subtitle>Brick Next Blog</subtitle>
    <icon>https://brick-next.js.org/zh/img/favicon.png</icon>
    <entry>
        <title type="html"><![CDATA[编写良好的单元测试]]></title>
        <id>https://brick-next.js.org/zh/blog/writing-good-unit-tests</id>
        <link href="https://brick-next.js.org/zh/blog/writing-good-unit-tests"/>
        <updated>2019-06-15T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[“万物之始，大道至简”]]></summary>
        <content type="html"><![CDATA[<blockquote>
<p>“万物之始，大道至简”</p>
</blockquote>
<p>本文尝试从简单的单元测试思想着手，探讨如何编写良好的单元测试。以下将主要基于 <a href="https://www.typescriptlang.org/" target="_blank" rel="noopener noreferrer">TypeScript</a>, <a href="https://jestjs.io/" target="_blank" rel="noopener noreferrer">Jest</a>, <a href="https://reactjs.org/" target="_blank" rel="noopener noreferrer">React</a>, <a href="https://airbnb.io/enzyme/" target="_blank" rel="noopener noreferrer">Enzyme</a> 给出示例。关于单元测试的<a href="https://zh.wikipedia.org/wiki/%E5%8D%95%E5%85%83%E6%B5%8B%E8%AF%95" target="_blank" rel="noopener noreferrer">基本概念和重要性</a>不在本文讨论范围。</p>
<p><img decoding="async" loading="lazy" src="https://brick-next.js.org/zh/assets/images/martin-sanchez-MD6E2Sv__iA-unsplash-789596ac9e4fb582a0e35d8acc8c5d9c.jpg" width="900" height="600" class="img_ev3q"></p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="basics">基本方法<a href="https://brick-next.js.org/zh/blog/writing-good-unit-tests#basics" class="hash-link" aria-label="基本方法的直接链接" title="基本方法的直接链接">​</a></h2>
<p>编写单元测试的基本方法其实很简单：</p>
<ol>
<li>给定输入</li>
<li>运行</li>
<li>断言输出</li>
</ol>
<p>而一个好的单元测试的要求也很简单：</p>
<ul>
<li>覆盖足够的输入场景</li>
<li>进行充分的输出断言</li>
</ul>
<p>一个最简单的例子：</p>
<div class="language-ts codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-ts codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token comment" style="color:#6B6B6B;font-style:italic">// `add.ts`</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#CF222E">function</span><span class="token plain"> </span><span class="token function" style="color:#8250DF">add</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">a</span><span class="token operator" style="color:#D73A49">:</span><span class="token plain"> </span><span class="token builtin">number</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> b</span><span class="token operator" style="color:#D73A49">:</span><span class="token plain"> </span><span class="token builtin">number</span><span class="token punctuation" style="color:#393A34">)</span><span class="token operator" style="color:#D73A49">:</span><span class="token plain"> </span><span class="token builtin">number</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#CF222E">return</span><span class="token plain"> a </span><span class="token operator" style="color:#D73A49">+</span><span class="token plain"> b</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="复制代码到剪贴板" title="复制" class="clean-btn"><span class="copyButtonIcons_eSgA" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_y97N"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_LjdS"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<div class="language-ts codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-ts codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token comment" style="color:#6B6B6B;font-style:italic">// `add.spec.ts`</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token function" style="color:#8250DF">test</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#0451a5">"add(1, 2) should return 3"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token operator" style="color:#D73A49">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token function" style="color:#8250DF">expect</span><span class="token punctuation" style="color:#393A34">(</span><span class="token function" style="color:#8250DF">add</span><span class="token punctuation" style="color:#393A34">(</span><span class="token number" style="color:#0000ff">1</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token number" style="color:#0000ff">2</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#8250DF">toBe</span><span class="token punctuation" style="color:#393A34">(</span><span class="token number" style="color:#0000ff">3</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="复制代码到剪贴板" title="复制" class="clean-btn"><span class="copyButtonIcons_eSgA" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_y97N"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_LjdS"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>我们编写单元测试的步骤如下：</p>
<ol>
<li>给定输入：<code>1</code>, <code>2</code></li>
<li>运行： <code>add(...)</code></li>
<li>断言输出：<code>expect(...).toBe(3)</code></li>
</ol>
<p>是不是很简单？当然，真实业务场景下我们要测试的单元远比上述例子复杂得多。</p>
<ul>
<li>输入更加复杂，除了普通的函数输入参数，还可能有外部的事件，因此难以覆盖所有场景</li>
<li>输出更加复杂，除了普通的函数输出结果，还可能有对外部的副作用，因此难以断言运行结果</li>
</ul>
<blockquote>
<p>这里提到的<em>输入</em>、<em>输出</em>不再是狭义上的函数输入、输出。我们将所有可能影响测试对象行为的外部因素都称之为输入，将所有测试对象运行后对外部造成的影响都称之为输出。这样理解之后，我们就可以化繁为简，将测试过程回归到前面提到的最基本的方法上。</p>
</blockquote>
<p>所以编写良好的单元测试首先要做的就是厘清测试对象的输入、输出，掌握覆盖不同形式的输入、断言不同形式的输出的方法。我们将分开讨论它们。</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="inputs">输入<a href="https://brick-next.js.org/zh/blog/writing-good-unit-tests#inputs" class="hash-link" aria-label="输入的直接链接" title="输入的直接链接">​</a></h2>
<p>足够简单的输入让我们可以花更少的时间、覆盖更多的场景。输入的来源大致有以下几种：</p>
<ul>
<li>普通变量参数</li>
<li>外部依赖发送的事件</li>
<li>GUI 操作事件</li>
</ul>
<p>编写测试覆盖它们的复杂度依次增大。除了第一个，其它都可以看作<em>外部事件</em>，也可以理解为<em>来自外界的副作用</em>。对于普通变量参数，我们只需构造这些参数即可完成<em>给定输入</em>的任务。而对于外部事件，我们要做的就是想办法触发这些事件。</p>
<p>我们依然看一个简单的例子：</p>
<div class="language-ts codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-ts codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token comment" style="color:#6B6B6B;font-style:italic">// `MyComponent.ts`</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">window</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#8250DF">addEventListener</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#0451a5">"resize"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token operator" style="color:#D73A49">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token operator" style="color:#D73A49">...</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="复制代码到剪贴板" title="复制" class="clean-btn"><span class="copyButtonIcons_eSgA" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_y97N"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_LjdS"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>如何覆盖？主动发送这个事件：</p>
<div class="language-ts codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-ts codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token comment" style="color:#6B6B6B;font-style:italic">// `MyComponent.spec.ts`</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token function" style="color:#8250DF">test</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#0451a5">"MyComponent"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token operator" style="color:#D73A49">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  window</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#8250DF">dispatchEvent</span><span class="token punctuation" style="color:#393A34">(</span><span class="token keyword" style="color:#CF222E">new</span><span class="token plain"> </span><span class="token class-name" style="color:#116329">Event</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#0451a5">"resize"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token comment" style="color:#6B6B6B;font-style:italic">// ...</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="复制代码到剪贴板" title="复制" class="clean-btn"><span class="copyButtonIcons_eSgA" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_y97N"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_LjdS"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>再看一个例子：</p>
<div class="language-tsx codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-tsx codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token comment" style="color:#6B6B6B;font-style:italic">// `MyComponent.tsx`</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#CF222E">const</span><span class="token plain"> handleChange </span><span class="token operator" style="color:#D73A49">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">value</span><span class="token operator" style="color:#D73A49">:</span><span class="token plain"> </span><span class="token builtin">string</span><span class="token punctuation" style="color:#393A34">)</span><span class="token operator" style="color:#D73A49">:</span><span class="token plain"> </span><span class="token keyword" style="color:#CF222E">void</span><span class="token plain"> </span><span class="token arrow operator" style="color:#D73A49">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> </span><span class="token spread operator" style="color:#D73A49">...</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#CF222E">return</span><span class="token plain"> </span><span class="token tag punctuation" style="color:#393A34">&lt;</span><span class="token tag class-name" style="color:#116329">Editor</span><span class="token tag" style="color:#800000"> </span><span class="token tag attr-name" style="color:#0550AE">onChange</span><span class="token tag script language-javascript script-punctuation punctuation" style="color:#393A34">=</span><span class="token tag script language-javascript punctuation" style="color:#393A34">{</span><span class="token tag script language-javascript" style="color:#24292E">handleChange</span><span class="token tag script language-javascript punctuation" style="color:#393A34">}</span><span class="token tag" style="color:#800000"> </span><span class="token tag punctuation" style="color:#393A34">/&gt;</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="复制代码到剪贴板" title="复制" class="clean-btn"><span class="copyButtonIcons_eSgA" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_y97N"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_LjdS"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>如何覆盖依赖组件的特定事件？主动触发依赖组件的事件：</p>
<div class="language-tsx codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-tsx codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token comment" style="color:#6B6B6B;font-style:italic">// `MyComponent.spec.tsx`</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token function" style="color:#8250DF">test</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#0451a5">"MyComponent"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#D73A49">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#CF222E">const</span><span class="token plain"> wrapper </span><span class="token operator" style="color:#D73A49">=</span><span class="token plain"> </span><span class="token function" style="color:#8250DF">shallow</span><span class="token punctuation" style="color:#393A34">(</span><span class="token tag punctuation" style="color:#393A34">&lt;</span><span class="token tag class-name" style="color:#116329">MyComponent</span><span class="token tag" style="color:#800000"> </span><span class="token tag punctuation" style="color:#393A34">/&gt;</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  wrapper</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#8250DF">find</span><span class="token punctuation" style="color:#393A34">(</span><span class="token maybe-class-name">Editor</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#8250DF">invoke</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#0451a5">"onChange"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#0451a5">"faked value"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token comment" style="color:#6B6B6B;font-style:italic">// ...</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="复制代码到剪贴板" title="复制" class="clean-btn"><span class="copyButtonIcons_eSgA" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_y97N"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_LjdS"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="outputs">输出<a href="https://brick-next.js.org/zh/blog/writing-good-unit-tests#outputs" class="hash-link" aria-label="输出的直接链接" title="输出的直接链接">​</a></h2>
<p>足够简单的输出让我们可以更容易地断言运行结果。输出的形态大致有以下几种：</p>
<ul>
<li>普通变量输出</li>
<li>GUI 的变化</li>
<li>外部依赖的调用</li>
</ul>
<p>在测试中对它们进行断言的复杂度依次增大。除了第一个，其它都可以看作<em>对外界的副作用</em>。对于普通变量输出，我们只需简单地断言它的值即可。而对于对外界的副作用，我们要做的就是想办法断言这些副作用的影响。</p>
<p>我们继续看一个简单的例子：</p>
<div class="language-ts codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-ts codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token comment" style="color:#6B6B6B;font-style:italic">// `handleClick.ts`</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#CF222E">function</span><span class="token plain"> </span><span class="token function" style="color:#8250DF">handleClick</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token operator" style="color:#D73A49">:</span><span class="token plain"> </span><span class="token keyword" style="color:#CF222E">void</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  history</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#8250DF">push</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#0451a5">"/next/url"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="复制代码到剪贴板" title="复制" class="clean-btn"><span class="copyButtonIcons_eSgA" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_y97N"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_LjdS"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>如何断言？我们可以断言副作用的影响结果：</p>
<div class="language-ts codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-ts codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token comment" style="color:#6B6B6B;font-style:italic">// `handleClick.spec.ts`</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token function" style="color:#8250DF">test</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#0451a5">"handleClick"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token operator" style="color:#D73A49">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token function" style="color:#8250DF">handleClick</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token function" style="color:#8250DF">expect</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">history</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">location</span><span class="token punctuation" style="color:#393A34">.</span><span class="token plain">pathname</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#8250DF">toBe</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#0451a5">"/next/url"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="复制代码到剪贴板" title="复制" class="clean-btn"><span class="copyButtonIcons_eSgA" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_y97N"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_LjdS"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>有时副作用所影响的结果难以断言，或者该依赖被 <em>Mocked</em>，那么我们可以监视该副作用的触发点是否被正确调用了：</p>
<div class="language-ts codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-ts codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token comment" style="color:#6B6B6B;font-style:italic">// `handleClick.spec.ts`</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token function" style="color:#8250DF">test</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#0451a5">"handleClick"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token operator" style="color:#D73A49">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#CF222E">const</span><span class="token plain"> spyOnHistoryPush </span><span class="token operator" style="color:#D73A49">=</span><span class="token plain"> jest</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#8250DF">spyOn</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">history</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token string" style="color:#0451a5">"push"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token function" style="color:#8250DF">expect</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">spyOnHistoryPush</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">.</span><span class="token function" style="color:#8250DF">toBeCalledWith</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#0451a5">"/next/url"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="复制代码到剪贴板" title="复制" class="clean-btn"><span class="copyButtonIcons_eSgA" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_y97N"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_LjdS"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>再看一个 React 组件的例子：</p>
<div class="language-tsx codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-tsx codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token comment" style="color:#6B6B6B;font-style:italic">// `MyComponent.tsx`</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#CF222E">const</span><span class="token plain"> handleValidation </span><span class="token operator" style="color:#D73A49">=</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">valid</span><span class="token operator" style="color:#D73A49">:</span><span class="token plain"> </span><span class="token builtin">boolean</span><span class="token punctuation" style="color:#393A34">)</span><span class="token operator" style="color:#D73A49">:</span><span class="token plain"> </span><span class="token keyword" style="color:#CF222E">void</span><span class="token plain"> </span><span class="token arrow operator" style="color:#D73A49">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#CF222E">this</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#8250DF">setState</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"> valid </span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token keyword" style="color:#CF222E">return</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token tag punctuation" style="color:#393A34">&lt;</span><span class="token tag class-name" style="color:#116329">Form.Item</span><span class="token tag" style="color:#800000"> </span><span class="token tag attr-name" style="color:#0550AE">className</span><span class="token tag script language-javascript script-punctuation punctuation" style="color:#393A34">=</span><span class="token tag script language-javascript punctuation" style="color:#393A34">{</span><span class="token tag script language-javascript keyword" style="color:#CF222E">this</span><span class="token tag script language-javascript punctuation" style="color:#393A34">.</span><span class="token tag script language-javascript property-access" style="color:#24292E">state</span><span class="token tag script language-javascript punctuation" style="color:#393A34">.</span><span class="token tag script language-javascript property-access" style="color:#24292E">valid</span><span class="token tag script language-javascript" style="color:#24292E"> </span><span class="token tag script language-javascript operator" style="color:#D73A49">?</span><span class="token tag script language-javascript" style="color:#24292E"> </span><span class="token tag script language-javascript string" style="color:#0451a5">"valid"</span><span class="token tag script language-javascript" style="color:#24292E"> </span><span class="token tag script language-javascript operator" style="color:#D73A49">:</span><span class="token tag script language-javascript" style="color:#24292E"> </span><span class="token tag script language-javascript string" style="color:#0451a5">"invalid"</span><span class="token tag script language-javascript punctuation" style="color:#393A34">}</span><span class="token tag punctuation" style="color:#393A34">&gt;</span><span class="token plain-text"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain-text">    </span><span class="token tag punctuation" style="color:#393A34">&lt;</span><span class="token tag class-name" style="color:#116329">Input</span><span class="token tag" style="color:#800000"> </span><span class="token tag punctuation" style="color:#393A34">/&gt;</span><span class="token plain-text"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain-text">  </span><span class="token tag punctuation" style="color:#393A34">&lt;/</span><span class="token tag class-name" style="color:#116329">Form.Item</span><span class="token tag punctuation" style="color:#393A34">&gt;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="复制代码到剪贴板" title="复制" class="clean-btn"><span class="copyButtonIcons_eSgA" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_y97N"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_LjdS"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>如何断言？判断依赖组件的变化：</p>
<div class="language-tsx codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-tsx codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token comment" style="color:#6B6B6B;font-style:italic">// `MyComponent.spec.tsx`</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token function" style="color:#8250DF">test</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#0451a5">"MyComponent"</span><span class="token punctuation" style="color:#393A34">,</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token plain"> </span><span class="token arrow operator" style="color:#D73A49">=&gt;</span><span class="token plain"> </span><span class="token punctuation" style="color:#393A34">{</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token keyword" style="color:#CF222E">const</span><span class="token plain"> wrapper </span><span class="token operator" style="color:#D73A49">=</span><span class="token plain"> </span><span class="token function" style="color:#8250DF">shallow</span><span class="token punctuation" style="color:#393A34">(</span><span class="token tag punctuation" style="color:#393A34">&lt;</span><span class="token tag class-name" style="color:#116329">MyComponent</span><span class="token tag" style="color:#800000"> </span><span class="token tag punctuation" style="color:#393A34">/&gt;</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token comment" style="color:#6B6B6B;font-style:italic">// ... after something trigger `handleValidation()`</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain">  </span><span class="token function" style="color:#8250DF">expect</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">wrapper</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#8250DF">find</span><span class="token punctuation" style="color:#393A34">(</span><span class="token maybe-class-name">Form</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access maybe-class-name">Item</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#8250DF">prop</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#0451a5">"className"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#8250DF">toBe</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#0451a5">"invalid"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token punctuation" style="color:#393A34">}</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="复制代码到剪贴板" title="复制" class="clean-btn"><span class="copyButtonIcons_eSgA" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_y97N"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_LjdS"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<p>始终记得要断言测试对象运行后对外界的副作用影响。</p>
<p>另外断言的目标应该是<em>对外的影响</em>，而不是<em>内部状态</em>，因为内部状态并不是测试对象的<em>输出</em>。一个错误的例子：</p>
<div class="language-tsx codeBlockContainer_Ckt0 theme-code-block" style="--prism-color:#393A34;--prism-background-color:#f6f8fa"><div class="codeBlockContent_biex"><pre tabindex="0" class="prism-code language-tsx codeBlock_bY9V thin-scrollbar" style="color:#393A34;background-color:#f6f8fa"><code class="codeBlockLines_e6Vv"><span class="token-line" style="color:#393A34"><span class="token comment" style="color:#6B6B6B;font-style:italic">// `MyComponent.bad.spec.tsx`</span><span class="token plain"></span><br></span><span class="token-line" style="color:#393A34"><span class="token plain"></span><span class="token function" style="color:#8250DF">expect</span><span class="token punctuation" style="color:#393A34">(</span><span class="token plain">wrapper</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#8250DF">instance</span><span class="token punctuation" style="color:#393A34">(</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">state</span><span class="token punctuation" style="color:#393A34">.</span><span class="token property-access">valid</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">.</span><span class="token method function property-access" style="color:#8250DF">toBe</span><span class="token punctuation" style="color:#393A34">(</span><span class="token string" style="color:#0451a5">"invalid"</span><span class="token punctuation" style="color:#393A34">)</span><span class="token punctuation" style="color:#393A34">;</span><br></span></code></pre><div class="buttonGroup__atx"><button type="button" aria-label="复制代码到剪贴板" title="复制" class="clean-btn"><span class="copyButtonIcons_eSgA" aria-hidden="true"><svg viewBox="0 0 24 24" class="copyButtonIcon_y97N"><path fill="currentColor" d="M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"></path></svg><svg viewBox="0 0 24 24" class="copyButtonSuccessIcon_LjdS"><path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"></path></svg></span></button></div></div></div>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="refactoring">重构与拆分<a href="https://brick-next.js.org/zh/blog/writing-good-unit-tests#refactoring" class="hash-link" aria-label="重构与拆分的直接链接" title="重构与拆分的直接链接">​</a></h2>
<p>更简单的输入、输出让我们可以更容易地编写好的单元测试，但往往实际情况是业务需求不断增长，组件内部逻辑不断复杂化，输入输出的形式形态更加多样化，为组件编写单元测试的难度也随之陡增。</p>
<p><strong>适时地重构与拆分</strong>是解决这个问题的关键。在如今的前端组件化的模式下尤为重要，合理拆分后的组件可以让每个测试单元的输入输出都变得更少、更聚焦。诸如 React, <a href="https://redux.js.org/" target="_blank" rel="noopener noreferrer">Redux</a> 等主流框架和工具推崇的<a href="https://flaviocopes.com/react-unidirectional-data-flow/" target="_blank" rel="noopener noreferrer">单向数据流</a>盛行的其中一个原因就是它们巧妙地让各个单元的输入来源、输出影响单一化，从而降低编写单元测试的难度，同时提升组件集成时的信心。</p>
<h2 class="anchor anchorWithStickyNavbar_LWe7" id="summary">总结<a href="https://brick-next.js.org/zh/blog/writing-good-unit-tests#summary" class="hash-link" aria-label="总结的直接链接" title="总结的直接链接">​</a></h2>
<p>编写良好的单元测试总结下来就是三条：</p>
<ul>
<li>识别测试对象的输入、输出</li>
<li>掌握不同形态下的输入覆盖、输出断言的方法</li>
<li>适时地重构与拆分</li>
</ul>
<p>希望以上内容对大家有所帮助。</p>]]></content>
    </entry>
</feed>