# 先说一下示例怎么运行,先确定本机安装好 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 中عندما تصبح تطبيقات الويب أكثر تعقيدًا ، تولي العديد من الشركات المزيد والمزيد من الاهتمام لاختبار الوحدة الأمامية. ستتحدث معظم البرامج التعليمية التي نراها عن أهمية اختبار الوحدة وكيفية استخدام بعض واجهات برمجة التطبيقات الإطار التمثيلية ، ولكن كيفية بدء اختبار الوحدة في المشاريع الفعلية؟ ما هو المحتوى المحدد الذي يجب أن يتضمنه حالات الاختبار؟
تبدأ هذه المقالة من سيناريو التطبيق الحقيقي ، وتحليل اختبارات الوحدة التي يجب أن تحتوي عليها من نمط التصميم وهيكل الرمز ، وكيفية كتابة حالات اختبار محددة. آمل أن تتمكن من الحصول على شيء من أحذية الأطفال التي تراها.
يستخدم المشروع مكدس تقنية react ، وتشمل الأطر الرئيسية المستخدمة: react ، redux ، react-redux ، redux-actions ، reselect ، redux-saga ، seamless-immutable ، antd .

من طبقة واجهة المستخدم ، يتكون سيناريو التطبيق هذا بشكل أساسي من جزأين:
عندما ترى بعض أحذية الأطفال هنا ، قد تقول: قطع! هل لا تزال مثل هذه الواجهة البسيطة ومنطق الأعمال سيناريو حقيقي؟ هل ما زلت بحاجة إلى كتابة اختبارات وحدة Shenma؟
لا تقلق ، من أجل ضمان تجربة القراءة وطولها للمقال ، مشهد بسيط يمكن أن يفسر المشكلة بوضوح هو مشهد جيد ، أليس كذلك؟ انظر ببطء.
في هذا السيناريو ، التصميم وتطويره ، نلتزم بصرامة بأفضل الممارسات redux البيانات في اتجاه واحد react-redux ، ونستخدم redux-saga لمعالجة تدفق العمل ، reselect لمعالجة ذاكرة التخزين المؤقت للحالة ، والاستدعاء واجهات الواجهة الخلفية من خلال fetch ، والتي لا تختلف عن المشاريع الحقيقية.
تنظيم التصميم والرمز الطبقات على النحو التالي:

ترتبط المحتويات في store الأوسط بـ redux ، ويجب أن تعرف المعنى من خلال النظر إلى الاسم.
للحصول على رموز محددة ، يرجى الاطلاع هنا.
دعنا نتحدث أولاً عن أطر الاختبار والأدوات التي يتم استخدامها ، وتشمل المحتويات الرئيسية:
jest ، إطار اختبارenzyme ، متخصص في اختبار طبقة واجهة المستخدمsinon لديها مكتبة مستقلة من Fakes و Spies و Stubs و Mocksnock ، محاكاة خادم HTTPإذا كان لديك أحذية أطفال غير على دراية بالاستخدام والتكوين أعلاه ، فما عليك سوى قراءة الوثائق الرسمية ، والتي تكون أفضل من أي تعليمي.
بعد ذلك ، نبدأ في كتابة رمز حالة اختبار محدد. سيعطي ما يلي قصاصات الرمز والمتوسطة لكل مستوى. ثم لنبدأ actions .
من أجل جعل المقالة قصيرة وواضحة قدر الإمكان ، فإن مقتطفات الكود التالية ليست المحتوى الكامل لكل ملف ، والمحتوى الكامل موجود هنا.
في الأعمال التجارية ، استخدمت redux-actions لإنشاء action . هنا أستخدم شريط الأدوات كمثال. دعونا أولاً نلقي نظرة على رمز العمل:
import { createAction } from 'redux-actions' ;
import * as type from '../types/bizToolbar' ;
export const updateKeywords = createAction ( type . BIZ_TOOLBAR_KEYWORDS_UPDATE ) ;
// ... لاختبار actions ، نتحقق بشكل أساسي مما إذا كان كائن action الذي تم إنشاؤه صحيحًا:
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 ) ;
} ) ;
// ...
} ) ;منطق حالة الاختبار هذه بسيطة للغاية. أولاً ، قم ببناء نتيجة نتوقعها ، ثم اتصل برمز العمل ، ثم تحقق أخيرًا مما إذا كانت نتيجة تشغيل رمز العمل تتفق مع التوقعات. هذا هو الروتين الأساسي لحالات اختبار الكتابة.
نحاول الحفاظ على مسؤولية حالة الاستخدام عند كتابة حالات الاختبار ولا تغطي الكثير من النطاقات التجارية المختلفة. يمكن أن يكون هناك العديد من حالات الاختبار ، ولكن لا ينبغي أن يكون كل منها معقدًا.
التالي هو reducers ، التي لا تزال تستخدم handleActions من redux-actions للكتابة لكتابة reducer . هنا جدول على سبيل المثال:
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
) ;يستخدم كائن الحالة هنا
seamless-immutable
reducer ، نختبر بشكل أساسي جانبين:
action.type غير معروف. النوع ، ما إذا كان يمكن إرجاع الوضع الحالي.فيما يلي رموز الاختبار للنقطتين أعلاه:
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 ) ;
} ) ;
// ...
} ) ;منطق حالة الاختبار هنا بسيط للغاية ، ولا يزال روتينًا لتأكيد النتيجة المتوقعة أعلاه. فيما يلي جزء من المختارين.
وظيفة selector هي الحصول على حالة الأعمال المقابلة. يتم استخدام reselect هنا لمنع ذاكرة التخزين المؤقت لمنع إعادة حساب عندما لا يتم تغيير state . دعونا أولاً نلقي نظرة على رمز المحدد للجدول:
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 } ) ;
} ) ;يتم تعيين معلمات Pager هنا بشكل موحد في المشروع ، لذلك Reselect تقوم بهذه المهمة بشكل جيد: إذا بقيت حالة العمل دون تغيير ، فسيعود مباشرة إلى ذاكرة التخزين المؤقت الأخيرة. الإعدادات الافتراضية لجهاز الترحيل هي كما يلي:
export const pagination = {
size : 'small' ,
showTotal : ( total , range ) => ` ${ range [ 0 ] } - ${ range [ 1 ] } / ${ total } ` ,
pageSizeOptions : [ '15' , '25' , '40' , '60' ] ,
showSizeChanger : true ,
showQuickJumper : true
} ;ثم اختباراتنا هي في الأساس جانبان:
رمز الاختبار كما يلي:
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 ) ;
} ) ;
} ) ;هل لا تزال حالة الاختبار بسيطة للغاية؟ فقط حافظ على هذه الوتيرة. دعنا نتحدث عن الجزء المعقد قليلاً ، الجزء Sagas.
أنا هنا أستخدم redux-saga لمعالجة تدفق الأعمال ، والذي يسمى خصيصًا لـ API لطلب البيانات بشكل غير متزامن ، معالجة النتائج الناجحة ونتائج الخطأ ، إلخ.
قد يعتقد بعض أحذية الأطفال أنها معقدة للغاية. ألم يكن الأمر قد انتهى لاستخدام redux-thunk المطلوب بشكل غير متزامن؟ لا تقلق ، سوف تفهم بعد قراءته بصبر.
هنا من الضروري تقديم طريقة عمل redux-saga بإيجاز. SAGA هي وظيفة مولد es6 - المولد ، الذي نستخدمه لإنشاء effects إعلانية مختلفة ، والتي يتم هضمها ومعالجتها بواسطة محرك redux-saga لقيادة الأعمال.
هنا نلقي نظرة على رمز العمل للحصول على بيانات الجدول:
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 ( ) ) ;
}
} إذا لم تكن على دراية بـ redux-saga فلا تولي الكثير من الاهتمام للكتابة المحددة للرمز. يجب أن تفهم الخطوات المحددة لهذا العمل من خلال النظر في التعليقات:
state المقابلة ، ويتم استدعاء المحدد الآن هنا.فكيف يجب أن نكتب حالات اختبار محددة؟ نعلم جميعًا أن هذا النوع من رمز العمل يتضمن مكالمات من API أو طبقات أخرى. إذا كنت ترغب في كتابة اختبارات الوحدة ، فيجب عليك القيام ببعض السخرية لمنع استدعاء طبقة API بالفعل. دعونا نلقي نظرة على كيفية كتابة حالات الاختبار لهذا الملحمة:
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 ) ;
} ) ; حالة الاختبار هذه أكثر تعقيدًا قليلاً من الحالة السابقة. دعنا نتحدث عن مبدأ اختبار الملحمة أولاً. كما ذكرنا سابقًا ، فإن SAGA يعيد فعليًا effects إعلانية مختلفة ثم يتم تنفيذه بالفعل بواسطة المحرك. لذلك ، فإن الغرض من اختبارنا هو معرفة ما إذا كان توليد effects يفي بالتوقعات. فهل effect سحري؟ إنه في الواقع كائن حرفي!
يمكننا إنشاء هذه الكائنات الحرفية بنفس الطريقة في رمز العمل. تأكيدات الأشياء الحرفية بسيطة للغاية ، وليس هناك حاجة للسخرية دون استدعاء طبقة API مباشرة! تتمثل خطوة حالة الاختبار هذه في استخدام وظيفة المولد لإنشاء effect التالي خطوة بخطوة ، ثم تأكيد ومقارنة.
كما يتضح من التعليقات 3 و 4 أعلاه ، يوفر
redux-sagaأيضًا بعض وظائف المساعد للتعامل بسهولة مع نقاط التوقف الفرعية.
ولهذا السبب اخترت redux-saga : إنه قوي ومفضّل للاختبار.
التالي هو طبقة API ذات الصلة. كما ذكرنا سابقًا ، استخدمت fetch لاستدعاء طلبات الخلفية. لقد قمت بتغليف طريقتين لتبسيط معالجة المكالمات والنتائج: getJSON() و postJSON() ، المقابلة لطلبات الحصول على و Post على التوالي. لنلقي نظرة على رمز طبقة API:
import { fetcher } from '@/utils/fetcher' ;
export function getBizTableData ( payload ) {
return fetcher . postJSON ( '/api/biz/get-table' , payload ) ;
}رمز العمل بسيط ، لذا فإن حالات الاختبار بسيطة:
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 ) ;
} ) ;
} ) ; نظرًا لأن طبقة API تستدعي مباشرة مكتبة الأدوات ، فإننا نستخدم هنا sinon.stub() لاستبدال مكتبة الأدوات لتحقيق أغراض الاختبار.
بعد ذلك ، اختبر مكتبة أداة الجلب التي تغليفها. هنا أستخدم isomorphic-fetch ، لذلك اخترت nock لمحاكاة الخادم للاختبار ، واختبار نتائج استثناءات الوصول العادية ومحاكاة الخادم ، وما إلى ذلك.
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 ) ;
} ) ;
// ...
} ) ; في الأساس ، لا يوجد شيء معقد. والشيء الرئيسي هو ملاحظة أن الجلب هو عودة الوعد ، ويمكن أن يتم الوفاء بحلول الاختبار غير المتزامنة المختلفة لـ jest بشكل جيد.
يرتبط الباقي بواجهة المستخدم.
الغرض الرئيسي من مكون الحاوية هو تمرير الحالة والإجراءات. انظر إلى رمز مكون الحاوية في شريط الأدوات:
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 ) ; ثم الغرض من حالات الاختبار هو أيضا التحقق من هذه. هنا نستخدم redux-mock-store لمحاكاة متجر Redux:
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));
});
});
الأمر بسيط للغاية ، لذلك لا يوجد شيء يمكن قوله.
هنا نأخذ مكون الجدول كمثال ، وسننظر مباشرة في كيفية كتابة حالة الاختبار. بشكل عام ، نختبر بشكل أساسي الجوانب التالية لمكونات واجهة المستخدم:
هنا رمز حالة الاختبار:
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}});
});
});
بفضل عقلانية التقسيم الطبقي للتصميم ، من السهل علينا استخدام props البناء لتحقيق أغراض الاختبار. الجمع بين enzyme sinon ، لا تزال حالات الاختبار تحافظ على إيقاع بسيط.
ما سبق هو أفكار كتابة حالة الاختبار الكاملة وعينة رمز لهذا السيناريو. يمكن أيضًا استخدام الأفكار والأساليب المذكورة في المقالة في مشاريع Vue و Angular . محتوى الكود الكامل هنا (سأقول أشياء أكثر أهمية ، وأعتقد أنه من السهل مساعدتي).
أخيرًا ، يمكننا استخدام معدل التغطية لمعرفة ما إذا كان مستوى التغطية لحالة الاستخدام كافية (عمومًا ، ليست هناك حاجة لمتابعة 100 ٪ عن عمد ، اعتمادًا على الموقف الفعلي):

اختبار الوحدة هو أساس التنمية التي تعتمد على اختبار TDD. من العملية أعلاه ، يمكننا أن نرى أن التسلسل الهرمي للتصميم الجيد سهل كتابة حالات الاختبار ، وأن اختبار الوحدة ليس فقط لضمان جودة الكود: هل ستجبرك على التفكير في عقلانية تصميم الرمز ورفض رمز المعكرونة؟
لاقتراض انتهاء الكود النظيف:
في عام 2005 ، أثناء حضوره مؤتمر الرشاقة في دنفر ، سلمتني إليزابيث هيدريكسون معصمًا أخضرًا يشبه إلى Lance Armstrong. يقول المعصم "اختبار مهووس". لقد وضعته بفرح وأبقيته بفخر. لقد كنت مهووسًا حقًا بالتنمية التي تعتمد على الاختبار منذ أن علمت عن TDD من Kent Beck في عام 1999.
ولكن حدث شيء غريب. لقد وجدت نفسي غير قادر على إزالة الشريط. ليس فقط لأن المعصم ضيقة للغاية ، ولكنه أيضًا تعويذة شد روحي. هذا المعصم هو إعلان عن أخلاقيات عملي ونصيحة لالتزاماتي ببذل قصارى جهدي لكتابة أفضل رمز. خلعه كما لو كان ضد هذه التصريحات والوعود.
لذلك لا يزال على معصمي. أثناء كتابة الكود ، رأيته من أسفل عيني. ظل يذكرني بأنني وعد بكتابة رمز أنيق.