保证工程质量
平衡投入产出的手段
保证工程质量
平衡投入产出的手段
/** parse.test.js */ import { parseMemoryRequirement } from './parse' if (parseMemoryRequirement(42) !== 42) { throw new Error('parse(42) !== 42') } if (parseMemoryRequirement('1Ki') !== 1024) { throw new Error('parse(\'1Ki\') !== 1024') } // ... more test cases console.log('Perfect!')
/** parse.test.js */ import { parseMemoryRequirement } from './parse' if (parseMemoryRequirement(42) !== 42) { throw new Error('parse(42) !== 42') } if (parseMemoryRequirement('1Ki') !== 1024) { throw new Error('parse(\'1Ki\') !== 1024') } // ... more test cases console.log('Perfect!')
/** parse.js **/ exports.parseMemoryRequirement = (input) => { // ... implementation }
/** parse.js **/ exports.parseMemoryRequirement = (input) => { // ... implementation }
$ node ./parse.test.js > Perfect!
$ node ./parse.test.js > Perfect!
test('Parse requirement with number', (t) => { t.is(parseMemoryRequirement(42), 42) })
test('Parse requirement with number', (t) => { t.is(parseMemoryRequirement(42), 42) })
test( 'Parse requirement with number', (t) => { const actual = parseMemoryRequirement(42) t.is(actual, 42) } )
test( 'Parse requirement with number', (t) => { const actual = parseMemoryRequirement(42) t.is(actual, 42) } )
Test Driven Development
重复
if (stauts !== 'ok') { throw new Error('Invalid status') }
if (stauts !== 'ok') { throw new Error('Invalid status') }
const assert = require('assert') assert(status === 'ok', 'Invalid status')
const assert = require('assert') assert(status === 'ok', 'Invalid status')
const test = require('ava') test('foo', async (t) => { const respoonse = await fetch('/user') t.is(response.data?.status === 'ok', 'Invalid Status') })
const test = require('ava') test('foo', async (t) => { const respoonse = await fetch('/user') t.is(response.data?.status === 'ok', 'Invalid Status') })
var assert = chai.assert; assert.typeOf(foo, 'string'); assert.equal(foo, 'bar'); assert.lengthOf(foo, 3) assert.property(tea, 'flavors'); assert.lengthOf(tea.flavors, 3);
var assert = chai.assert; assert.typeOf(foo, 'string'); assert.equal(foo, 'bar'); assert.lengthOf(foo, 3) assert.property(tea, 'flavors'); assert.lengthOf(tea.flavors, 3);
chai.should(); foo.should.be.a('string'); foo.should.equal('bar'); foo.should.have.lengthOf(3); tea.should.have.property('flavors') .with.lengthOf(3);
chai.should(); foo.should.be.a('string'); foo.should.equal('bar'); foo.should.have.lengthOf(3); tea.should.have.property('flavors') .with.lengthOf(3);
var expect = chai.expect; expect(foo).to.be.a('string'); expect(foo).to.equal('bar'); expect(foo).to.have.lengthOf(3); expect(tea).to.have.property('flavors') .with.lengthOf(3);
var expect = chai.expect; expect(foo).to.be.a('string'); expect(foo).to.equal('bar'); expect(foo).to.have.lengthOf(3); expect(tea).to.have.property('flavors') .with.lengthOf(3);
import test from 'ava' import sinon from 'sinon' import { PubSub } from '..' test("PubSub should call subscribers on publish", (t) => { const callback = sinon.spy() PubSub.subscribe("message", callback) PubSub.publishSync("message") t.true(callback.called) });
import test from 'ava' import sinon from 'sinon' import { PubSub } from '..' test("PubSub should call subscribers on publish", (t) => { const callback = sinon.spy() PubSub.subscribe("message", callback) PubSub.publishSync("message") t.true(callback.called) });
import axios from 'axios' export const getUsers = async () => { const response = await axios.get('/users') return response.data.map((user) => ({ first: user.firstname, last: user.lastname, })) }
import axios from 'axios' export const getUsers = async () => { const response = await axios.get('/users') return response.data.map((user) => ({ first: user.firstname, last: user.lastname, })) }
import test from 'ava' import sinon from 'sinon' import axios from 'axios' import { getUsers } from '..' test("getUsers should resolves users list", async (t) => { const get = sinon.stub(axios, 'get').resolves([ { firstname: 'Reeson', lastname: 'Shearsmith' }, { firstname: 'Steve', lastname: 'Pemberton' }, ]); const users = await getUsers() t.deepEqual(users, [ { first: 'Reeson', last: 'Shearsmith' }, { first: 'Steve', last: 'Pemberton' }, ]) get.restore(); });
import test from 'ava' import sinon from 'sinon' import axios from 'axios' import { getUsers } from '..' test("getUsers should resolves users list", async (t) => { const get = sinon.stub(axios, 'get').resolves([ { firstname: 'Reeson', lastname: 'Shearsmith' }, { firstname: 'Steve', lastname: 'Pemberton' }, ]); const users = await getUsers() t.deepEqual(users, [ { first: 'Reeson', last: 'Shearsmith' }, { first: 'Steve', last: 'Pemberton' }, ]) get.restore(); });
const delay = (ms) => new Promise(resolve => { setTimeout(() => resolve(), ms) }) const combo = async (hit) => { hit({ at: new Date() }) await delay(100) hit({ at: new Date() }) } test.beforeEach((t) => { t.context.clock = sinon.useFakeTimers() }) test.afterEach((T) => { t.context.clock.restore() }) test('Combo should hit twice in 100ms', async (t) => { const hit = sinon.spy() combo(hit) await t.context.clock.tickAsync(200) t.is(hit.firstCall.firstArg.at, 0) t.is(hit.secondCall.firstArg.at, 100) })
const delay = (ms) => new Promise(resolve => { setTimeout(() => resolve(), ms) }) const combo = async (hit) => { hit({ at: new Date() }) await delay(100) hit({ at: new Date() }) } test.beforeEach((t) => { t.context.clock = sinon.useFakeTimers() }) test.afterEach((T) => { t.context.clock.restore() }) test('Combo should hit twice in 100ms', async (t) => { const hit = sinon.spy() combo(hit) await t.context.clock.tickAsync(200) t.is(hit.firstCall.firstArg.at, 0) t.is(hit.secondCall.firstArg.at, 100) })
import { getUsers } from '..' test.beforeEach((t) => { t.context.server = sinon.fakeServer.create() }) test.afterEach((T) => { t.context.server.restore() }) test('Combo should hit twice in 100ms', async (t) => { t.context.server.respondWith('GET', '/users', 200, { 'Content-Type': 'application/json' }, JSON.stringify([ { firstname: 'Reeson', lastname: 'Shearsmith' }, ]) ) const callback = sinon.spy() getUsers.then(callback) t.context.server.respond() t.deepEqual(callback.lastCall.firstArg, [ { first: 'Reeson', last: 'Shearsmith' }, ])) })
import { getUsers } from '..' test.beforeEach((t) => { t.context.server = sinon.fakeServer.create() }) test.afterEach((T) => { t.context.server.restore() }) test('Combo should hit twice in 100ms', async (t) => { t.context.server.respondWith('GET', '/users', 200, { 'Content-Type': 'application/json' }, JSON.stringify([ { firstname: 'Reeson', lastname: 'Shearsmith' }, ]) ) const callback = sinon.spy() getUsers.then(callback) t.context.server.respond() t.deepEqual(callback.lastCall.firstArg, [ { first: 'Reeson', last: 'Shearsmith' }, ])) })
const request = require('request') module.exports = (url) => { // Do something with request. }
const request = require('request') module.exports = (url) => { // Do something with request. }
const request = require('request') const mockedRequest = (url, options, callback) => { // Log all requests. console.log('Request made:', url) request(url, options, callback) }; const foo = muk('./foo', { // Will overwrite all requires of "request" // with our own version. request: mockedRequest })
const request = require('request') const mockedRequest = (url, options, callback) => { // Log all requests. console.log('Request made:', url) request(url, options, callback) }; const foo = muk('./foo', { // Will overwrite all requires of "request" // with our own version. request: mockedRequest })
import { JSDOM } from 'jsdom' const { document } = new JSDOM(` <!DOCTYPE html> <p>Hello world</p> `) console.log(document.querySelector('p').textContent) // "Hello world"
import { JSDOM } from 'jsdom' const { document } = new JSDOM(` <!DOCTYPE html> <p>Hello world</p> `) console.log(document.querySelector('p').textContent) // "Hello world"
import { mount } from 'enzyme' const wrapper = mount(<Foo bar="Hellow World" />) console.log(wrapper.props().bar) // "Hello World"
import { mount } from 'enzyme' const wrapper = mount(<Foo bar="Hellow World" />) console.log(wrapper.props().bar) // "Hello World"
$ nyc ava
$ nyc ava
-------------------------|---------|----------|---------|---------|--------------------------------------------- File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s -------------------------|---------|----------|---------|---------|--------------------------------------------- All files | 85.34 | 61.14 | 74.02 | 85.05 | src | 100 | 100 | 100 | 100 | constants.ts | 100 | 100 | 100 | 100 | index.tsx | 100 | 100 | 100 | 100 | src/components | 97.87 | 79.31 | 88.46 | 97.7 | Context.tsx | 87.5 | 100 | 0 | 85.71 | 10 Field.tsx | 97.06 | 75 | 100 | 96.97 | 63 Form.tsx | 100 | 86.67 | 80 | 100 | 28-29
-------------------------|---------|----------|---------|---------|--------------------------------------------- File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s -------------------------|---------|----------|---------|---------|--------------------------------------------- All files | 85.34 | 61.14 | 74.02 | 85.05 | src | 100 | 100 | 100 | 100 | constants.ts | 100 | 100 | 100 | 100 | index.tsx | 100 | 100 | 100 | 100 | src/components | 97.87 | 79.31 | 88.46 | 97.7 | Context.tsx | 87.5 | 100 | 0 | 85.71 | 10 Field.tsx | 97.06 | 75 | 100 | 96.97 | 63 Form.tsx | 100 | 86.67 | 80 | 100 | 28-29
name: Test on: push: branches: [ master ] pull_request: branches: [ master ] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Use Node.js 16 uses: actions/setup-node@v2 with: node-version: 16 - run: npm ci - run: npm run build - run: npm run test
name: Test on: push: branches: [ master ] pull_request: branches: [ master ] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Use Node.js 16 uses: actions/setup-node@v2 with: node-version: 16 - run: npm ci - run: npm run build - run: npm run test
{ "name": "foobar", "scripts": { "build": "tsc", "test": "ava", "prepublishOnly": "npm run test" } }
{ "name": "foobar", "scripts": { "build": "tsc", "test": "ava", "prepublishOnly": "npm run test" } }
"For every complex problem there is an answer that is clear, simple, and wrong." – H. L. Mencken
The saddest thing about articles like these is that they are so full of wisdom, but they can never seem to compete with the "hot new methodology."
Like it or not, the "agile" movement had a huge impact on the way people do their jobs, in part because it had a name, it had advocates, and it had principles that you could fit on a PowerPoint slide.
But good testing (like good design) just can’t be encapsulated in a handful of principles you can teach to a newbie. Good testing requires wisdom.
I can write a lot of words about what I’ve learned about testing over the years, but it probably won’t do a good job of conveying that wisdom to others. The best testers I’ve ever worked with have a strong gut instinct, developed by experience, for where the bugs will be found, and how to effectively spend our limited time-budget for tests.
Is it worth adding more end-to-end UI tests? What if they run slowly? (How slowly?) Is it better to add more unit tests? What if adding unit tests requires aggressively mocking out dependencies? Which parts of the product require more testing, and which ones have been adequately tested?
These questions don’t have quick, easy answers, but the quick, easy answers keep winning mindshare, and we’re all impoverished as a result.
Dedication brings wisdom; lack of dedication leaves ignorance.
Know what leads you forward and what holds you back and choose the path that leads to wisdom.