Стратегии тестирования. Подходы к разработке. Подмены
Жигалов Сергей
стратегия тестирования программы с точки зрения внешнего мира, при котором не используется знание о внутреннем устройстве тестируемого объекта.
метод тестирования программного обеспечения, который предполагает, что внутренняя структура системы известна тестировщику.
function getPokerHand(dice) {
if (!Array.isArray(dice)) {
throw new Error('Dice is not an array');
}
if (dice.length !== 5) {
throw new Error('Wrong amount of dice');
}
/* ... */
}
Количество тестов равно количеству вариантов развития
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(({ dice, expected }) =>
it(`should return ${expected} for ${dice}`,
() => {
const actual = getPokerHand(dice);
assert.equal(actual, expected);
}
)
);
describe('getPokerHand', () => {
[
{ dice: [6, 6, 6, 6, 6], expected: 'Покер' },
{ dice: [6, 6, 3, 6, 6], expected: 'Каре' },
{ dice: [6, 1, 1, 6, 6], expected: 'Фулл хаус' }
].forEach(({ dice, expected }) =>
it(`should return ${expected} for ${dice}`)
);
});
(Test Last Development) тесты после написания кода
Естественный процесс
Нет накладных расходов
Понятно как писать тесты
Ложно-положительные тесты
Нет гарантии 100% покрытия кода
(Test Driven Development) тесты до написания кода
describe('getPokerHand', () => {
it('should throw error when dice is not array', ()=>{
const cb = () => getPokerHand('not array');
assert.throws(cb, /Dice 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('Dice 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 length great 5', () => {
const cb = () => getPokerHand([1, 2, 3, 4, 5, 6]);
assert.throws(cb, /Dice length not equal 5/);
});
});
getPokerHand
✓ should throw error when dice is not array
1) should throw error when length great 5
1 passing (8ms)
1 failing
1) getPokerHand should throw error when length great 5:
AssertionError: Missing expected exception..
function getPokerHand(dice) {
if(!Array.isArray(dice)) {
throw new Error('Dice is not array');
}
if (dice.length > 5) {
throw new Error('Dice length not equal 5');
}
}
getPokerHand
✓ should throw error when dice is not array
✓ should throw error when length great 5
2 passing (7ms)
По-прежнему всё хорошо 👍
describe('getPokerHand', () => {
it('should throw error when dice is not array', ...);
it('should throw error when length great 5', ...);
it('should throw error when length less 5', () => {
const cb = () => getPokerHand([1, 2, 3, 4]);
assert.throws(cb, /Dice length not equal 5/);
});
});
getPokerHand
✓ should throw error when dice is not array
✓ should throw error when length great 5
1) should throw error when length less 5
2 passing (9ms)
1 failing
1) getPokerHand should throw error when length less 5:
AssertionError: Missing expected exception..
function getPokerHand(dice) {
if(!Array.isArray(dice)) {
throw new Error('Dice is not array');
}
if (dice.length > 5) {
throw new Error('Dice length not equal 5');
}
if (dice.length < 5) {
throw new Error('Dice length not equal 5');
}
}
getPokerHand
✓ should throw error when dice is not array
✓ should throw error when length great 5
✓ should throw error when length less 5
3 passing (8ms)
function getPokerHand(dice) {
if(!Array.isArray(dice)) {
throw new Error('Dice is not array');
}
if (dice.length !== 5) {
throw new Error('Dice length not equal 5');
}
}
getPokerHand
✓ should throw error when dice is not array
✓ should throw error when length great 5
✓ should throw error when length less 5
3 passing (8ms)
100% покрытие кода тестами
Продумать поведение до реализации
Нет ложно-положительных тестов
Обнаружение 🐞 на ранней стадии
Непривычно, сложно начать
Игрок, чья комбинация сильнее, становится победителем в игре "покер на костях". Если комбинации игроков совпадают, наступает ничья.
// lib/playPoker.js
const getPokerHand = require('./getPokerHand');
const pokerHands = [
'Наивысшее очко', // 0
'Пара', // 1
'Две пары', // 2
'Тройка', // 3
'Фулл хаус', // 4
'Каре', // 5
'Покер' // 6
];
function playPoker(firstDice, secondDice) {
/* ... */
}
/* ... */
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 ? 'Первый' : 'Второй';
}
/* ... */
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 ? 'Первый' : 'Второй';
}
Не понятно кто виноват, если тест упал
Сложные тесты
function getPokerHand(dice) {
return 'Пара';
}
(от англ. mock object, буквально: «объект-пародия», «объект-имитация», а также «подставка») — тип объектов, реализующих заданные аспекты моделируемого программного окружения.
npm install proxyquire --save-dev
// lib/playPoker.js
const getPokerHand = require('./getPokerHand');
const pokerHands = [ /* ... */ ];
function playPoker(firstDice, secondDice) {
/* ... */
}
// tests/playPoker-test.js
const proxyquire = require('proxyquire');
it('should return `Ничья` for equal poker hand', () => {
const playPoker = proxyquire('../lib/playPoker', {
'./getPokerHand': () => 'Пара'
});
const actual = playPoker([1, 1, 2, 3, 4], [1, 1, 2, 3, 4]);
assert.equal(actual, 'Ничья');
});
// tests/playPoker-test.js
const proxyquire = require('proxyquire');
it('should return `Ничья` for equal poker hand', () => {
const playPoker = proxyquire('../lib/playPoker', {
'./getPokerHand': () => 'Пара'
});
const actual = playPoker();
assert.equal(actual, 'Ничья');
});
const answers = ['Каре', 'Тройка'];
const getPokerHand = () => answers.shift();
getPokerHand(); // 'Каре'
getPokerHand(); // 'Тройка'
getPokerHand(); // undefined
// tests/playPoker-test.js
it('should return `Первый` when first hand great', () => {
const answers = ['Каре', 'Тройка'];
const playPoker = proxyquire('../lib/playPoker', {
'./getPokerHand': () => answers.shift()
});
const actual = playPoker();
assert.equal(actual, 'Первый');
});
npm install proxyquire --save-dev
const mockery = require('mockery');
it('should return `Ничья` for equal combinations', () => {
mockery.registerMock('./getPokerHand', () => 'Пара');
mockery.enable();
mockery.disable();
});
const mockery = require('mockery');
it('should return `Ничья` for equal combinations', () => {
mockery.registerMock('./getPokerHand', () => 'Пара');
mockery.enable();
const playPoker = require('../lib/playPoker');
const actual = playPoker();
assert.equal(actual, 'Ничья');
mockery.disable();
});
mockery распространяется на модули, подключенные после
enable()require кеширует
it('should return `Ничья` for equal combinations', () => {
mockery.registerMock('./getPokerHand', () => 'Пара');
mockery.enable({ useCleanCache: true });
const playPoker = require('../lib/playPoker');
const actual = playPoker();
assert.equal(actual, 'Ничья');
mockery.disable();
});
it('should return `Ничья` for equal combinations', () => {
mockery.registerMock('./getPokerHand', () => 'Пара');
mockery.enable({ useCleanCache: true });
const playPoker = require('../lib/playPoker');
const actual = playPoker();
assert.equal(actual, 'Ничья');
});
afterEach(() => mockery.disable());
const getPokerHand = createStub();
getPokerHand.withArgs([1, 1, 2, 3, 4]).returns('Пара');
getPokerHand.withArgs([1, 1, 2, 3, 5]).returns('Пара');
getPokerHand.throws('Illegal arguments');
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('Пара');
getPokerHand.throws('Illegal arguments');
функция с предопределённым поведением
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, 5]);
assert.equal(actual, 'Ничья');
});