隨著產品不斷地變大,在開發程式的時候多少會遇到上線的結果和預期中的樣子不太一樣,如果我們把一個大問題切割成很多個小問題去處理檢視,那麼就能降低測試上的負擔。今天要來帶大家經由撰寫簡單的單元測試來了解測試的原理是什麼。

為什麼要寫測試

你可能會想說,前端主要都是以畫面視覺為主呀?使用者也都是再用前端介面呀,為什麼還需要寫測試?
隨著modern web的發展,前端技術的含金量也越來越高,而且有些情境其實也很難保證一定寫對的。
如果你團隊的成員都能回答以下的答案,那麼再考慮不要在前端寫測試這件事情:

附上當你發現JS到底是什麼鬼東西時的表情:
⠄⠄⠄⠄⠄⠄⠄⠈⠉⠁⠈⠉⠉⠙⠿⣿⣿⣿⣿⣿這
⠄⠄⠄⠄⠄⠄⠄⠄⣀⣀⣀⠄⠄⠄⠄⠄⠹⣿⣿⣿什
⠄⠄⠄⠄⠄⢐⣲⣿⣿⣯⠭⠉⠙⠲⣄⡀⠄⠈⢿⣿麼
⠐⠄⠄⠰⠒⠚⢩⣉⠼⡟⠙⠛⠿⡟⣤⡳⡀⠄⠄⢻到
⠄⠄⢀⣀⣀⣢⣶⣿⣦⣭⣤⣭⣵⣶⣿⣿⣏⠄⠄⣿底
⠄⣼⣿⣿⣿⡉⣿⣀⣽⣸⣿⣿⣿⣿⣿⣿⣿⡆⣀⣿什
⢠⣿⣿⣿⠿⠟⠛⠻⢿⠿⣿⣿⣿⣿⣿⣿⣿⣿⣶⣼麼
⠄⣿⣿⣿⡆⠄⠄⠄⠄⠳⡈⣿⣿⣿⣿⣿⣿⣿⣿⣿輸
⠄⢹⣿⣿⡇⠄⠄⠄⠄⢀⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿出
⠄⠄⢿⣿⣷⣨⣽⣭⢁⣡⣿⣿⠟⣩⣿⣿⣿⠿⠿⠟哦
⠄⠄⠈⡍⠻⣿⣿⣿⣿⠟⠋⢁⣼⠿⠋⠉⠄⠄⠄⠄齁
⠄⠄⠄⠈⠴⢬⣙⣛⡥⠴⠂⠄⠄⠄⠄⠄⠄⠄⠄⠄齁
⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄齁

測試需要具備的工具

這篇的範例會使用最近較流行與常用的jest來當作範例,jest是facebook所開發的測試工具,所以當你CRA完之後,你會發現他們自動就幫你裝好jest了。

安裝

  1. 建置環境
    你可以使用codesandbox的javascript專案或是使用npm在你的local起一個專案。

  2. 安裝套件
    由於是在開發環境使用,所以會安裝在dev的dependencies

    1
    npm install --save-dev jest
  3. 設置指令
    你可以在terminal直接打jest,或是在package.json設置script

    1
    "test": "jest"
  4. 設置監聽
    我們希望開發的過程中不斷的監聽我們的更改,就不用一直重新啟動測試
    所以會在package.json加上:

    1
    "test": "jest --watch *.js"

設計function

再來我們先撰寫程式,要測試之前當然要先有可以被測試的程式
我們以下面簡單的function為例,有一個function輸入A句子會回傳B句子
Input:這什麼到底什麼閃現
Output:哦齁齁齁

1
2
3
4
5
6
7
const memeDb = {
'這什麼到底什麼閃現': "哦齁齁齁"
};

export const echoMeme = (keyword) => {
return memeDb[keyword];
};

開始寫測試

如果你剛剛的程式是寫在index.js
那麼你需要創建一個檔案叫index.test.js
寫測試很簡單,我們會使用到it

it是什麼

it就是字面上的意思,代表這個測試
當你的寫測試的時候會盡可能地讓它語意話
以這個例子來說,假設你預期echoMeme回傳哦齁齁齁,那你會說“這個句子是哦齁齁齁”
轉換成英文就會是:It should be ohhohoho/哦齁齁齁.
所以我們可以寫成:

1
2
3
it("should be ohhohoho/哦齁齁齁.", () => {
// ...
});

再來中間就是要放你需要測試的東西

測試句子

再來把需要測試的單元import,這邊會使用到expect,表示你預期要測試的單元。
toBe表示,你預期return的結果是什麼,測試寫起來非常的語意話與直覺。
可以自己唸一次:
it should be ohhohoho/哦齁齁齁.
it expect echoMeme’s keyword to be 哦齁齁齁.

1
2
3
4
5
import { echoMeme } from "./index";
it("should be ohhohoho/哦齁齁齁.", () => {
const keyword = "這什麼到底什麼閃現";
expect(echoMeme(keyword)).toBe('哦齁齁齁');
});

要測試的資料是資料庫

如果你要測試的資料來源是資料庫,那麼你可以mock一份資料在測試內使用(原因後面會寫)。
因為測試過程我們是不會用到任何連線的,一律都是local測試。
所以你會寫成:

1
2
3
4
5
6
7
8
9
import { echoMeme } from "./index";
const myExpectedDb = {
'這什麼到底什麼閃現': "哦齁齁齁"
};
it("should be ohhohoho", () => {
const keyword = "這到底什麼閃現";
expect(echoMeme(keyword)).toBe(myExpectedDb[keyword]);
});

執行測試

沒錯,這樣就寫好一個測試了,非常的單純。
執行指令:

1
npm run test

執行結果:

1
2
3
4
5
PASS ./index.test.js
should be ohhohoho
Test Suites: 1 passed, 1 total
...

當然你也可以試著改改看config,結果就會顯示測試不成功:

1
2
3
const myExpectedDb = {
'這什麼到底什麼閃現': "轟💣~"
};

失敗範例:

1
2
3
4
Expected value to be:
"哦齁齁齁"
Received:
"轟💣~"

以下提供範例程式給大家參考:

認識單元測試 - Unit test

好了,上面我們認識如何寫出基本的單元測試後,再來就要開始正式介紹了
單元測試通常是指可測試的最小單位,因為是unit,所以它可以是function, model……,任何可視為一個單位的範疇。
通常可測試的單元會擁有低耦合的特性,控制點不會是外來的。

為什麼需要它

通常一個專案都會有很多個function或是元件所組成,當專案越龐大的時候,其實越難保證寫出來的專案到底有沒有問題,特別是如果有人在協作開發的時候,誤用了不該被使用到的元件,就會發生改A壞B的狀況。而單元測試可以把一個大專案切分成很多個小單位進行測試,測試量寫的越多,越能夠提升產品的品質保證度。

好處

基本上寫單元測試你需要確保幾件事情:

  1. 盡可能不要用到Database或連線
    單元測試主要是要測試最小功能的可靠性,因此在正常的狀況下,你是不會需要依靠資料庫或是撈取外部的資料。
    如果前端在使用unit test的時候,不僅測試了function,也連線到了後端提供的payload。
    事實上你把測試範圍的範疇擴大了,外在因素也會影響到你的測試結果,因此這個測試的意義可能就會縮小了。
    另一個問題是,測試應該是要快速的,如果一個單元就要測試這麼久,那也失去了單元測試的意義了。‘
    因此過多的連線等待都是不希望發生的狀況。

  2. 測試完的資料不會被重複使用
    如果會被重複使用,那這個單元的切割本身就是一個問題了。
    測試的資料應該馬上消除,不應該去暫存它再使用它。

非常適合的對象

  1. pure function
    function如果設計的非常乾淨,就可以寫出越漂亮的測試,尤其是pure function。
    由於pure function不會受到外部的影響,因此也不會產生非預期的side effects。
    可參考我寫的認識與避免非預期的Side effects

  2. stateless component
    以React來說,掌握props in, view out的原則即可輕鬆寫元件測試。
    下面將會示範使用React搭配jest測試component的例子。

  3. Redux的reducers
    沒錯,reducers是一個很好的測試例子,每個reducer都是個別獨立的對象。
    讓我們來看以下範例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    const initialState = {
    loading: false,
    users: [],
    error: null
    };

    const userReducer = (state = initialState, action) => {
    switch (action.type) {
    case types.GET_USERS_REQUEST: {
    return {
    ...state,
    loading: true
    };
    }
    case types.GET_USERS_SUCCESS: {
    return {
    ...state,
    loading: action.payload.loading,
    users: action.payload.users
    };
    }
    case types.GET_USERS_FAILED: {
    return {
    ...state,
    loading: false,
    error: action.payload.error
    };
    }
    default:
    return state;
    }
    };

這是一個User的reducer,userReducer這個function傳進去的參數有起始的state和送進來的action,每次都會確保回傳一個state。
如果不考慮types的使用方式,你是否會發現,它不就是一個pure function嗎!
它只會做:收到資料 -> 轉換資料 -> 更新資料
因此reducer是一個適合測試的例子,你可以測試裡面轉換資料格式的過程中是否有沒有狀況發生。

導入單元測試到React上

目前React常見的測試工具是Enzyme,是由Airbnb所開發的,可參考官方網站的介紹
官方的測試是以mocha和chai作為示範,但是你也可以根據自己的需求客製化,Enzyme並沒有硬性規定一定要用哪些:
Enzyme is unopinionated regarding which test runner or assertion library you use, and should be compatible with all major test runners and assertion libraries out there. The documentation and examples for enzyme use mocha and chai, but you should be able to extrapolate to your framework of choice.

以下我會以這個範例來帶大家介紹,你也可以先玩玩看我寫的範例:

安裝

1
npm i --save-dev enzyme enzyme-adapter-react-16

根據官方的描述,enzyme需要另外安裝adapter來擴充測試需要用到的dependencies
例如,react-16需要安裝react與react-dom套件:
Each adapter may have additional peer dependencies which you will need to install as well. For instance, enzyme-adapter-react-16 has peer dependencies on react and react-dom.

如何使用Adapter

搭配configure使用即可

1
2
3
4
import Enzyme from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';

Enzyme.configure({ adapter: new Adapter() });

主要功能

這裏有三個你一定要會的分別叫做:

  1. shallow - 淺層
    shallow這個詞對使用過React的setState的你應該不陌生,在87%的時機你會使用到它,因為它非常的好用且簡易使用。
    當你使用shallow在測試上的時候,它僅會畫出指定的shallow DOM。
    假設我現在有一個Card(上次的範例):
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    import Hello from "./Hello";
    const Card = ({ name, id }) => {
    return (
    <li>
    <p>Name: {name}</p>
    <p>ID: {id}</p>
    <Hello />
    </li>
    );
    };
    export default Card;

我另外在裡面加了一個Hello的component:

1
2
3
4
5
6
7
8
9
const Hello = () => {
return (
<>
<p>Hey</p>
<p>Hello</p>
</>
);
};
export default Hello;

再來我們印出shallow查看(注意,這還不算是一種測試,只是debug):

1
2
3
4
5
6
7
8
9
10
11
import React from "react";
import Card from "./Card";
import Enzyme, { shallow } from "enzyme";
import Adapter from "enzyme-adapter-react-16";

Enzyme.configure({ adapter: new Adapter() });

it("expect to render Card component.", () => {
console.log(shallow(<Card />).debug());
expect(shallow(<Card />).length).toEqual(1);
});

輸出的內容如下:

1
2
3
4
5
6
7
8
9
<li>
<p>
Name:
</p>
<p>
ID:
</p>
<Hello />
</li>

你會發現它並不會畫出children的DOM,只會看到shallow,這樣非常適合測試。
因為你只需要注意的是這個component的結構,不需要去管其他的東西。

補充:在Enzyme v3以上可以使用shallow觸發component lifecycle,請看這篇文章

  1. mount - 掛載
    官方用簡單明嘹的詞解釋:Full DOM Rendering
    讓我們來看他們的解釋:
    Full DOM rendering requires that a full DOM API be available at the global scope. This means that it must be run in an environment that at least “looks like” a browser environment. If you do not want to run your tests inside of a browser, the recommended approach to using mount is to depend on a library called jsdom which is essentially a headless browser implemented completely in JS.

渲染出所有DOM的好處就是,你可以用一些DOM API和它們做互動,同時也可以讓你不用打開瀏覽器就能進行測試。
另外,以上面舉過的的例子來說,Hello component裡面可能會包含lifecycle的method。
有學過React lifecycle的人應該會知道,React一開始會將component進行mount,也就是說mount這個動作就會觸發component的lifecycle。
但是老實說,你不會常用到mount在你的測試上,因為使用到mount,他不僅會畫出所有的DOM,也會觸發到lifecycle,因此會讓整個測試情境變得很複雜,請注意!測試盡可能的保持簡潔與可靠才具有意義。

另外這裡要特別注意一點!因為React目前的版本是17,所以無法使用enzyme-adapter-react-16
已經有一些人遇到這種問題發issue了,詳細請查看這裡
目前enzyme-adapter-react-17正在開發,不過你可以先退回到React16進行練習
這邊列一下你可能需要留意的版本:

  • react: 16.14.0
  • react-dom:16.14.0
  • enzyme-adapter-react-16
  1. render - 渲染
    render會畫出component的DOM,但是並不會像是實際的DOM,而是static DOM,它所畫出來的DOM會和shallow與mount相似,不過最大的差別是在,它會使用到第三方的API叫做Cheerio,因為Airbnb團隊認為這個套件提供的走訪與轉換非常的優。
    經過render會產生一個CheerioWrapper,如果你對這個有興趣的話可以參考cheerio API

開始來測試Component

下面的例子會以簡易的container來帶大家簡單了解原理。
假設我有一個About介紹關於一位正在用你們產品的客戶
Card會介紹這個客戶是誰,Content會回饋給你它目前的狀況是什麼。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { useState } from "react";
import Content from "../components/Content";
import Card from "../components/Card";
const About = () => {
const [playerName] = useState("改革家");
const [crazyValue, setCrazyValue] = useState(80);
const handleCrazyValue = (event) => setCrazyValue(event.target.value);
return (
<div>
<Card
name={playerName}
value={crazyValue}
handleCrazyValue={handleCrazyValue}
/>
<Content value={crazyValue} />
</div>
);
};
export default About;

卡片僅是簡單的pure component
這裡可以修改中風指數:

1
2
3
4
5
6
7
8
9
10
11
12
const Card = ({ name, value, handleCrazyValue }) => {
return (
<div>
<p>玩家: {name}</p>
<div>
<span>中風指數</span>
<input value={value} onChange={handleCrazyValue} />
</div>
</div>
);
};
export default Card;

內文會根據值的改變顯示文案
你的客戶用你的產品用到快中風了,就會說:這什麼到底什麼網站Ohhohoho
如果用起來還行就會說:爽啦 AAAAAAAAAAAAA

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
const Content = ({ value }) => {
const isCrazy = value >= 80;
const text = isCrazy ? "這什麼到底什麼網站Ohhohoho" : "爽啦 AAAAAAAAAAAAA";
if (isCrazy) {
return (
<p>
⠄⠄⠄⠄⠄⠄⠄⠈⠉⠁⠈⠉⠉⠙⠿⣿⣿⣿⣿⣿
<br />
⠄⠄⠄⠄⠄⠄⠄⠄⣀⣀⣀⠄⠄⠄⠄⠄⠹⣿⣿⣿
<br />
⠄⠄⠄⠄⠄⢐⣲⣿⣿⣯⠭⠉⠙⠲⣄⡀⠄⠈⢿⣿
<br />
⠐⠄⠄⠰⠒⠚⢩⣉⠼⡟⠙⠛⠿⡟⣤⡳⡀⠄⠄⢻
<br />
⠄⠄⢀⣀⣀⣢⣶⣿⣦⣭⣤⣭⣵⣶⣿⣿⣏⠄⠄⣿
<br />
⠄⣼⣿⣿⣿⡉⣿⣀⣽⣸⣿⣿⣿⣿⣿⣿⣿⡆⣀⣿
<br />
⢠⣿⣿⣿⠿⠟⠛⠻⢿⠿⣿⣿⣿⣿⣿⣿⣿⣿⣶⣼
<br />
⠄⣿⣿⣿⡆⠄⠄⠄⠄⠳⡈⣿⣿⣿⣿⣿⣿⣿⣿⣿
<br />
⠄⢹⣿⣿⡇⠄⠄⠄⠄⢀⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
<br />
⠄⠄⢿⣿⣷⣨⣽⣭⢁⣡⣿⣿⠟⣩⣿⣿⣿⠿⠿⠟
<br />
⠄⠄⠈⡍⠻⣿⣿⣿⣿⠟⠋⢁⣼⠿⠋⠉⠄⠄⠄⠄
<br />
⠄⠄⠄⠈⠴⢬⣙⣛⡥⠴⠂⠄⠄⠄⠄⠄⠄⠄⠄⠄
<br />
{text}
<br />
⠄⠄⠄⠄⠄⠄你的使用者⠄⠄⠄⠄⠄⠄⠄
<br />
</p>
);
} else {
return <p>{text}</p>;
}
};
export default Content;

測試元件與輸出

介紹完這個簡易的作品之後,就要開始寫測試了,請建立一個 *.test.js 的檔案,或是放在test裡面
跑測試的時候會找尋整個專案裡的測試檔案或是test資料夾內的檔案
現在我先建立一個Card.test.js做簡單的輸出

查看輸出的話一樣可以使用console.log查看,debug查看component的shallow
最後寫出預期的結果,如果變數命名的好看的話,一樣可以寫出一個順暢的句子
唸起來像:it expect card length to equal 1.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import React from "react";
import Enzyme, { shallow } from "enzyme";
import Adapter from "enzyme-adapter-react-16";
import Card from "./Card";

Enzyme.configure({ adapter: new Adapter() });

it("expect Card length equals to 1.", () => {
const cardWrapper = shallow(<Card />);
const cardLength = cardWrapper.length;
console.log(cardWrapper.debug());
console.log(cardWrapper.length);
expect(cardLength).toEqual(1);
});

測試是否包含元素

要測試有沒有渲染元素用contains即可

1
2
3
4
5
it("contains 中風指數.", () => {
const cardWrapper = shallow(<Card />);
const isContains = cardWrapper.contains(<span>中風指數</span>);
expect(isContains).toBe(true);
});

群組測試

再來為了好管理,我們把相似的測試收納到describe內

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
describe('Card component', () => {
it("expect Card length equals to 1.", () => {
const cardWrapper = shallow(<Card />);
const cardLength = cardWrapper.length;
// console.log(cardWrapper.debug());
// console.log(cardWrapper.length);
expect(cardLength).toEqual(1);
});

it("contains 中風指數.", () => {
const cardWrapper = shallow(<Card />);
const isContains = cardWrapper.contains(<span>中風指數</span>);
expect(isContains).toBe(true);
});
});

測試Full rendering

剛剛只是在pure component上測試,如果是在外層一階寫測試呢?我們來試試看
我們先來寫基本的debug查看:

1
2
3
4
5
6
7
8
9
10
11
12
13
import React from "react";
import Enzyme, { mount } from "enzyme";
import Adapter from "enzyme-adapter-react-16";
import About from "./About";

Enzyme.configure({ adapter: new Adapter() });

describe("Page component", () => {
it("expects rendering full About page.", () => {
const wrapper = mount(<About />);
console.log(wrapper.debug());
});
});

你會看到它把所有的child都畫出來了,是一個Full rendering

再來我們寫一個testcase來更改input內的value看看DOM會不會改變
先用find去尋找element,找到之後使用simulate來模擬E2E的測試情境
這裡使用change來改變目標的值

1
2
3
4
5
6
7
8
it("tests good mood.", () => {
const wrapper = mount(<About />);
console.log('更改value前的DOM:', wrapper.debug());
wrapper.find('input').simulate('change', { target: { value: 0 } })
console.log('更改value後的DOM:', wrapper.debug());
const isGoodMood = wrapper.contains('爽啦 AAAAAAAAAAAAA');
expect(isGoodMood).toBe(true);
});

讓我們一起來觀察debug的變化,你會發現印出來的值
更改前:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
<About>
<div>
<Card name="改革家" value={80} handleCrazyValue={[Function: handleCrazyValue]}>
<div>
<p className="name">
玩家:
改革家
</p>
<div>
<span>
中風指數
</span>
<input value={80} onChange={[Function: handleCrazyValue]} />
</div>
</div>
</Card>
<Content value={80}>
<p>
⠄⠄⠄⠄⠄⠄⠄⠈⠉⠁⠈⠉⠉⠙⠿⣿⣿⣿⣿⣿
<br />
⠄⠄⠄⠄⠄⠄⠄⠄⣀⣀⣀⠄⠄⠄⠄⠄⠹⣿⣿⣿
<br />
⠄⠄⠄⠄⠄⢐⣲⣿⣿⣯⠭⠉⠙⠲⣄⡀⠄⠈⢿⣿
<br />
⠐⠄⠄⠰⠒⠚⢩⣉⠼⡟⠙⠛⠿⡟⣤⡳⡀⠄⠄⢻
<br />
⠄⠄⢀⣀⣀⣢⣶⣿⣦⣭⣤⣭⣵⣶⣿⣿⣏⠄⠄⣿
<br />
⠄⣼⣿⣿⣿⡉⣿⣀⣽⣸⣿⣿⣿⣿⣿⣿⣿⡆⣀⣿
<br />
⢠⣿⣿⣿⠿⠟⠛⠻⢿⠿⣿⣿⣿⣿⣿⣿⣿⣿⣶⣼
<br />
⠄⣿⣿⣿⡆⠄⠄⠄⠄⠳⡈⣿⣿⣿⣿⣿⣿⣿⣿⣿
<br />
⠄⢹⣿⣿⡇⠄⠄⠄⠄⢀⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿
<br />
⠄⠄⢿⣿⣷⣨⣽⣭⢁⣡⣿⣿⠟⣩⣿⣿⣿⠿⠿⠟
<br />
⠄⠄⠈⡍⠻⣿⣿⣿⣿⠟⠋⢁⣼⠿⠋⠉⠄⠄⠄⠄
<br />
⠄⠄⠄⠈⠴⢬⣙⣛⡥⠴⠂⠄⠄⠄⠄⠄⠄⠄⠄⠄
<br />
這什麼到底什麼網站Ohhohoho
<br />
⠄⠄⠄⠄⠄⠄你的使用者⠄⠄⠄⠄⠄⠄⠄
<br />
</p>
</Content>
</div>
</About>

更改後:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<About>
<div>
<Card name="改革家" value={0} handleCrazyValue={[Function: handleCrazyValue]}>
<div>
<p className="name">
玩家:
改革家
</p>
<div>
<span>
中風指數
</span>
<input value={0} onChange={[Function: handleCrazyValue]} />
</div>
</div>
</Card>
<Content value={0}>
<p>
爽啦 AAAAAAAAAAAAA
</p>
</Content>
</div>
</About>

最後,恭喜你走到這一步!東西有點多,不過一切都是值得的!

結論

相信做到這邊你應該也對測試有一些概念了,也不會害怕踏出寫測試的那一步了。
官方文件其實就描述得非常詳細,也有更多的範例可以做,相信搭配我提供的範例練習後,就對測試有基本的了解了。
如果擔心產品上線前出狀況的話,不仿可以研究看看測試如何寫。
不然,你會發現你客戶的表情變成……:

⠄⠄⠄⠄⠄⠄⠄⠈⠉⠁⠈⠉⠉⠙⠿⣿⣿⣿⣿⣿這
⠄⠄⠄⠄⠄⠄⠄⠄⣀⣀⣀⠄⠄⠄⠄⠄⠹⣿⣿⣿什
⠄⠄⠄⠄⠄⢐⣲⣿⣿⣯⠭⠉⠙⠲⣄⡀⠄⠈⢿⣿麼
⠐⠄⠄⠰⠒⠚⢩⣉⠼⡟⠙⠛⠿⡟⣤⡳⡀⠄⠄⢻到
⠄⠄⢀⣀⣀⣢⣶⣿⣦⣭⣤⣭⣵⣶⣿⣿⣏⠄⠄⣿底
⠄⣼⣿⣿⣿⡉⣿⣀⣽⣸⣿⣿⣿⣿⣿⣿⣿⡆⣀⣿什
⢠⣿⣿⣿⠿⠟⠛⠻⢿⠿⣿⣿⣿⣿⣿⣿⣿⣿⣶⣼麼
⠄⣿⣿⣿⡆⠄⠄⠄⠄⠳⡈⣿⣿⣿⣿⣿⣿⣿⣿⣿網
⠄⢹⣿⣿⡇⠄⠄⠄⠄⢀⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿站
⠄⠄⢿⣿⣷⣨⣽⣭⢁⣡⣿⣿⠟⣩⣿⣿⣿⠿⠿⠟哦
⠄⠄⠈⡍⠻⣿⣿⣿⣿⠟⠋⢁⣼⠿⠋⠉⠄⠄⠄⠄齁
⠄⠄⠄⠈⠴⢬⣙⣛⡥⠴⠂⠄⠄⠄⠄⠄⠄⠄⠄⠄齁
⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄齁

而當你開發產品發現很難找問題時,你的表情:
⠄⠄⠄⠄⠄⠄⠄⠈⠉⠁⠈⠉⠉⠙⠿⣿⣿⣿⣿⣿這
⠄⠄⠄⠄⠄⠄⠄⠄⣀⣀⣀⠄⠄⠄⠄⠄⠹⣿⣿⣿什
⠄⠄⠄⠄⠄⢐⣲⣿⣿⣯⠭⠉⠙⠲⣄⡀⠄⠈⢿⣿麼
⠐⠄⠄⠰⠒⠚⢩⣉⠼⡟⠙⠛⠿⡟⣤⡳⡀⠄⠄⢻到
⠄⠄⢀⣀⣀⣢⣶⣿⣦⣭⣤⣭⣵⣶⣿⣿⣏⠄⠄⣿底
⠄⣼⣿⣿⣿⡉⣿⣀⣽⣸⣿⣿⣿⣿⣿⣿⣿⡆⣀⣿什
⢠⣿⣿⣿⠿⠟⠛⠻⢿⠿⣿⣿⣿⣿⣿⣿⣿⣿⣶⣼麼
⠄⣿⣿⣿⡆⠄⠄⠄⠄⠳⡈⣿⣿⣿⣿⣿⣿⣿⣿⣿輸
⠄⢹⣿⣿⡇⠄⠄⠄⠄⢀⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿出
⠄⠄⢿⣿⣷⣨⣽⣭⢁⣡⣿⣿⠟⣩⣿⣿⣿⠿⠿⠟哦
⠄⠄⠈⡍⠻⣿⣿⣿⣿⠟⠋⢁⣼⠿⠋⠉⠄⠄⠄⠄齁
⠄⠄⠄⠈⠴⢬⣙⣛⡥⠴⠂⠄⠄⠄⠄⠄⠄⠄⠄⠄齁
⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄⠄齁

參考資料

十分鐘上手前端單元測試