# 先说一下示例怎么运行,先确定本机安装好 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 中À mesure que les applications Web deviennent de plus en plus complexes, de nombreuses entreprises accordent de plus en plus d'attention aux tests unitaires frontaux. La plupart des tutoriels que nous voyons parleront de l'importance des tests unitaires et de la façon d'utiliser certaines API de framework de test représentatif, mais comment démarrer les tests unitaires dans les projets réels? Quel contenu spécifique devrait inclure les cas de test?
Cet article commence à partir d'un scénario d'application réel, analyse ce que les tests unitaires doivent contenir à partir du modèle de conception et de la structure du code et comment rédiger des cas de test spécifiques. J'espère que vous pouvez gagner quelque chose des chaussures des enfants que vous voyez.
Le projet utilise react Technology Stack, et les principaux frameworks utilisés comprennent: react , redux , react-redux , redux-actions , reselect , redux-saga , seamless-immutable , antd .

Depuis la couche d'interface utilisateur, ce scénario d'application se compose principalement de deux parties:
Lorsque vous voyez des chaussures pour enfants ici, vous pouvez dire: Coupez! Une interface aussi simple et une logique commerciale est-elle toujours un scénario réel? Avez-vous toujours besoin d'écrire des tests unitaires Shenma?
Ne vous inquiétez pas, afin d'assurer l'expérience de lecture et la durée de l'article, une scène simple qui peut expliquer le problème clairement est une bonne scène, non? Regardez lentement.
Dans ce scénario, la conception et le développement, nous respectons strictement les meilleures pratiques redux flux de données et react-redux sens unique, et utilisons redux-saga pour traiter le flux commercial, reselect au traitement du cache d'état et appelez les interfaces de backend via fetch , ce qui n'est pas différent des projets réels.
L'organisation de conception et de code en couches est la suivante:

Le contenu du milieu du store est tous lié à redux , et vous devez connaître le sens en regardant le nom.
Pour des codes spécifiques, veuillez consulter ici.
Parlons d'abord de quels frameworks et outils de test sont utilisés, le contenu principal comprend:
jest , Test Frameworkenzyme , spécialisée dans le test de la couche d'interface utilisateur réagisinon possède une bibliothèque indépendante de faux, espions, talons et simulationsnock , simulez le serveur HTTPSi vous avez des chaussures pour enfants qui ne connaissent pas l'utilisation et la configuration ci-dessus, lisez simplement la documentation officielle, qui est meilleure que n'importe quel tutoriel.
Ensuite, nous commençons à écrire un code de cas de test spécifique. Ce qui suit donnera des extraits de code et des analyses pour chaque niveau. Commençons ensuite par actions .
Afin de rendre l'article aussi court et clair que possible, les extraits de code suivants ne sont pas le contenu complet de chaque fichier, et le contenu complet est ici.
Dans l'entreprise, j'ai utilisé redux-actions pour générer action . Ici, j'utilise la barre d'outils comme exemple. Regardons d'abord un code commercial:
import { createAction } from 'redux-actions' ;
import * as type from '../types/bizToolbar' ;
export const updateKeywords = createAction ( type . BIZ_TOOLBAR_KEYWORDS_UPDATE ) ;
// ... Pour les tests actions , nous vérifions principalement si l'objet action généré est correct:
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 ) ;
} ) ;
// ...
} ) ;La logique de ce cas de test est très simple. Tout d'abord, créez un résultat que nous attendons, puis appelez le code commercial et vérifiez enfin si le résultat de l'opération du code commercial est conforme aux attentes. Il s'agit de la routine de base des cas de test d'écriture.
Nous essayons de maintenir la responsabilité unique du cas d'utilisation lors de la rédaction de cas de test et ne couvrons pas trop de lunettes commerciales différentes. Il peut y avoir de nombreux cas de test, mais chacun ne doit pas être compliqué.
Ensuite, reducers , qui utilise toujours handleActions d' redux-actions pour écrire reducer . Voici un tableau par exemple:
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
) ;L'objet d'état ici utilise
seamless-immutable
Pour reducer , nous testons principalement deux aspects:
action.type inconnue. Type, si l'état actuel peut être renvoyé.Voici les codes de test pour les deux points ci-dessus:
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 ) ;
} ) ;
// ...
} ) ;La logique du cas de test ici est également très simple, et c'est toujours la routine d'affirmer le résultat attendu ci-dessus. Vous trouverez ci-dessous la partie des sélecteurs.
La fonction du selector est d'obtenir l'état de l'entreprise correspondante. reselect est utilisée ici pour se cacher pour éviter le recalcul lorsque state n'est pas modifié. Examinons d'abord le code sélecteur du tableau:
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 } ) ;
} ) ;Les paramètres du téléavertisseur ici sont définis uniformément dans le projet, donc la redélect fait bien ce travail: si le statut commercial reste inchangé, il reviendra directement au dernier cache. Les paramètres par défaut du périphérique de pagination sont les suivants:
export const pagination = {
size : 'small' ,
showTotal : ( total , range ) => ` ${ range [ 0 ] } - ${ range [ 1 ] } / ${ total } ` ,
pageSizeOptions : [ '15' , '25' , '40' , '60' ] ,
showSizeChanger : true ,
showQuickJumper : true
} ;Ensuite, nos tests sont principalement deux aspects:
Le code de test est le suivant:
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 ) ;
} ) ;
} ) ;Le cas de test est-il toujours très simple? Gardez ce rythme. Parlons de la partie légèrement compliquée, la partie des sagas.
Ici, j'utilise redux-saga pour traiter le flux commercial, qui est spécifiquement appelé API pour demander des données de manière asynchrone, traiter les résultats réussis et les résultats d'erreur, etc.
Certaines chaussures pour enfants peuvent penser que c'est tellement compliqué. N'est-il pas fini d'utiliser redux-thunk demandée de manière asynchrone? Ne vous inquiétez pas, vous comprendrez après l'avoir lu patiemment.
Ici, il est nécessaire d'introduire brièvement la méthode de travail de redux-saga . Saga est une fonction de générateur es6 - Générateur, que nous utilisons pour générer divers effects déclaratifs, qui sont digérés et traités par redux-saga pour stimuler les affaires.
Ici, nous jetons un œil au code commercial pour obtenir des données de table:
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 ( ) ) ;
}
} Si vous n'êtes pas familier avec redux-saga ne faites pas trop d'attention à l'écriture spécifique du code. Vous devez comprendre les étapes spécifiques de cette entreprise en examinant les commentaires:
state correspondant, et le sélecteur est appelé ici.Alors, comment devrions-nous rédiger des cas de test spécifiques? Nous savons tous que ce type de code commercial implique des appels d'API ou d'autres couches. Si vous souhaitez rédiger des tests unitaires, vous devez faire des simulations pour éviter que la couche API ne soit réellement appelée. Jetons un œil à la façon d'écrire des cas de test pour cette saga:
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 ) ;
} ) ; Ce cas de test est un peu plus compliqué que le précédent. Parlons d'abord du principe de test de la saga. Comme mentionné précédemment, Saga renvoie en fait divers effects déclaratifs et est ensuite réellement exécuté par le moteur. Par conséquent, le but de notre test est de voir si la génération d' effects répond aux attentes. L' effect est-il donc une chose magique? C'est en fait un objet littéral!
Nous pouvons générer ces objets littéraux de la même manière dans le code commercial. Les affirmations d'objets littéraux sont très simples, et il n'est pas nécessaire de se moquer sans appeler directement la couche API! L'étape de ce cas de test consiste à utiliser la fonction du générateur pour générer l' effect suivant étape par étape, puis affirmer et comparer.
Comme on peut le voir dans les commentaires 3 et 4 ci-dessus,
redux-sagafournit également des fonctions d'assistance pour gérer facilement les points d'arrêt des branches.
C'est aussi pourquoi j'ai choisi redux-saga : il est puissant et propice aux tests.
Vient ensuite la couche API liée. Comme mentionné précédemment, j'ai utilisé fetch pour appeler les demandes d'arrière-plan. J'ai encapsulé deux méthodes pour simplifier le traitement des appels et des résultats: getJSON() et postJSON() , correspondant respectivement aux demandes d'obtention et de poste. Jetons un coup d'œil au code de couche API:
import { fetcher } from '@/utils/fetcher' ;
export function getBizTableData ( payload ) {
return fetcher . postJSON ( '/api/biz/get-table' , payload ) ;
}Le code commercial est simple, donc les cas de test sont simples:
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 ) ;
} ) ;
} ) ; Étant donné que la couche API appelle directement la bibliothèque d'outils, nous utilisons ici sinon.stub() pour remplacer la bibliothèque d'outils pour réaliser des objectifs de test.
Ensuite, testez la bibliothèque d'outils Fetch que vous avez encapsulée. Ici, j'utilise isomorphic-fetch , j'ai donc choisi nock pour simuler le serveur pour les tests, testant principalement les résultats de l'accès normal et simule les exceptions du serveur, etc. Le fragment d'exemple est le suivant:
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 ) ;
} ) ;
// ...
} ) ; Fondamentalement, il n'y a rien de compliqué. L'essentiel est de noter que Fetch est un retour prometteur, et diverses solutions de test asynchrones de jest peuvent être très bien respectées.
Le reste est lié à l'interface utilisateur.
L'objectif principal du composant conteneur est de passer l'état et les actions. Regardez le code du composant de conteneur dans la barre d'outils:
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 ) ; Alors le but des cas de test est également de les vérifier. Ici, nous utilisons redux-mock-store pour simuler le magasin 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));
});
});
C'est très simple, donc il n'y a rien à dire.
Ici, nous prenons le composant de la table à titre d'exemple, et nous examinerons directement comment le cas de test est écrit. D'une manière générale, nous testons principalement les aspects suivants des composants de l'interface utilisateur:
Voici le code du cas de test:
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}});
});
});
Grâce à la rationalité de la stratification de conception, il est facile pour nous d'utiliser props de construction pour atteindre des objectifs de test. En combinant enzyme et sinon , les cas de test maintiennent toujours un rythme simple.
Ce qui précède est les idées de rédaction de cas de test complètes et le code d'exemple de ce scénario. Les idées et méthodes mentionnées dans l'article peuvent également être utilisées dans les projets Vue et Angular . Le contenu complet du code est ici (je dirai des choses plus importantes, je pense qu'il est facile de m'aider).
Enfin, nous pouvons utiliser le taux de couverture pour voir si le niveau de couverture du cas d'utilisation est suffisant (généralement, il n'est pas nécessaire de poursuivre délibérément 100%, selon la situation réelle):

Les tests unitaires sont la base du développement axé sur les tests TDD. D'après le processus ci-dessus, nous pouvons voir qu'une bonne hiérarchie de conception est facile à rédiger des cas de test, et les tests unitaires ne sont pas seulement pour assurer la qualité du code: vous obligera-t-il à réfléchir à la rationalité de la conception du code et à rejeter le code de nouilles?
Pour emprunter la conclusion du code propre:
En 2005, alors qu'il était assisté à la conférence Agility à Denver, Elisabeth Hedrickson m'a remis un bracelet vert similaire à celui vendu par Lance Armstrong. Le bracelet dit "tester obsédé". Je l'ai mis avec joie et je l'ai fièrement gardé. Je suis vraiment obsédé par le développement axé sur les tests depuis que j'ai appris le TDD de Kent Beck en 1999.
Mais quelque chose d'étrange s'est produit. Je me suis retrouvé incapable de retirer la sangle. Non seulement parce que le bracelet est très serré, mais c'est aussi un sort de resserrement spirituel. Ce bracelet est une déclaration de mon éthique de travail et un conseil pour mon engagement à faire de mon mieux pour écrire le meilleur code. Enlevez-le comme s'il était contre ces déclarations et promesses.
C'est donc toujours sur mon poignet. En écrivant du code, je l'ai vu du fond de mon œil. Cela me rappelait que j'ai fait une promesse d'écrire du code soigné.