Автотесты

Стратегии тестирования. Подходы к разработке. Подмены

Жигалов Сергей

Способы выбора тест-кейсов

  • Тестирование чёрного ящика
  • Тестирование белого ящика

Чёрный ящик

Тестирование чёрного ящика

стратегия тестирования программы с точки зрения внешнего мира, при котором не используется знание о внутреннем устройстве тестируемого объекта.

Документация

  • Покер 1️⃣ 1️⃣ 1️⃣ 1️⃣ 1️⃣ — пять костей одного вида
  • Каре 1️⃣ 1️⃣ 1️⃣ 1️⃣ 2️⃣ — четыре кости одного вида
  • Фулл хаус 1️⃣ 1️⃣ 1️⃣ 2️⃣ 2️⃣ — три кости одного вида + пара
  • Тройка 1️⃣ 1️⃣ 1️⃣ 2️⃣ 3️⃣ — три кости одного вида
  • Две пары 1️⃣ 1️⃣ 2️⃣ 2️⃣ 3️⃣ — две кости одного вида и две кости другого вида
  • Пара 1️⃣ 1️⃣ 2️⃣ 3️⃣ 4️⃣ — две кости одного вида
  • Наивысшее очко 1️⃣ 2️⃣ 3️⃣ 4️⃣ 5️⃣ — во всех остальных случаях

Позитивные проверки

  • возвращает `Покер` для [1, 1, 1, 1, 1]
  • возвращает `Каре` для [1, 1, 1, 1, 2]
  • возвращает `Фулл хаус` для [1, 1, 1, 2, 2]
  • возвращает `Тройка` для [1, 1, 1, 2, 3]
  • возвращает `Две пары` для [1, 1, 2, 2, 3]
  • возвращает `Пара` для [1, 1, 2, 3, 4]
  • возвращает `Наивысшее очко` для [1, 2, 3, 4, 5]

Негативные проверки

  • на вход ничего не подаётся
  • на вход подается не массив
  • размер массива больше 5
  • размер массива меньше 5
  • элемент массива не число
  • элемент массива меньше 1
  • элемент массива больше 6
  • элемент массива дробное число

Белый ящик

Белый ящик

метод тестирования программного обеспечения, который предполагает, что внутренняя структура системы известна тестировщику.

Сценарии развития


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, 'Фулл хаус');
});
                

level 1


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], 'Фулл хаус');
});
                

level 2


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], 'Фулл хаус')
);
        

level 3


[
    { 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}`)
    );
});
                

Подходы к разработке

TLD

(Test Last Development) тесты после написания кода

Алгоритм

  1. Написать код
  2. Покрыть код тестами
  3. Проверить, что тесты проходят

TLD

    Естественный процесс

    Нет накладных расходов

    Понятно как писать тесты

    Ложно-положительные тесты

    Нет гарантии 100% покрытия кода

TDD

(Test Driven Development) тесты до написания кода
😯

Алгоритм

  1. Описываем поведение в тесте
  2. Проверяем, что тест не проходит
  3. Реализуем поведение в коде
  4. Проверяем, что тест проходит
  5. Рефакторинг кода

Пример

  1. на вход подается не массив
  2. размер массива больше 5
  3. размер массива меньше 5

1.1 Тест


describe('getPokerHand', () => {
    it('should throw error when dice is not array', ()=>{
        const cb = () => getPokerHand('not array');

        assert.throws(cb, /Dice is not array/);
    });
});
        

1.2 Тест не проходит


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
                

1.3 Код


function getPokerHand(dice) {
    if(!Array.isArray(dice)) {
        throw new Error('Dice is not array');
    }
}
                

1.4 Тест проходит


getPokerHand
  ✓ should throw error when dice is not array


1 passing (6ms)
        

1.5 Рефакторинг

Всё и так хорошо 👌

  1. ✓ на вход подается не массив
  2. размер массива больше 5
  3. размер массива меньше 5

2.1 Тест


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/);
    });
});
                 

2.2 Тест не проходит


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..
        

2.3 Код


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');
    }
}
        

2.4 Тест проходит


getPokerHand
  ✓ should throw error when dice is not array
  ✓ should throw error when length great 5


2 passing (7ms)
        

2.5 Рефакторинг

По-прежнему всё хорошо 👍

  1. ✓ на вход подается не массив
  2. ✓ размер массива больше 5
  3. размер массива меньше 5

3.1 Тест


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/);
    });
});
                

3.2 Тест не проходит


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..
        

3.3 Код


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');
    }
}
        

3.4 Тест проходит


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)
        

3.5 Рефакторинг


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');
    }
}
                

3.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)
        

TDD

    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 ? 'Первый' : 'Второй';
}
                

Проблемы

Не понятно кто виноват, если тест упал

Сложные тесты

Один модуль

getPokerHand

Несколько модулей

playPoker

Подмена

playPoker

Пример


function getPokerHand(dice) {
    return 'Пара';
}
                

Mock-объект

(от англ. mock object, буквально: «объект-пародия», «объект-имитация», а также «подставка») — тип объектов, реализующих заданные аспекты моделируемого программного окружения.

proxyquire ⭐️ 1962


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, 'Первый');
});
                

mockery ⭐️ 999


npm install proxyquire --save-dev
                

Тест


const mockery = require('mockery');

it('should return `Ничья` for equal combinations', () => {
    mockery.registerMock('./getPokerHand', () => 'Пара');
    mockery.enable();







    mockery.disable();
});
                

mockery.enable();

Тест


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 кеширует

useCleanCache


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();
});
                

afterEach


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');
                
sinon

sinon ⭐️ 5721


npm install sinon --save-dev
                

sinon


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');
                

stub

функция с предопределённым поведением

тест


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, 'Ничья');
});
        

Почитать

Домашка