# 先说一下示例怎么运行,先确定本机安装好 node 环境
# 安装项目依赖
npm install
# 首先启动 webpack-dev-server
npm run start-dev
# 上一个运行完毕后不要关闭,开一个新的命令行,启动 node server 服务
npm run start-server
#上述两个启动好后打开浏览器访问 http://localhost:3000 即可
# 跑测试用例
npm test
# 生成测试覆盖报告,跑完后看 coverage 子目录下的内容
npm run test-coverage
# 以上脚本定义都在 package.json 中Ketika aplikasi web menjadi semakin kompleks, banyak perusahaan lebih memperhatikan pengujian unit front-end. Sebagian besar tutorial yang kita lihat akan berbicara tentang pentingnya pengujian unit dan bagaimana menggunakan beberapa kerangka kerja tes representatif API, tetapi bagaimana memulai pengujian unit dalam proyek aktual? Konten spesifik apa yang harus dimasukkan oleh kasus pengujian?
Artikel ini dimulai dari skenario aplikasi nyata, menganalisis tes unit apa yang harus dikandung dari pola desain dan struktur kode, dan cara menulis kasus uji tertentu. Saya harap Anda bisa mendapatkan sesuatu dari sepatu anak -anak yang Anda lihat.
Proyek ini menggunakan tumpukan teknologi react , dan kerangka kerja utama yang digunakan meliputi: react , redux , react-redux , redux-actions , reselect , redux-saga , antd seamless-immutable .

Dari lapisan UI, skenario aplikasi ini terutama terdiri dari dua bagian:
Ketika Anda melihat sepatu anak -anak di sini, Anda dapat mengatakan: Potong! Apakah antarmuka dan logika bisnis yang sederhana masih merupakan skenario nyata? Apakah Anda masih perlu menulis tes unit shenma?
Jangan khawatir, untuk memastikan pengalaman membaca dan panjang artikel, adegan sederhana yang dapat menjelaskan masalahnya dengan jelas adalah adegan yang baik, bukan? Lihatlah ke bawah perlahan.
Dalam desain dan pengembangan skenario ini, kami benar-benar mematuhi praktik terbaik aliran data redux satu arah dan react-redux , dan menggunakan redux-saga untuk memproses aliran bisnis, reselect untuk memproses cache keadaan, dan memanggil antarmuka backend melalui fetch , yang tidak berbeda dari proyek nyata.
Organisasi desain dan kode berlapis adalah sebagai berikut:

Isi di store tengah semuanya terkait dengan redux , dan Anda harus tahu artinya dengan melihat namanya.
Untuk kode tertentu, silakan lihat di sini.
Mari kita bicara tentang kerangka kerja dan alat pengujian mana yang digunakan, konten utama meliputi:
jest , Test Frameworkenzyme , berspesialisasi dalam pengujian bereaksi lapisan UIsinon memiliki perpustakaan yang independen untuk memalsukan, mata -mata, potongan, dan tiruannock , simulasikan server httpJika Anda memiliki sepatu anak -anak yang tidak terbiasa dengan penggunaan dan konfigurasi di atas, cukup baca dokumentasi resmi, yang lebih baik daripada tutorial apa pun.
Selanjutnya, kami mulai menulis kode kasus uji tertentu. Berikut ini akan memberikan cuplikan kode dan parse untuk setiap level. Lalu mari kita mulai dengan actions .
Untuk membuat artikel sesingkat dan sejelas mungkin, cuplikan kode berikut bukan konten lengkap dari setiap file, dan konten lengkap ada di sini.
Dalam bisnis ini, saya menggunakan redux-actions untuk menghasilkan action . Di sini saya menggunakan toolbar sebagai contoh. Pertama -tama mari kita lihat sepotong kode bisnis:
import { createAction } from 'redux-actions' ;
import * as type from '../types/bizToolbar' ;
export const updateKeywords = createAction ( type . BIZ_TOOLBAR_KEYWORDS_UPDATE ) ;
// ... Untuk pengujian actions , kami terutama memverifikasi apakah objek action yang dihasilkan benar:
import * as type from '@/store/types/bizToolbar' ;
import * as actions from '@/store/actions/bizToolbar' ;
/* 测试 bizToolbar 相关 actions */
describe ( 'bizToolbar actions' , ( ) => {
/* 测试更新搜索关键字 */
test ( 'should create an action for update keywords' , ( ) => {
// 构建目标 action
const keywords = 'some keywords' ;
const expectedAction = {
type : type . BIZ_TOOLBAR_KEYWORDS_UPDATE ,
payload : keywords
} ;
// 断言 redux-actions 产生的 action 是否正确
expect ( actions . updateKeywords ( keywords ) ) . toEqual ( expectedAction ) ;
} ) ;
// ...
} ) ;Logika test case ini sangat sederhana. Pertama, bangun hasil yang kami harapkan, kemudian hubungi kode bisnis, dan akhirnya verifikasi apakah hasil operasi kode bisnis konsisten dengan harapan. Ini adalah rutinitas dasar kasus uji penulisan.
Kami mencoba menjaga tanggung jawab tunggal kasus penggunaan saat menulis kasus tes dan tidak mencakup terlalu banyak lingkup bisnis yang berbeda. Mungkin ada banyak kasus uji, tetapi masing -masing tidak boleh rumit.
Berikutnya adalah reducers , yang masih menggunakan handleActions redux-actions untuk menulis reducer . Berikut adalah tabel misalnya:
import { handleActions } from 'redux-actions' ;
import Immutable from 'seamless-immutable' ;
import * as type from '../types/bizTable' ;
/* 默认状态 */
export const defaultState = Immutable ( {
loading : false ,
pagination : {
current : 1 ,
pageSize : 15 ,
total : 0
} ,
data : [ ]
} ) ;
export default handleActions (
{
// ...
/* 处理获得数据成功 */
[ type . BIZ_TABLE_GET_RES_SUCCESS ] : ( state , { payload } ) => {
return state . merge (
{
loading : false ,
pagination : { total : payload . total } ,
data : payload . items
} ,
{ deep : true }
) ;
} ,
// ...
} ,
defaultState
) ;Objek negara di sini menggunakan
seamless-immutable
Untuk reducer , kami terutama menguji dua aspek:
action.type yang tidak diketahui. Type, apakah status saat ini dapat dikembalikan.Berikut adalah kode uji untuk dua poin di atas:
import * as type from '@/store/types/bizTable' ;
import reducer , { defaultState } from '@/store/reducers/bizTable' ;
/* 测试 bizTable reducer */
describe ( 'bizTable reducer' , ( ) => {
/* 测试未指定 state 参数情况下返回当前缺省 state */
test ( 'should return the default state' , ( ) => {
expect ( reducer ( undefined , { type : 'UNKNOWN' } ) ) . toEqual ( defaultState ) ;
} ) ;
// ...
/* 测试处理正常数据结果 */
test ( 'should handle successful data response' , ( ) => {
/* 模拟返回数据结果 */
const payload = {
items : [
{ id : 1 , code : '1' } ,
{ id : 2 , code : '2' }
] ,
total : 2
} ;
/* 期望返回的状态 */
const expectedState = defaultState
. setIn ( [ 'pagination' , 'total' ] , payload . total )
. set ( 'data' , payload . items )
. set ( 'loading' , false ) ;
expect (
reducer ( defaultState , {
type : type . BIZ_TABLE_GET_RES_SUCCESS ,
payload
} )
) . toEqual ( expectedState ) ;
} ) ;
// ...
} ) ;Logika test case di sini juga sangat sederhana, dan masih merupakan rutin untuk menegaskan hasil yang diharapkan di atas. Di bawah ini adalah bagian dari pemilih.
Fungsi selector adalah untuk mendapatkan status bisnis yang sesuai. reselect digunakan di sini untuk cache untuk mencegah penghitungan ulang ketika state tidak diubah. Mari pertama -tama lihat kode pemilih tabel:
import { createSelector } from 'reselect' ;
import * as defaultSettings from '@/utils/defaultSettingsUtil' ;
// ...
const getBizTableState = ( state ) => state . bizTable ;
export const getBizTable = createSelector ( getBizTableState , ( bizTable ) => {
return bizTable . merge ( {
pagination : defaultSettings . pagination
} , { deep : true } ) ;
} ) ;Parameter pager di sini ditetapkan secara seragam dalam proyek, jadi pilih kembali melakukan pekerjaan ini dengan baik: jika status bisnis tetap tidak berubah, itu akan langsung kembali ke cache terakhir. Pengaturan default perangkat paging adalah sebagai berikut:
export const pagination = {
size : 'small' ,
showTotal : ( total , range ) => ` ${ range [ 0 ] } - ${ range [ 1 ] } / ${ total } ` ,
pageSizeOptions : [ '15' , '25' , '40' , '60' ] ,
showSizeChanger : true ,
showQuickJumper : true
} ;Maka tes kami terutama dua aspek:
Kode tes adalah sebagai berikut:
import Immutable from 'seamless-immutable' ;
import { getBizTable } from '@/store/selectors' ;
import * as defaultSettingsUtil from '@/utils/defaultSettingsUtil' ;
/* 测试 bizTable selector */
describe ( 'bizTable selector' , ( ) => {
let state ;
beforeEach ( ( ) => {
state = createState ( ) ;
/* 每个用例执行前重置缓存计算次数 */
getBizTable . resetRecomputations ( ) ;
} ) ;
function createState ( ) {
return Immutable ( {
bizTable : {
loading : false ,
pagination : {
current : 1 ,
pageSize : 15 ,
total : 0
} ,
data : [ ]
}
} ) ;
}
/* 测试返回正确的 bizTable state */
test ( 'should return bizTable state' , ( ) => {
/* 业务状态 ok 的 */
expect ( getBizTable ( state ) ) . toMatchObject ( state . bizTable ) ;
/* 分页默认参数设置 ok 的 */
expect ( getBizTable ( state ) ) . toMatchObject ( {
pagination : defaultSettingsUtil . pagination
} ) ;
} ) ;
/* 测试 selector 缓存是否有效 */
test ( 'check memoization' , ( ) => {
getBizTable ( state ) ;
/* 第一次计算,缓存计算次数为 1 */
expect ( getBizTable . recomputations ( ) ) . toBe ( 1 ) ;
getBizTable ( state ) ;
/* 业务状态不变的情况下,缓存计算次数应该还是 1 */
expect ( getBizTable . recomputations ( ) ) . toBe ( 1 ) ;
const newState = state . setIn ( [ 'bizTable' , 'loading' ] , true ) ;
getBizTable ( newState ) ;
/* 业务状态改变了,缓存计算次数应该是 2 了 */
expect ( getBizTable . recomputations ( ) ) . toBe ( 2 ) ;
} ) ;
} ) ;Apakah test case masih sangat sederhana? Tetap laju ini. Mari kita bicara tentang bagian yang sedikit rumit, bagian Sagas.
Di sini saya menggunakan redux-saga untuk memproses aliran bisnis, yang secara khusus disebut API untuk meminta data secara tidak sinkron, memproses hasil yang berhasil dan hasil kesalahan, dll.
Beberapa sepatu anak -anak mungkin berpikir itu sangat rumit. Bukankah itu akan menggunakan redux-thunk yang diminta secara asinkron? Jangan khawatir, Anda akan mengerti setelah membacanya dengan sabar.
Di sini perlu secara singkat memperkenalkan metode kerja redux-saga . Saga adalah fungsi generator es6 - generator, yang kami gunakan untuk menghasilkan berbagai effects deklaratif, yang dicerna dan diproses oleh mesin redux-saga untuk mendorong bisnis.
Di sini kita melihat kode bisnis untuk mendapatkan data tabel:
import { all , takeLatest , put , select , call } from 'redux-saga/effects' ;
import * as type from '../types/bizTable' ;
import * as actions from '../actions/bizTable' ;
import { getBizToolbar , getBizTable } from '../selectors' ;
import * as api from '@/services/bizApi' ;
// ...
export function * onGetBizTableData ( ) {
/* 先获取 api 调用需要的参数:关键字、分页信息等 */
const { keywords } = yield select ( getBizToolbar ) ;
const { pagination } = yield select ( getBizTable ) ;
const payload = {
keywords ,
paging : {
skip : ( pagination . current - 1 ) * pagination . pageSize , max : pagination . pageSize
}
} ;
try {
/* 调用 api */
const result = yield call ( api . getBizTableData , payload ) ;
/* 正常返回 */
yield put ( actions . putBizTableDataSuccessResult ( result ) ) ;
} catch ( err ) {
/* 错误返回 */
yield put ( actions . putBizTableDataFailResult ( ) ) ;
}
} Jika Anda tidak terbiasa dengan redux-saga jangan terlalu memperhatikan penulisan kode tertentu. Anda harus memahami langkah -langkah spesifik dari bisnis ini dengan melihat komentar:
state yang sesuai, dan pemilih baru saja disebut di sini.Jadi bagaimana kita harus menulis kasus tes tertentu? Kita semua tahu bahwa kode bisnis semacam ini melibatkan panggilan dari API atau lapisan lain. Jika Anda ingin menulis tes unit, Anda harus melakukan beberapa tiruan untuk mencegah lapisan API benar -benar dipanggil. Mari kita lihat cara menulis kasus tes untuk kisah ini:
import { put , select } from 'redux-saga/effects' ;
// ...
/* 测试获取数据 */
test ( 'request data, check success and fail' , ( ) => {
/* 当前的业务状态 */
const state = {
bizToolbar : {
keywords : 'some keywords'
} ,
bizTable : {
pagination : {
current : 1 ,
pageSize : 15
}
}
} ;
const gen = cloneableGenerator ( saga . onGetBizTableData ) ( ) ;
/* 1. 是否调用了正确的 selector 来获得请求时要发送的参数 */
expect ( gen . next ( ) . value ) . toEqual ( select ( getBizToolbar ) ) ;
expect ( gen . next ( state . bizToolbar ) . value ) . toEqual ( select ( getBizTable ) ) ;
/* 2. 是否调用了 api 层 */
const callEffect = gen . next ( state . bizTable ) . value ;
expect ( callEffect [ 'CALL' ] . fn ) . toBe ( api . getBizTableData ) ;
/* 调用 api 层参数是否传递正确 */
expect ( callEffect [ 'CALL' ] . args [ 0 ] ) . toEqual ( {
keywords : 'some keywords' ,
paging : { skip : 0 , max : 15 }
} ) ;
/* 3. 模拟正确返回分支 */
const successBranch = gen . clone ( ) ;
const successRes = {
items : [
{ id : 1 , code : '1' } ,
{ id : 2 , code : '2' }
] ,
total : 2
} ;
expect ( successBranch . next ( successRes ) . value ) . toEqual (
put ( actions . putBizTableDataSuccessResult ( successRes ) ) ) ;
expect ( successBranch . next ( ) . done ) . toBe ( true ) ;
/* 4. 模拟错误返回分支 */
const failBranch = gen . clone ( ) ;
expect ( failBranch . throw ( new Error ( '模拟产生异常' ) ) . value ) . toEqual (
put ( actions . putBizTableDataFailResult ( ) ) ) ;
expect ( failBranch . next ( ) . done ) . toBe ( true ) ;
} ) ; Kasing uji ini sedikit lebih rumit dari yang sebelumnya. Mari kita bicara tentang prinsip pengujian saga terlebih dahulu. Seperti yang disebutkan sebelumnya, Saga sebenarnya mengembalikan berbagai effects deklaratif dan kemudian benar -benar dieksekusi oleh mesin. Oleh karena itu, tujuan tes kami adalah untuk melihat apakah generasi effects memenuhi harapan. Jadi apakah effect merupakan hal yang ajaib? Ini sebenarnya objek literal!
Kami dapat menghasilkan objek literal ini dengan cara yang sama dalam kode bisnis. Penegasan objek literal sangat sederhana, dan tidak perlu mengejek tanpa secara langsung memanggil lapisan API! Langkah dari kasus uji ini adalah menggunakan fungsi generator untuk menghasilkan effect selanjutnya langkah demi langkah, dan kemudian menegaskan dan membandingkan.
Seperti yang dapat dilihat dari komentar 3 dan 4 di atas,
redux-sagajuga menyediakan beberapa fungsi pembantu untuk dengan mudah menangani breakpoint cabang.
Ini juga mengapa saya memilih redux-saga : sangat kuat dan kondusif untuk pengujian.
Berikutnya adalah lapisan API yang terkait. Seperti yang disebutkan sebelumnya, saya menggunakan fetch untuk memanggil permintaan latar belakang. Saya merangkum dua metode untuk menyederhanakan pemrosesan panggilan dan hasil: getJSON() dan postJSON() , sesuai untuk masing -masing permintaan mendapatkan dan memposting. Mari kita lihat kode lapisan API:
import { fetcher } from '@/utils/fetcher' ;
export function getBizTableData ( payload ) {
return fetcher . postJSON ( '/api/biz/get-table' , payload ) ;
}Kode bisnis sederhana, jadi kasus uji sederhana:
import sinon from 'sinon' ;
import { fetcher } from '@/utils/fetcher' ;
import * as api from '@/services/bizApi' ;
/* 测试 bizApi */
describe ( 'bizApi' , ( ) => {
let fetcherStub ;
beforeAll ( ( ) => {
fetcherStub = sinon . stub ( fetcher ) ;
} ) ;
// ...
/* getBizTableData api 应该调用正确的 method 和传递正确的参数 */
test ( 'getBizTableData api should call postJSON with right params of fetcher' , ( ) => {
/* 模拟参数 */
const payload = { a : 1 , b : 2 } ;
api . getBizTableData ( payload ) ;
/* 检查是否调用了工具库 */
expect ( fetcherStub . postJSON . callCount ) . toBe ( 1 ) ;
/* 检查调用参数是否正确 */
expect ( fetcherStub . postJSON . lastCall . calledWith ( '/api/biz/get-table' , payload ) ) . toBe ( true ) ;
} ) ;
} ) ; Karena lapisan API secara langsung memanggil pustaka alat, di sini kami menggunakan sinon.stub() untuk mengganti pustaka alat untuk mencapai tujuan pengujian.
Selanjutnya, uji pustaka Alat Fetch yang Anda selenakan. Di sini saya menggunakan isomorphic-fetch , jadi saya memilih nock untuk mensimulasikan server untuk pengujian, terutama menguji hasil akses normal dan mensimulasikan pengecualian server, dll. Contoh fragmen adalah sebagai berikut:
import nock from 'nock' ;
import { fetcher , FetchError } from '@/utils/fetcher' ;
/* 测试 fetcher */
describe ( 'fetcher' , ( ) => {
afterEach ( ( ) => {
nock . cleanAll ( ) ;
} ) ;
afterAll ( ( ) => {
nock . restore ( ) ;
} ) ;
/* 测试 getJSON 获得正常数据 */
test ( 'should get success result' , ( ) => {
nock ( 'http://some' )
. get ( '/test' )
. reply ( 200 , { success : true , result : 'hello, world' } ) ;
return expect ( fetcher . getJSON ( 'http://some/test' ) ) . resolves . toMatch ( / ^hello.+$ / ) ;
} ) ;
// ...
/* 测试 getJSON 捕获 server 大于 400 的异常状态 */
test ( 'should catch server status: 400+' , ( done ) => {
const status = 500 ;
nock ( 'http://some' )
. get ( '/test' )
. reply ( status ) ;
fetcher . getJSON ( 'http://some/test' ) . catch ( ( error ) => {
expect ( error ) . toEqual ( expect . any ( FetchError ) ) ;
expect ( error ) . toHaveProperty ( 'detail' ) ;
expect ( error . detail . status ) . toBe ( status ) ;
done ( ) ;
} ) ;
} ) ;
/* 测试 getJSON 传递正确的 headers 和 query strings */
test ( 'check headers and query string of getJSON()' , ( ) => {
nock ( 'http://some' , {
reqheaders : {
'Accept' : 'application/json' ,
'authorization' : 'Basic Auth'
}
} )
. get ( '/test' )
. query ( { a : '123' , b : 456 } )
. reply ( 200 , { success : true , result : true } ) ;
const headers = new Headers ( ) ;
headers . append ( 'authorization' , 'Basic Auth' ) ;
return expect ( fetcher . getJSON (
'http://some/test' , { a : '123' , b : 456 } , headers ) ) . resolves . toBe ( true ) ;
} ) ;
// ...
} ) ; Pada dasarnya, tidak ada yang rumit. Yang utama adalah mencatat bahwa Fetch adalah pengembalian janji, dan berbagai solusi pengujian asinkron dari jest dapat dipenuhi dengan sangat baik.
Sisanya terkait dengan UI.
Tujuan utama komponen wadah adalah untuk lulus status dan tindakan. Lihatlah kode komponen kontainer di toolbar:
import { connect } from 'react-redux' ;
import { getBizToolbar } from '@/store/selectors' ;
import * as actions from '@/store/actions/bizToolbar' ;
import BizToolbar from '@/components/BizToolbar' ;
const mapStateToProps = ( state ) => ( {
... getBizToolbar ( state )
} ) ;
const mapDispatchToProps = {
reload : actions . reload ,
updateKeywords : actions . updateKeywords
} ;
export default connect ( mapStateToProps , mapDispatchToProps ) ( BizToolbar ) ; Maka tujuan dari kasus uji juga untuk memeriksa ini. Di sini kami menggunakan redux-mock-store untuk mensimulasikan Redux Store:
import React from 'react';
import { shallow } from 'enzyme';
import configureStore from 'redux-mock-store';
import BizToolbar from '@/containers/BizToolbar';
/* 测试容器组件 BizToolbar */
describe('BizToolbar container', () => {
const initialState = {
bizToolbar: {
keywords: 'some keywords'
}
};
const mockStore = configureStore();
let store;
let container;
beforeEach(() => {
store = mockStore(initialState);
container = shallow(<BizToolbar store={store}/>);
});
/* 测试 state 到 props 的映射是否正确 */
test('should pass state to props', () => {
const props = container.props();
expect(props).toHaveProperty('keywords', initialState.bizToolbar.keywords);
});
/* 测试 actions 到 props 的映射是否正确 */
test('should pass actions to props', () => {
const props = container.props();
expect(props).toHaveProperty('reload', expect.any(Function));
expect(props).toHaveProperty('updateKeywords', expect.any(Function));
});
});
Ini sangat sederhana, jadi tidak ada yang bisa dikatakan.
Di sini kita mengambil komponen tabel sebagai contoh, dan kita akan langsung melihat bagaimana test case ditulis. Secara umum, kami terutama menguji aspek -aspek berikut dari komponen UI:
Berikut adalah kode kasus uji:
import React from 'react';
import { mount } from 'enzyme';
import sinon from 'sinon';
import { Table } from 'antd';
import * as defaultSettingsUtil from '@/utils/defaultSettingsUtil';
import BizTable from '@/components/BizTable';
/* 测试 UI 组件 BizTable */
describe('BizTable component', () => {
const defaultProps = {
loading: false,
pagination: Object.assign({}, {
current: 1,
pageSize: 15,
total: 2
}, defaultSettingsUtil.pagination),
data: [{id: 1}, {id: 2}],
getData: sinon.fake(),
updateParams: sinon.fake()
};
let defaultWrapper;
beforeEach(() => {
defaultWrapper = mount(<BizTable {...defaultProps}/>);
});
// ...
/* 测试是否渲染了正确的功能子组件 */
test('should render table and pagination', () => {
/* 是否渲染了 Table 组件 */
expect(defaultWrapper.find(Table).exists()).toBe(true);
/* 是否渲染了 分页器 组件,样式是否正确(mini) */
expect(defaultWrapper.find('.ant-table-pagination.mini').exists()).toBe(true);
});
/* 测试首次加载时数据列表为空是否发起加载数据请求 */
test('when componentDidMount and data is empty, should getData', () => {
sinon.spy(BizTable.prototype, 'componentDidMount');
const props = Object.assign({}, defaultProps, {
pagination: Object.assign({}, {
current: 1,
pageSize: 15,
total: 0
}, defaultSettingsUtil.pagination),
data: []
});
const wrapper = mount(<BizTable {...props}/>);
expect(BizTable.prototype.componentDidMount.calledOnce).toBe(true);
expect(props.getData.calledOnce).toBe(true);
BizTable.prototype.componentDidMount.restore();
});
/* 测试 table 翻页后是否正确触发 updateParams */
test('when change pagination of table, should updateParams', () => {
const table = defaultWrapper.find(Table);
table.props().onChange({current: 2, pageSize: 25});
expect(defaultProps.updateParams.lastCall.args[0])
.toEqual({paging: {current: 2, pageSize: 25}});
});
});
Berkat rasionalitas stratifikasi desain, mudah bagi kami untuk menggunakan props konstruk untuk mencapai tujuan pengujian. Menggabungkan enzyme dan sinon , kasus uji masih mempertahankan ritme sederhana.
Di atas adalah ide penulisan test case lengkap dan kode sampel untuk skenario ini. Gagasan dan metode yang disebutkan dalam artikel ini juga dapat digunakan dalam proyek Vue dan Angular . Konten kode lengkap ada di sini (saya akan mengatakan hal -hal yang lebih penting, saya pikir mudah untuk membantu saya).
Akhirnya, kita dapat menggunakan tingkat cakupan untuk melihat apakah tingkat cakupan kasus penggunaan sudah cukup (umumnya, tidak perlu secara sengaja mengejar 100%, tergantung pada situasi aktual):

Pengujian unit adalah dasar dari pengembangan yang digerakkan oleh TDD. Dari proses di atas, kita dapat melihat bahwa hierarki desain yang baik mudah untuk menulis kasus uji, dan pengujian unit tidak hanya untuk memastikan kualitas kode: apakah itu memaksa Anda untuk memikirkan rasionalitas desain kode dan menolak kode mie?
Meminjam kesimpulan dari kode bersih:
Pada tahun 2005, saat menghadiri Konferensi Agility di Denver, Elisabeth Hedrickson memberi saya gelang hijau yang mirip dengan yang dijual oleh Lance Armstrong. Gelang mengatakan "Tes Terobsesi". Saya memakai dengan sukacita dan dengan bangga mempertahankannya. Saya benar-benar terobsesi dengan pengembangan yang didorong oleh tes sejak saya belajar tentang TDD dari Kent Beck pada tahun 1999.
Tapi sesuatu yang aneh terjadi. Saya mendapati diri saya tidak dapat menghapus tali. Bukan hanya karena gelangnya sangat ketat, tetapi juga mantra pengetatan spiritual. Gelang itu adalah deklarasi etika kerja saya dan tip untuk komitmen saya untuk melakukan yang terbaik untuk menulis kode terbaik. Lepaskan seolah -olah bertentangan dengan deklarasi dan janji -janji ini.
Jadi masih di pergelangan tangan saya. Saat menulis kode, saya melihatnya dari dasar mata saya. Itu terus mengingatkan saya bahwa saya berjanji untuk menulis kode yang rapi.