# 先说一下示例怎么运行,先确定本机安装好 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 中Webアプリケーションがますます複雑になるにつれて、多くの企業はフロントエンドのユニットテストにますます注意を払います。私たちが見るチュートリアルのほとんどは、単体テストの重要性と、いくつかの代表的なテストフレームワークAPIを使用する方法について説明しますが、実際のプロジェクトで単体テストを開始する方法は?テストケースにはどのような特定のコンテンツを含める必要がありますか?
この記事は、実際のアプリケーションシナリオから始まり、デザインパターンとコード構造からどのユニットテストが含まれるべきか、および特定のテストケースの記述方法を分析します。私はあなたがあなたが見る子供の靴から何かを得ることができることを願っています。
プロジェクトはreactテクノロジースタックを使用し、使用される主なフレームワークには、 react 、 redux 、 react-redux 、 redux-actions 、 reselect 、 redux-saga 、 seamless-immutable 、 antd含まれます。

UIレイヤーから、このアプリケーションシナリオは主に2つの部分で構成されています。
ここに子供の靴を見たら、あなたは言うかもしれません:カット!このようなシンプルなインターフェースとビジネスロジックは、まだ実際のシナリオですか?あなたはまだシェンマの単位テストを書く必要がありますか?
心配しないでください。読書体験と記事の長さを確保するために、問題を説明できるシンプルなシーンは明らかに良いシーンですよね?ゆっくりと見下ろしてください。
このシナリオの設計と開発では、 redux一方向データフローとreact-reduxのベストプラクティスを厳密に順守し、 redux-sagaを使用してビジネスフローを処理し、状態キャッシュの処理にreselect 、実際のfetchと違いはありません。
階層化された設計とコード組織は次のとおりです。

中央のstoreの内容はすべてreduxに関連しており、名前を見ることで意味を知っている必要があります。
特定のコードについては、こちらをご覧ください。
まず、どのテストフレームワークとツールを使用するかについて説明しましょう。主なコンテンツには以下が含まれます。
jest 、テストフレームワークenzyme 、React UI層のテストに特化していますsinonは、偽物、スパイ、スタブ、モックの独立したライブラリがありますnock 、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 ) ;
} ) ;
// ...
} ) ;このテストケースのロジックは非常に簡単です。まず、予想される結果を作成し、ビジネスコードを呼び出し、最後にビジネスコードの操作結果が期待と一致しているかどうかを確認します。これは、テストケースを作成する基本的なルーチンです。
テストケースを書くときは、ユースケースを単一の責任を維持しようとしますが、あまりにも多くの異なるビジネススコープをカバーしません。多くのテストケースがありますが、それぞれが複雑であるべきではありません。
次は、 redux-actionsのhandleActions使用してreducerを書き込む還元reducersです。たとえば、こちらがテーブルです。
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の場合、主に2つの側面をテストします。
action.typeの場合、現在のステータスを返すことができるかどうか。上記の2つのポイントのテストコードは次のとおりです。
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の機能は、対応するビジネスのステータスを取得することです。ここでは、 stateが変更されない場合の再計算を防ぐためにキャッシュにreselectします。まず、テーブルのセレクターコードを見てみましょう。
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 } ) ;
} ) ;ここのポケットベルのパラメーターはプロジェクトで均一に設定されているため、再選択します。このジョブはうまく機能します。ビジネスステータスが変更されていない場合、最後のキャッシュに直接戻ります。ページングデバイスのデフォルト設定は次のとおりです。
export const pagination = {
size : 'small' ,
showTotal : ( total , range ) => ` ${ range [ 0 ] } - ${ range [ 1 ] } / ${ total } ` ,
pageSizeOptions : [ '15' , '25' , '40' , '60' ] ,
showSizeChanger : true ,
showQuickJumper : true
} ;次に、テストは主に2つの側面です。
テストコードは次のとおりです。
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 ) ;
} ) ;
} ) ;テストケースはまだ非常に簡単ですか?このペースを維持してください。わずかに複雑な部分、サガの部分について話しましょう。
ここでは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または他のレイヤーからの呼び出しが含まれることを知っています。ユニットテストを作成する場合は、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を最初にテストする原則について話しましょう。前述のように、SAGAは実際にさまざまな宣言的effectsを返し、実際にエンジンによって実行されます。したがって、私たちのテストの目的は、 effectsの生成が期待を満たしているかどうかを確認することです。それで、 effect魔法のことですか?それは実際には文字通りのオブジェクトです!
これらのリテラルオブジェクトをビジネスコードで同じ方法で生成できます。文字通りのオブジェクトのアサーションは非常にシンプルであり、APIレイヤーを直接呼び出すことなくock笑する必要はありません!このテストケースのステップは、ジェネレーター関数を使用して次のeffect段階的に生成し、主張して比較することです。
上記のコメント3と4からわかるように、
redux-saga、ブランチブレークポイントを簡単に処理するためのヘルパー機能も提供します。
これが私がredux-sagaを選んだ理由でもあります。それは強力でテストを助長します。
次はAPIレイヤーに関連しています。前述のように、 fetchを使用してバックグラウンドリクエストを呼び出しました。コールと結果の処理を簡素化するための2つの方法をカプセル化しました: getJSON()とpostJSON() 。 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のさまざまな非同期検査ソリューションを非常によく満たすことができることに注意することです。
残りはUIに関連しています。
コンテナコンポーネントの主な目的は、状態とアクションを渡すことです。ツールバーのコンテナコンポーネントコードを見てください。
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));
});
});
とてもシンプルなので、何も言うことはありません。
ここでは、テーブルコンポーネントを例として取ります。テストケースの書き込み方法を直接調べます。一般的に言えば、主にUIコンポーネントの次の側面をテストします。
これがテストケースコードです:
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年、デンバーで開催されたアジリティカンファレンスに出席している間、エリザベスヘドリクソンは、ランスアームストロングが販売したものと同様のグリーンリストバンドを渡しました。リストバンドは「テストに取りつかれている」と言っています。私はそれを喜びでつけて、誇らしげにそれを続けました。 1999年にKent BeckからTDDについて学んだ以来、私はテスト駆動型の開発に本当に夢中になっています。
しかし、奇妙なことが起こりました。ストラップを取り外すことができないことに気付きました。リストバンドが非常にタイトであるだけでなく、精神的な引き締め呪文でもあるからです。そのリストバンドは、私の仕事の倫理の宣言であり、最高のコードを書くために最善を尽くすという私のコミットメントのヒントです。これらの宣言と約束に反するかのようにそれを脱いでください。
それで、それはまだ私の手首にあります。コードを書いている間、私は目の底からそれを見ました。きちんとしたコードを書くことを約束したことを思い出させ続けました。