一篇文章學會 Vue 專案單元測試
關注微信公眾號“依賴注入”以獲得更好閱讀體驗。
這篇文章有三部分,閱讀完大概需要 10 分鐘(程式碼塊較多,建議使用電腦瀏覽)
一: 搭建基於 jest 的 vue 單元測試環境
二: 使用 vue-test-util 提高測試編碼效率
三: 複雜場景下的測試(模組,非同步,rxjs)
第一部分: 搭建基於 jest 的 vue 單元測試環境
因為 jest 包含了 karma + mocha + chai + sinon 的所有常用功能,零配置開箱即用,所以這個教程只講解 jest。
1.安裝依賴
npm install jest vue-jest babel-jest @vue/test-utils -D
2.編寫 jest 配置檔案
// 。/test/unit/jest。conf。js
const
path
=
require
(
‘path’
);
module
。
exports
=
{
rootDir
:
path
。
resolve
(
__dirname
,
‘。。/。。/’
),
// 類似 webpack。context
moduleFileExtensions
:
[
// 類似 webpack。resolve。extensions
‘js’
,
‘json’
,
‘vue’
,
],
moduleNameMapper
:
{
‘^@/(。*)$’
:
‘
,
// 類似 webpack。resolve。alias
},
transform
:
{
// 類似 webpack。module。rules
‘^。+\\。js$’
:
‘
,
‘。*\\。(vue)$’
:
‘
,
},
setupFiles
:
[
‘
],
// 類似 webpack。entry
coverageDirectory
:
‘
,
// 類似 webpack。output
collectCoverageFrom
:
[
// 類似 webpack 的 rule。include
‘src/**/*。{js,vue}’
,
‘!src/main。js’
,
‘!src/router/index。js’
,
‘!**/node_modules/**’
,
],
};
3.編寫啟動檔案 setup.js
// 。/test/unit/setup。js
import
Vue
from
‘vue’
;
Vue
。
config
。
productionTip
=
false
;
4.加入啟動 jest 的 npm script
“scripts”
:
{
“unit”
:
“jest ——config test/unit/jest。conf。js ——coverage”
,
}
,
5.編寫第一個測試檔案
有一個元件
// 。/src/components/hello-world/hello-world。vue
<
template
>
<
div
>
<
h1
>
{{
msg
}}
<
/h1>
<
/div>
<
/template>
<
script
>
export
default
{
name
:
‘HelloWorld’
,
data
()
{
return
{
msg
:
‘Hello Jest’
,
};
},
};
<
/script>
對該元件進行測試
// 。/src/components/hello-world/hello-world。spec。js
import
{
shallowMount
}
from
‘@vue/test-utils’
;
import
HelloWorld
from
‘。/hello-world’
;
describe
(
‘
,
()
=>
{
it
(
‘should render correct contents’
,
()
=>
{
const
wrapper
=
shallowMount
(
HelloWorld
);
expect
(
wrapper
。
find
(
‘。hello h1’
)。
text
())
。
toEqual
(
‘Welcome to Your Vue。js App’
);
});
});
6.啟動測試
npm run unit
jest 會自動掃描專案目錄下所有檔名以 。spec。js/。test。js 結尾的測試檔案,並執行測試用例。
最後最佳化一下測試編碼體驗
到上一步我們已經可以開始編寫測試程式碼了,但每次對程式碼的改動都需要手動執行 jest 啟動命令,沒有類似 hot-reload 的功能很難受。
可能你有一百種方式可以解決這個需求,但是我現在想告訴你一個最簡單且體驗最好的一種方式 -> 在 vscode 編輯器安裝一個名為
jest
的外掛
但是安裝後它可能還不能很好的工作,因為 vscode-jest 暫時並不知道我們的 jest 配置檔案在哪裡。
你可以選用下面任意一種方式解決這個問題:
1。 修改 vscode 配置檔案,將 jest。pathToConfig 指向我們剛才編寫的配置檔案
2。 將 jest 配置寫在 package。json 中的 jest 欄位
3。 將 jest 配置檔案提到專案根目錄,並且更名為 jest。config。js 或者 jest。json
現在 vs-code-jest 會根據 git 修改記錄自動執行應該執行的測試檔案,並在控制檯實時給出測試結果。至此第一部分大功告成。
第二部分:使用 vue-test-util 提高測試編碼效率
因為 vue-test-util 的官方文件寫的實在是太好了,不再贅述其 API,重點說明一點,為什麼推薦使用 vue-test-util 來編寫 Vue 元件單元測試,因為它不僅提供了很多實用的 API ,還同步了 DOM 的更新,也就是說我們的測試程式碼裡不會再充斥著 vm。$nextTick() 等程式碼,舉個例子
有一個元件
// 。/src/components/hello-world/hello-world。vue
<
template
>
<
div
>
<
h1
>
{{
msg
}}
<
/h1>
<
button
@
click
=
“onClick”
>
click
me
<
/button>
<
/div>
<
/template>
<
script
>
export
default
{
name
:
‘HelloWorld’
,
data
()
{
return
{
msg
:
‘Hello Jest’
,
};
},
methods
:
{
onClick
()
{
this
。
msg
=
‘new message’
;
},
},
};
<
/script>
對該元件進行測試
// 。/src/components/hello-world/hello-world。spec。js
import
{
shallowMount
}
from
‘@vue/test-utils’
;
import
HelloWorld
from
‘。/hello-world’
;
describe
(
‘
,
()
=>
{
const
wrapper
=
shallowMount
(
HelloWorld
);
it
(
“update ‘msg’ correctly”
,
()
=>
{
// 點選 button
wrapper
。
find
(
‘button’
)。
trigger
(
‘click’
);
// 可以立即獲取 msg 最新的值,不再需要 wrapper。vm。$nextTick();
expect
(
wrapper
。
find
(
‘h1’
)。
text
())
。
toEqual
(
‘new message’
);
});
});
如果需要做一些全域性的 vue-test-util 的配置,可以在 setup。js 裡指定,比如在每個元件例項化時候注入一個 GLOBAL 物件。
// 。/test/unit/setup。js
import
Vue
from
‘vue’
;
import
{
config
}
from
‘@vue/test-utils’
;
Vue
。
config
。
productionTip
=
false
;
// provide 的模擬
config
。
provide
。
GLOBAL
=
{
logined
:
false
,
};
有一個元件注入了 GLOBAL 物件
// 。/src/components/hello-world/hello-world。vue
<
template
>
<
div
>
<
h1
v
-
show
=
“GLOBAL。logined”
>
{{
msg
}}
<
/h1>
<
/div>
<
/template>
<
script
>
export
default
{
name
:
‘HelloWorld’
,
inject
:
[
‘GLOBAL’
],
data
()
{
return
{
msg
:
‘Hello Jest’
,
};
},
};
<
/script>
對該元件進行測試
// 。/src/components/hello-world/hello-world。spec。js
import
{
shallowMount
}
from
‘@vue/test-utils’
;
import
HelloWorld
from
‘。/hello-world’
;
describe
(
‘
,
()
=>
{
const
wrapper
=
shallowMount
(
HelloWorld
);
it
(
‘should render correct contents’
,
()
=>
{
expect
(
wrapper
。
find
(
‘h1’
)。
isVisible
())。
toBe
(
false
);
});
});
第三部分: 複雜場景下的測試
1.元件發起了API 請求,我只想知道它發沒發,不想讓它真實發出去。
有一個元件在會在 created 時候發起一個 http 請求
// 。/src/components/user-info/user-info。vue
<
template
>
<
div
class
=
“user-info”
>
<
div
class
=
“name”
>
{{
user
。
name
}}
<
/div>
<
div
class
=
“desc”
>
{{
user
。
desc
}}
<
/div>
<
/div>
<
/template>
<
script
>
import
UserApi
from
‘。。/。。/apis/user’
;
export
default
{
name
:
‘UserInfo’
,
data
()
{
return
{
user
:
{},
};
},
created
()
{
UserApi
。
getUserInfo
()
。
then
((
user
)
=>
{
this
。
user
=
user
;
});
},
};
<
/script>
API 介面如下
// 。/src/apis/user。js
function
getUserInfo
()
{
return
$http
。
get
(
‘/user’
);
}
export
default
{
getUserInfo
,
};
對該元件進行測試
// 。/src/components/user-info/user-info。spec。js
import
{
shallowMount
}
from
‘@vue/test-utils’
;
import
UserInfo
from
‘。/user-info’
;
import
UserApi
from
‘。。/。。/apis/user’
;
// mock 掉 user 模組
jest
。
mock
(
‘。。/。。/apis/user’
);
// 指定 getUserInfo 方法返回假資料
UserApi
。
getUserInfo
。
mockResolvedValue
({
name
:
‘olive’
,
desc
:
‘software engineer’
,
});
describe
(
‘
,
()
=>
{
const
wrapper
=
shallowMount
(
UserInfo
);
test
(
‘getUserInfo 有且只 call 了一次’
,
()
=>
{
expect
(
UserApi
。
getUserInfo
。
mock
。
calls
。
length
)。
toBe
(
1
);
});
it
(
‘使用者資訊渲染正確’
,
()
=>
{
expect
(
wrapper
。
find
(
‘。name’
)。
text
())。
toEqual
(
‘olive’
);
expect
(
wrapper
。
find
(
‘。desc’
)。
text
())。
toEqual
(
‘software engineer’
);
});
});
2.簡單的 A 元件依賴了一個複雜的 B 元件,但是我只想測試 A 的邏輯,不想拉起 B 的邏輯。
這種場景其實很常見,比如某些複雜元件 import 了某些會自執行的程式碼,這個時候為了保證單元測試的純粹,我們應該忽略掉所依賴的子元件的邏輯。
// 。/src/components/simple/simple。vue
<
template
>
<
div
>
<
div
class
=
“header”
>
{{
msg
}}
<
/div>
<
div
>
<
complex
><
/complex>
// 即使 vue-test-util 可以透過存根的方式將這個元件渲染為 complex-stub
// 但其內部的其他程式碼可能依然被執行
<
/div>
<
/div>
<
/template>
<
script
>
import
Complex
from
‘。/children/complex’
;
export
default
{
name
:
‘Simple’
,
data
()
{
return
{
msg
:
‘simple’
,
};
},
components
:
{
Complex
,
},
};
<
/script>
對該元件進行測試
// 。/src/components/simple/simple。spec。js
import
{
shallowMount
}
from
‘@vue/test-utils’
;
import
Simple
from
‘。/simple’
;
// 攔截掉 。vue 檔案的內容
jest
。
mock
(
‘。/children/complex。vue’
,
()
=>
({
render
(
h
)
{
h
();
},
}));
describe
(
‘
,
()
=>
{
const
wrapper
=
shallowMount
(
Simple
,
{
stubs
:
[
‘user-info’
],
});
it
(
‘文字渲染正確’
,
()
=>
{
expect
(
wrapper
。
find
(
‘。header’
)。
text
())。
toEqual
(
‘simple’
);
});
});
3. 測試 Rx.js
假設有一個 subject,訂閱的時候會發射 ‘hello-rx’
// 。/src/components/rx-demo/msg。stream。js
import
{
BehaviorSubject
}
from
‘rxjs’
;
const
msg$$
=
new
BehaviorSubject
(
‘hello-rx’
);
export
default
msg$$
;
有一個元件訂閱該 subject
// 。/src/components/rx-demo/rx-demo。vue
<
template
>
<
div
class
=
“rx-demo”
>
{{
msg
}}
<
/div>
<
/template>
<
script
>
import
msg$$
from
‘。/msg。stream’
;
export
default
{
name
:
‘RxDemo’
,
data
()
{
return
{
msg
:
‘’
,
};
},
created
()
{
msg$$
。
subscribe
((
res
)
=>
{
this
。
msg
=
res
;
});
},
};
<
/script>
對該元件進行測試
// 。/test/utils/index。js
import
{
BehaviorSubject
}
from
‘rxjs’
;
function
mockSubject
(
data
)
{
return
new
BehaviorSubject
(
data
);
}
export
{
mockSubject
,
};
// 。/src/components/rx-demo/rx-demo。spec。js
import
{
shallowMount
}
from
‘@vue/test-utils’
;
import
RxDemo
from
‘。/rx-demo’
;
jest
。
mock
(
‘。/msg。stream。js’
,
()
=>
{
//這一部分會被 babel-jest 提升到程式碼頂部,所以需要這麼動態去 require 才可以保證程式碼順序是正確的
const
{
mockSubject
}
=
require
(
‘。。/。。/。。/test/unit/util’
);
return
mockSubject
(
‘mock-data’
);
});
describe
(
‘
,
()
=>
{
it
(
‘Rx 訂閱成功,文字渲染正確’
,
()
=>
{
const
wrapper
=
shallowMount
(
RxDemo
);
expect
(
wrapper
。
find
(
‘。rx-demo’
)。
text
())。
toEqual
(
‘mock-data’
);
});
});
總得來說,把測試目標元件範圍外的不好測試的模組全部 mock 掉,然後再根據你們對單元測試要求的細粒度進行斷言。
其他簡單場景不再做贅述,遇到問題請隨時問我。謝謝。
參考資料:
專案地址
Vue Test Utils
Jest
Rxjs