Автотесты

Assert. Mocha. Организация тестов

Тациенко Антон

«Операция завершиться через 41 секунд»

Задача

Выбрать правильную форму склонения числительного

Пример


const forms = ['секунда', 'секунды', 'секунд'];

forms[0]; // 'секунда'
forms[1]; // 'секунды'
forms[2]; // 'секунд'
                

Пример


function getPlural(count, forms) {
    /* ... */
}

const forms = ['секунда', 'секунды', 'секунд'];
getPlural(21, forms); // 'секунда'
getPlural(62, forms); // 'секунды'
getPlural(5, forms);  // 'секунд'
                

Наблюдения


const forms = ['секунда', 'секунды', 'секунд'];
                
  • Заканчивается на 1, то forms[0]
  • Заканчивается на 2 - 4, то forms[1]
  • Заканчивается на 0, 5 - 9, то forms[2]
  • Заканчивается на 11 - 14, то forms[2]

Алгоритм

  1. Для 11-14 вернуть forms[2]
  2. Взять остаток от деления на 10
  3. Для 1 вернуть forms[0]
  4. Для 2-4 вернуть forms[1]
  5. Для 0, 5-9 вернуть forms[2]

Код


function getPlural(count, forms) {
    if (count > 10 && count < 15)
        return forms[2];

    const residue = count % 10;

    if (residue === 1)
        return forms[0];
    if (residue > 0 && residue < 5)
        return forms[1];
    return forms[2];
}
                    

// Для 11-14
// вернуть forms[2]

// отстаток от % 10

// Для 1
// вернуть forms[0]
// Для 2-4
// вернуть forms[1]
// Иначе forms[2]
                    

Проверяем


const forms = ['секунда', 'секунды', 'секунд'];

getPlural(1, forms);  // 'секунда'
getPlural(2, forms);  // 'секунды'
getPlural(5, forms);  // 'секунд'
getPlural(11, forms); // 'секунд'
getPlural(22, forms); // 'секунды'
                

Проверяем


const forms = ['секунда', 'секунды', 'секунд'];

getPlural(1, forms) === 'секунда';  // true
getPlural(2, forms) === 'секунды';  // true
getPlural(5, forms) === 'секунд';   // true
getPlural(11, forms) === 'секунд';  // true
getPlural(22, forms) === 'секунды'; // true
                

assert

Своя реализация


function assert(value) {
    const hasError = !value;
    if (hasError) {
        throw new Error('Assertion failed');
    }
}
                

Готовая реализация


const assert = require('assert');
                

success


const assert = require('assert');
const forms = ['секунда', 'секунды', 'секунд'];

const actual = getPlural(1, forms);
const expected = 'секунда';

assert(actual === expected);
                

>
                

error


const assert = require('assert');
const forms = ['секунда', 'секунды', 'секунд'];

const actual = getPlural(1, forms);
const expected = 'секунд';

assert(actual === expected);
        

assert.js:89
  throw new assert.AssertionError({
  ^
AssertionError: false == true
    at Object.<anonymous> (/plural.js:17:1)
        

message


const assert = require('assert');
const forms = ['секунда', 'секунды', 'секунд'];

const actual = getPlural(1, forms);
const expected = 'секунд';

assert(actual === expected,
      'Должна вернуться`секунда`,' +
      'если передать `1`');
        

assert.js:89
  throw new assert.AssertionError({
  ^
AssertionError: Должна вернуться`секунда`, если передать `1`
    at Object.<anonymous> (/plural.js:17:1)
        

equal (deprecated)


const assert = require('assert');
const forms = ['секунда', 'секунды', 'секунд'];

const actual = getPlural(1, forms);
const expected = 'секунда';

assert.equal(actual, expected);
                

assert.js:89
  throw new assert.AssertionError({
  ^
AssertionError: 'секунда' == 'секунд'
    at Object.<anonymous> (/plural.js:17:8)
                

strictEqual


const assert = require('assert');

const actual = '1';
const expected = 1;

assert.equal(actual, expected); # нет ошибки
assert.strictEqual(actual, expected);
                

assert.js:42
  throw new errors.AssertionError({
  ^

AssertionError [ERR_ASSERTION]: '1' === 1
    at Object.<anonymous> (/path/to/file.js:6:8)
                

Сравнение объектов


const assert = require('assert');

const actual = ['секунда'];
const expected = ['секунда'];

assert.equal(actual, expected);
                

assert.js:42
  throw new errors.AssertionError({
  ^

AssertionError [ERR_ASSERTION]: [ 'секунда' ] == [ 'секунда' ]
    at Object.<anonymous> (/path/to/file.js:6:8)
                

deepStrictEqual


const assert = require('assert');

const actual = ['секунда'];
const expected = ['секунда'];

assert.deepStrictEqual(actual, expected);
                

>
                

assert.deepEqual
assert.deepStrictEqual
assert.doesNotThrow
assert.equal
assert.fail
assert.fail
assert.ifError
                    

assert.notDeepEqual
assert.notDeepStrictEqual
assert.notEqual
assert.notStrictEqual
assert.ok
assert.strictEqual
assert.throws
                    

итог

  • Проверяет данные
  • Локализует ошибки
  • Используется в коде
  • Никак не влияет в случае успеха

Проверяем

 
const assert = require('assert');
const forms = ['секунда', 'секунды', 'секунд'];

assert.equal(getPlural(1, forms), 'секунда');
assert.equal(getPlural(2, forms), 'секунды');
assert.equal(getPlural(11, forms), 'секунд');
assert.equal(getPlural(22, forms), 'секунды');
                

assert.equal(getPlural(113, forms), 'секунд');
                

assert.js:89
  throw new assert.AssertionError({
  ^
AssertionError: 'секунды' == 'секунд'
    at Object.<anonymous> (/plural.js:22:8)
        

Алгоритм

  1. Взять остаток от деления на 100
  2. Для 11-14 вернуть forms[2]
  3. Взять остаток от деления на 10
  4. Для 1 вернуть forms[0]
  5. Для 2-4 вернуть forms[1]
  6. Для 0, 5-9 вернуть forms[2]

Исправляем


function getPlural(count, forms) {
    count %= 100;
    if (count > 10 && count < 15)
        return forms[2];
    const residue = count % 10;
    if (residue === 1)
        return forms[0];
    if (residue > 0 && residue < 5)
        return forms[1];
    return forms[2];
}
                    

// отстаток % 100
// Для 11-14
// вернуть forms[2]
// отстаток % 10
// Для 1
// вернуть forms[0]
// Для 2-4
// вернуть forms[1]
// Иначе forms[2]
                    

Автотест

(автоматизированный тест) – это скрипт, имитирующий взаимодействия пользователя с приложением, цель которого – локализация ошибок в работе программного обеспечения.
Я - ответственный разработчик. Думаю перед тем как писать код.

Тесты?

No

Для чего нужны тесты?

  • В коде нет ошибок
  • Вносить изменения
  • Рефакторить
  • Живая документация
  • Локализация ошибки
  • Структура кода

Организация тестов

Фреймворки

установка


npm install mocha --save-dev
                

// package.json
{
    "devDependencies": {
        "mocha": "7.1.1"
    }
}
                

Структура файлов



├── node_modules
├── package.json
├─┬ lib
│ └── plural.js
└─┬ tests
  └── plural-test.js
                

подготовка


// tests/plural-test.js

describe('getPlural', () => {
    it('should return `секунда` for `1`');
    it('should return `секунды` for `2`');
    it('should return `секунд` for `5`');
    it('should return `секунд` for `11`');
    it('should return `секунды` for `22`');
    it('should return `секунд` for `113`');
});
                

BDD

(Behavior-driven development) разработка через поведение.

подготовка


// tests/plural-test.js

describe('getPlural', () => {
    it('should return `секунда` for `1`');
    it('should return `секунды` for `2`');
    it('should return `секунд` for `5`');
    it('should return `секунд` for `11`');
    it('should return `секунды` for `22`');
    it('should return `секунд` for `113`');
});
                

запуск


node_modules/.bin/mocha tests
                

getPlural
  - should return `секунда` for `1`
  - should return `секунды` for `2`
  - should return `секунд` for `5`
  - should return `секунд` for `11`
  - should return `секунды` for `22`
  - should return `секунд` for `113`


0 passing (6ms)
6 pending
                

Пример описания теста


const assert = require('assert');

describe('Calculator', () => {
    it('should sum digits', () => {
        assert.equal(1 + 1, 2);
    });
});
                

  Calculator
    ✓ should sum digits


  1 passing (6ms)
                

Пример ошибки в тесте


const assert = require('assert');

describe('Calculator', () => {
    it('should multiple digits', () => {
        assert.equal(1 * 1, 2);
    });
});
                

  Calculator
    1) should multiple digits

  0 passing (9ms)
  1 failing

  1) Calculator
       should multiple digits:

      AssertionError [ERR_ASSERTION]: 1 == 2
      + expected - actual

      -1
      +2

      at Context.it (tests/example-test.js:5:16)
                

тест


// tests/plural-test.js
const getPlural = require('../lib/plural.js')
const assert = require('assert');

describe('getPlural', () => {
    const forms = ['секунда', 'секунды', 'секунд'];

    it('should return `секунда` for `1`', () => {
        const actual = getPlural(1, forms);

        assert.equal(actual, 'секунда');
    });

    // ...
});
                

запуск


node_modules/.bin/mocha tests
        

getPlural
  ✓ should return `секунда` for `1`
  - should return `секунды` for `2`
  - should return `секунд` for `5`
  - should return `секунд` for `11`
  - should return `секунды` for `22`
  - should return `секунд` for `113`


1 passing (7ms)
5 pending
                

запуск


node_modules/.bin/mocha tests
                

getPlural
  ✓ should return `секунда` for `1`
  ✓ should return `секунды` for `2`
  ✓ should return `секунд` for `5`
  ✓ should return `секунд` for `11`
  ✓ should return `секунды` for `22`
  ✓ should return `секунд` for `113`


6 passing (9ms)
                

Оптимизации: параметризованные кейсы


// tests/plural-test.js
const getPlural = require('../lib/plural.js')
const assert = require('assert');

describe('getPlural', () => {
    const forms = ['секунда', 'секунды', 'секунд'];

    const allCases = [
        { number: 1, form: 'секунда' },
        { number: 2, form: 'секунды'},
        { number: 5, form: 'секунд'},
        { number: 11, form: 'секунд'},
        { number: 22, form: 'секунды'},
        { number: 113, form: 'секунд'}
    ]

    for(const testCase of allCases) {
        it(`should return '${testCase.form}' for '${testCase.number}'`, () => {
            const actual = getPlural(testCase.number, forms);

            assert.equal(actual, testCase.form);
        });
    }
});
                                    

Проверка исключений

Пример


function getPlural(count, forms) {
    if (isNaN(count)) {
        throw new Error('Count is not a number');
    }

    // ...
}
                

Тест


it('should throw error when count is not a number', () => {
    const cb = () => getPlural('NaN', forms);

    assert.throws(cb, /Count is not a number/);
});
                

Вложенность

Вложенность


describe('getPlural', () => {
    it('should return `секунда` for `1`');
    it('should return `секунды` for `2`');
    it('should return `секунд` for `5`');
    it('should return `секунд` for `11`');
    it('should return `секунды` for `22`');
    it('should return `секунд` for `113`');
    it('should throw error when ...');
});
                

Вложенность


describe('getPlural', () => {
    describe('positive', () => {
        it('should return `секунда` for `1`');
        it('should return `секунды` for `2`');
        describe('third form', () => {
            it('should return `секунд` for `5`');
            it('should return `секунд` for `11`');
            it('should return `секунд` for `113`');
        });
    });
    describe('negative', () => {
        it('should throw error when ...');
    });
});
                

getPlural
    positive
        - should return `секунда` for `1`
        - should return `секунды` for `2`
    third form
        - should return `секунд` for `5`
        - should return `секунд` for `11`
        - should return `секунд` for `113`
    negative
        - should throw error when ...   


0 passing (6ms)
6 pending
                    

hook

beforeEach


describe('hooks', () => {
    beforeEach(() => {
        // Код этой функции выполнится
        // перед каждым тестом
    });

    // ...
});
        

hook

  • beforeEach
  • afterEach
  • before
  • after

Цикл выполнения хуков


describe('Module name', () => {
    before(() => console.log(1));
    beforeEach(() => console.log(2));

    describe('method name', () => {
        before(() => console.log(3));
        beforeEach(() => console.log(4));

        it('should do ...', () => console.log(5));
        it('should do other', () => console.log(6));
    });
});
                

Пример


Module name
1
  method name
3
2
4
5
    ✓ should do ...
2
4
6
    ✓ should do other
        

only


describe('Module name', () => {
    it.only('should do ...', () => {
        console.log('first');
    });

    it('should do other', () => {
        console.log('second');
    });
});
        

Module name
first
  ✓ should do ...


1 passing (7ms)
        

only


describe.only('Module name', () => {
    it('should do ...', () => {
        console.log('first');
    });

    it('should do other', () => {
        console.log('second');
    });
});
        

Module name
first
  ✓ should do ...
  ✓ should do other


2 passing (7ms)
        

skip


describe('Module name', () => {
    it('should do ...', () => {
        console.log('first');
    });

    it.skip('should do other', () => {
        console.log('second');
    });
});
        

Module name
first
  ✓ should do ...
  - should do other

1 passing (7ms)
1 pending
        

Отчеты

Отчеты SPEC

spec

Отчеты DOT MATRIX

dot

Отчеты NYAN

nyan

Отчеты TAP

tap

Отчеты LANDING STRIP

landing

Отчеты LIST

list

Отчеты PROGRESS

progress

Отчеты JSON

json

Отчеты JSON STREAM

json-stream

Отчеты MIN

min

Отчеты DOC

doc

запуск


// package.json
{
    "scripts": {
        "test": "mocha tests"
    }
}
                

npm run test
npm test
npm t
                

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

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

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

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

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

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

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

TLD

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

Алгоритм

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

TLD

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

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

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

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

    Нет гарантии 100% покрытия требований

TDD

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

Алгоритм

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

TDD

    100% покрытие требований тестами

    Продумать поведение до реализации

    Нет ложно-положительных тестов

    Обнаружение 🐞 на ранней стадии

    Непривычно, сложно начать

Итого

  • Assert
  • mocha icomocha
  • Организация тестов
  • Hook
  • Отчёты
  • Запуск тестов
  • Подходы к написанию тестов
  • Подходы к разработке тестов: TLD и TDD

Почитать