Стратегии тестирования. TDD. Mock. Nock. Sinon
Жигалов Сергей
it(`should throw error when dice is string`, () => {
const cb = () => getPokerHand('hello');
assert.throws(cb, /Dice is not an array/);
});
it(`should throw error when dice is empty`, () => {
const cb = () => getPokerHand();
assert.throws(cb, /Dice is not an array/);
});
При выборе тест-кейсов смотрим на исходный код: каждый оператор в программе должен быть выполнен хотябы один раз.
function getPokerHand(dice) {
if (dice.length != 5)
throw new Error('Wrong amount of dices');
var combo = dice.reduce(function(acc, val) {
if(val < 1 || val > 6 || !Number.isInteger(val))
throw new Error('Dice should be int from 1 to 6');
/* ... */
});
}
Тест-кейсы выбираем исходя из того, как должна работать программа
it('should return `Покер` for [6, 6, 6, 6, 6]', () => {
const actual = getPokerHand([6, 6, 6, 6, 6]);
assert.equal(actual, 'Покер');
});
it('should return `Каре` for [6, 6, 3, 6, 6]', () => {
const actual = getPokerHand([6, 6, 3, 6, 6]);
assert.equal(actual, 'Каре');
});
it('should return `Фулл хаус` for [6, 1, 1, 6, 6]', () => {
const actual = getPokerHand([6, 1, 1, 6, 6]);
assert.equal(actual, 'Фулл хаус');
});
function runSuccessTest(dice, expected) {
const actual = getPokerHand(dice);
assert.equal(actual, expected);
}
it('should return `Покер` for [6, 6, 6, 6, 6]', () => {
runSuccessTest([6, 6, 6, 6, 6], 'Покер');
});
it('should return `Каре` for [6, 6, 3, 6, 6]', () => {
runSuccessTest([6, 6, 3, 6, 6], 'Каре');
});
it('should return `Фулл хаус` for [6, 1, 1, 6, 6]', () => {
runSuccessTest([6, 1, 1, 6, 6], 'Фулл хаус');
});
function runSuccessTest(dice, expected) {
return () => {
const actual = getPokerHand(dice);
assert.equal(actual, expected);
}
}
it('should return `Покер` for [6, 6, 6, 6, 6]',
runSuccessTest([6, 6, 6, 6, 6], 'Покер'));
it('should return `Каре` for [6, 6, 3, 6, 6]',
runSuccessTest([6, 6, 3, 6, 6], 'Каре'));
it('should return `Фулл хаус` for [6, 1, 1, 6, 6]',
runSuccessTest([6, 1, 1, 6, 6], 'Фулл хаус'));
[
{ dice: [6, 6, 6, 6, 6], expected: 'Покер' },
{ dice: [6, 6, 3, 6, 6], expected: 'Каре' },
{ dice: [6, 1, 1, 6, 6], expected: 'Фулл хаус' },
].forEach(test =>
it(`should return ${test.expected} for [${test.dice}]`,()=>{
const actual = getPokerHand(test.dice);
assert.equal(actual, test.expected);
}
);
describe('getPokerHand', () => {
it('should throw error when dice is not array', () => {
const cb = () => getPokerHand('not array');
assert.throws(cb, /Arguments is not array/);
});
});
getPokerHand
1) should throw error when dice is not array
0 passing (9ms)
1 failing
1) getPokerHand should throw error when dice is not array:
TypeError: getPokerHand is not a function
function getPokerHand(dice) {
if(!Array.isArray(dice)) {
throw new Error('Arguments is not array');
}
}
getPokerHand
✓ should throw error when dice is not array
1 passing (6ms)
Всё и так хорошо 👌
describe('getPokerHand', () => {
it('should throw error when dice is not array', /*...*/);
it('should throw error when dice length great 5', () => {
const cb = () => getPokerHand([1, 2, 3, 4, 5, 6]);
assert.throws(cb, /Arguments length not equal 5/);
});
});
getPokerHand
✓ should throw error when dice is not array
1) should throw error when dice length great 5
1 passing (8ms)
1 failing
1) getPokerHand should throw error when dice length great 5:
AssertionError: Missing expected exception..
function getPokerHand(dice) {
if(!Array.isArray(dice)) {
throw new Error('Arguments is not array');
}
if (dice.length > 5) {
throw new Error('Arguments length not equal 5');
}
}
getPokerHand
✓ should throw error when dice is not array
✓ should throw error when dice length great 5
2 passing (7ms)
По-прежнему всё хорошо 👌
describe('getPokerHand', () => {
it('should throw error when dice is not array', /*...*/);
it('should throw error when dice length great 5', /*...*/);
it('should throw error when dice length less 5', () => {
const cb = () => getPokerHand([1, 2, 3, 4]);
assert.throws(cb, /Arguments length not equal 5/);
});
});
getPokerHand
✓ should throw error when dice is not array
✓ should throw error when dice length great 5
1) should throw error when dice length less 5
2 passing (9ms)
1 failing
1) getPokerHand should throw error when dice length less 5:
AssertionError: Missing expected exception..
function getPokerHand(dice) {
if(!Array.isArray(dice)) {
throw new Error('Arguments is not array');
}
if (dice.length > 5) {
throw new Error('Arguments length not equal 5');
}
if (dice.length < 5) {
throw new Error('Arguments length not equal 5');
}
}
getPokerHand
✓ should throw error when dice is not array
✓ should throw error when dice length great 5
✓ should throw error when dice length less 5
3 passing (8ms)
function getPokerHand(dice) {
if(!Array.isArray(dice)) {
throw new Error('Arguments is not array');
}
if (dice.length !== 5) {
throw new Error('Arguments length not equal 5');
}
}
Определить победителя в игре "покер на костях"
// playPoker.js
const getPokerHand = require('./getPokerHand');
const pokerHands = [
'Наивысшее очко',
'Пара',
'Две пары',
'Тройка',
'Фулл хаус',
'Каре',
'Покер'
];
function playPoker(firstDice, secondDice) {/* ... */}
// playPoker.js
function playPoker(firstDice, secondDice) {
const first = getPokerHand(firstDice);
const second = getPokerHand(secondDice);
const compareHands =
pokerHands.indexOf(first) -
pokerHands.indexOf(second);
return compareHands === 0
? 'Ничья'
: compareHands > 0 ? 'Первый' : 'Второй';
}
(от англ. mock object, буквально: «объект-пародия», «объект-имитация», а также «подставка») — тип объектов, реализующих заданные аспекты моделируемого программного окружения.
proxyquire⭐️ 1,588
npm install proxyquire --save-dev
const proxyquire = require('proxyquire');
proxyquire({string} request, {Object} stubs);
// playPoker.js
const getPokerHand = require('./getPokerHand');
const pokerHands = [/* ... */];
function playPoker(firstDice, secondDice) {
const first = getPokerHand(firstDice);
const second = getPokerHand(secondDice);
const compareHands =
pokerHands.indexOf(first) -
pokerHands.indexOf(second);
return compareHands === 0
? 'Ничья'
: compareHands > 0 ? 'Первый' : 'Второй';
}
proxyquire
// tests/playPoker-test.js
it('should return `Ничья` for equal poker hand', () => {
const playPoker = proxyquire('../playPoker', {
'./getPokerHand': () => 'Пара'
});
const actual = playPoker([1, 1, 2, 3, 4], [1, 1, 2, 3, 4]);
assert.equal(actual, 'Ничья');
});
proxyquire
// tests/playPoker-test.js
it('should return `Первый` when first hand great', () => {
const answers = ['Каре', 'Тройка']
const playPoker = proxyquire('../playPoker', {
'./getPokerHand': () => answers.shift()
});
const actual = playPoker([1, 1, 1, 1, 4], [1, 1, 1, 3, 4]);
assert.equal(actual, 'Первый');
});
Test stubs are functions with pre-programmed behavior.
npm install sinon --save-dev
const sinon = require('sinon');
const getPokerHand = sinon.stub();
getPokerHand.withArgs([1, 1, 2, 3, 4]).returns('Пара');
getPokerHand.withArgs([1, 1, 2, 3, 5]).returns('Пара');
// tests/playPoker-test.js
it('should return `Ничья` for equal poker hand', () => {
const getPokerHand = sinon.stub();
getPokerHand.withArgs([1, 1, 2, 3, 4]).returns('Пара');
getPokerHand.withArgs([1, 1, 2, 3, 5]).returns('Пара');
const playPoker = proxyquire('../playPoker', {
'./getPokerHand': getPokerHand
});
const actual = playPoker([1, 1, 2, 3, 4], [1, 1, 2, 3, 5]);
assert.equal(actual, 'Ничья');
});
playPoker
✓ should return `Ничья` for equal poker hand
1 passing (13ms)
// tests/playPoker-test.js
it('should return `Ничья` for equal poker hand', () => {
const getPokerHand = sinon.stub();
getPokerHand.withArgs([1, 1, 2, 3, 4]).returns('Пара');
getPokerHand.withArgs([1, 1, 2, 3, 5]).returns('Пара');
getPokerHand.throws('Illegal arguments');
const playPoker = proxyquire('../playPoker', {
'./getPokerHand': getPokerHand
});
const actual = playPoker([1, 1, 2, 3, 4], [1, 1, 2, 3, 6]);
assert.equal(actual, 'Ничья');
});
playPoker
1) should return `Ничья` for equal poker hand
0 passing (12ms)
1 failing
1) playPoker should return `Ничья` for equal poker hand:
Illegal arguments
at Context.it (tests/playPoker-test.js:10:22)
Вывести на консоль результат игры.
// poker.js
const playPoker = require('./playPoker');
function poker(firstDice, secondDice) {
try {
const result = playPoker(firstDice, secondDice);
console.log(result);
} catch(error) {
console.error(error.message);
}
}
A test spy is a function that records arguments, return value, the value of this and exception thrown for all its calls.
// tests/poker-test.js
it('should print success result', () => {
const log = sinon.spy(console, 'log');
const error = sinon.spy(console, 'error');
/* ... */
});
it('should print success result', () => {
/* ... */
const playPoker = sinon.stub();
playPoker
.withArgs([1, 2, 3, 4, 5], [1, 2, 3, 4, 6])
.returns('Ничья');
const poker = proxyquire('../poker', {
'./playPoker': playPoker
});
poker([1, 2, 3, 4, 5], [1, 2, 3, 4, 6]);
/* ... */
});
it('should print success result', () => {
/* ... */
assert(log.calledOnce);
assert(log.calledWith('Ничья'));
assert(!error.called);
});
afterEach(() => {
console.log.restore();
console.error.restore();
});
Poker
Ничья
✓ should print success result
1 passing (13ms)
it('should print success result', () => {
const log = sinon.stub(console, 'log');
const error = sinon.stub(console, 'error');
/* ... */
}
Poker
2 passing (28ms)
Отрисовать прогноз погоды в консоли
curl http://wttr.in/ekaterinburg
// weather.js
const request = require('request');
const url = 'https://api.weather.yandex.ru/v1/forecast';
function weather(cb) {
request(url, (requestError, res, body) => {
if (requestError || res.statusCode !== 200) {
return cb(requestError.message);
}
try {
const data = JSON.parse(body);
cb(null, data.fact.temp);
} catch (parseError) {
cb(parseError);
}
});
}
nock
npm install nock --save-dev
nock('https://api.weather.yandex.ru')
.get('/v1/forecast')
.reply(200, '{"fact":{"temp":25}}');
nock
// tests/weather-test.js
it('should print temperature', done => {
nock('https://api.weather.yandex.ru')
.get('/v1/forecast')
.reply(200, '{"fact":{"temp":25}}');
weather((error, actual) => {
assert.equal(actual, 25);
done(error);
});
});
nock
// tests/weather-test.js
it('should return error when request failed', done => {
nock('https://api.weather.yandex.ru')
.get('/v1/forecast')
.reply(500, 'Internal server error');
weather((error, actual) => {
assert.equal(error, 'Request failed');
done();
});
});
nock
// tests/weather-test.js
afterEach(nock.cleanAll);