Автотесты

Подмены. Управление временем. Интеграционные тесты. Тестирование в браузере

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

spy

spy

функция, котороя запоминает для каждого вызова: с какими аргументами её вызвали, какое значение вернули и произошло ли исключение.

Пример


const getTweets = require('./getTweets');

function showTweets() {
    const tweets = getTweets('#urfu-testing-2017');

    tweets.forEach(tweet => console.log(tweet.text));
};
                

Тест


const sinon = require('sinon');

describe('showTweets', () => {
    beforeEach(() => {
        sinon.spy(console, 'log');
    });

    afterEach(() => {
        console.log.restore();
    });

    /* ... */
});
                

Тест


describe('showTweets', () => {
    /* ... */

    it('should print tweet text', () => {
        showTweets();

        assert.equal(console.log.callCount, 2);
        assert.deepEqual(
            console.log.getCall(0).args,
            ['Для подмены сетевых запросов ...']
        );
    });
});
                
Функции-шпионы не только сохраняют прежнее поведение, но позволяют узнать как эти функции вызвали.

Контекст


it('spy ordering', () => {
    const first = sinon.spy();
    const second = sinon.spy();
    const third = sinon.spy();

    first();
    second();
    third();

    assert(first.calledBefore(third));
    assert(first.calledImmediatelyBefore(second));
});
                

Управление временем

Бонусное задание

... Сделать из твитов бегущую строку. Для этого нужно выводить на консоль по одному символу раз в 100ms.

setTimeout


setTimeout(() => {
    console.log('Прошла секунда');
}, 1000);
        

console.log


console.log('Длинное предложение');
console.log('в одну строку.');
                

Длинное предложение
в одну строку.
                
👎

process.stdout.write


process.stdout.write('Одно');
process.stdout.write('строчный');
                

Однострочный
                
👍

Решение


function crawlLine(text, cb) {
    const letters = text.split('');

    function print() {
        if (!letters.length) return cb();

        process.stdout.write(letters.shift());
        setTimeout(print, 100);
    }

    print();
}
                

Тест


describe('crawlLine', () => {
    beforeEach(() => {
        sinon.spy(process.stdout, 'write');
    });

    afterEach(() => {
        process.stdout.write.restore();
    });

    /* ... */
});
                

Тест


describe('crawlLine', () => {
    /* ... */

    it('should print letters one by one', () => {
        crawlLine('I don’t always bend time and ' +
            'space in unit tests, but when I do, ' +
            'I use Buster.JS + Sinon.JS');
        assert.equal(process.stdout.write.callCount, 91);
    });
});
                
cb будет вызван, когда работа функции завершится.

Тест


it('should print letters one by one', () => {
    crawlLine('I don’t always bend time and ' +
        'space in unit tests, but when I do, ' +
        'I use Buster.JS + Sinon.JS', () => {
        assert.equal(process.stdout.write.callCount, 91);
    });
});
                

Тест


it('should print letters one by one', done => {
    crawlLine('I don’t always bend time and ' +
        'space in unit tests, but when I do, ' +
        'I use Buster.JS + Sinon.JS', () => {
        assert.equal(process.stdout.write.callCount, 91);
        done();
    });
});
                

Тест


it('should print letters one by one', function (done) {
    this.timeout(30000);
    crawlLine('I don’t always bend time and ' +
        'space in unit tests, but when I do, ' +
        'I use Buster.JS + Sinon.JS', () => {
        assert.equal(process.stdout.write.callCount, 91);
        done();
    });
});
                

Faking time


beforeEach(() => {
    clock = sinon.useFakeTimers();
});

afterEach(() => {
    clock.restore();
});
                

it('should print letters one by one', done => {
    crawlLine('I don’t always bend time and ' +
        'space in unit tests, but when I do, ' +
        'I use Buster.JS + Sinon.JS', () => {
        assert.equal(process.stdout.write.callCount, 91);
        done();
    });

    clock.tick(10000);
});
                

Относительное время

Относительное время


describe('formatDate', () => {
    let clock;

    beforeEach(() => {
        const startDate = new Date(2018, 4, 15).getTime();
        clock = sinon.useFakeTimers(startDate);
    });

    afterEach(() => {
        clock.restore();
    });

    /* ... */
});
                

Относительное время


describe('formatDate', () => {
    /* ... */

    it('should return only time', () => {
        const tweetsDate = new Date(2018, 4, 15, 6, 17);
        const actual = formatDate(tweetsDate);

        assert.equal(actual, '06:17');
    });
});
                

Работа с асинхронностью

Задача

Вывести прогноз погоды на консоль

curl http://wttr.in/ekaterinburg

https://api.weather.yandex.ru/v1/forecast

Решение


const got = require('got');
const url = 'https://api.weather.yandex.ru' +
            '/v1/forecast';

function getFactTemp() {
    return got(url, { json: true })
        .then(res => res.body.fact.temp);
};
                

Тесты


describe('getFactTemp', () => {
    it('should get fact temperature', done => {
        getWeather()
            .then(actual => assert.equal(actual, 2))
            .then(done, done);
    });
});
                
Если вызвать done() без аргументов, то тест завершится успешно. Если вызвать done(error) с аргументом, то тест завершится с ошибкой.

Тесты


describe('getFactTemp', () => {
    it('should get fact temperature', () => {
        return getWeather()
            .then(actual => assert.equal(actual, 2));
    });
});
                

Тесты


describe('getFactTemp', () => {
    it('should get fact temperature', async () => {
        const actual = await getWeather();

        assert.equal(actual, 2);
    });
});
                

Подмена сетевых запросов

nock ⭐️ 6190


npm install nock --save-dev
                

Тесты


describe('getFactTemp', () => {
    beforeEach(() => {
        nock('https://api.weather.yandex.ru')
            .get('/v1/forecast')
            .reply(200, { fact: { temp: 2 } });
    });

    afterEach(() => {
        nock.cleanAll();
    });

    /* ... */
});
                
Все запросы по сети будут подменяться по правилам, описанным в nock().

Интеграционные тесты

тестирование группы взаимодействующих модулей

О важности интеграционных тестов

marsRover

тест


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

Weather


describe('getFactTemp', () => {
    beforeEach(() => {
        nock('https://api.weather.yandex.ru')
            .get('/v1/forecast')
            .reply(200, { fact: { temp: 2 } });
    });

    afterEach(() => {
        nock.cleanAll();
    });

    it('should get fact temperature', async () => {
        const actual = await getWeather();

        assert(Number.isInteger(actual));
    });
});
                

Рекомендации

  • Подготовить изолированное окружение
  • Покрывать всю логику модульными тестами
  • Писать минимум интеграционных тестов

Интеграционные тесты

Полезные

Медленные

Нестабильные

Тестирование через интерфейс

Задача

Пользователи вводят адрес электронной почты при заполнении формы. Реализовать валидацию полей формы.

Пример

Решение


<input  id="email"
        type="email"
        placeholder="Ваш email"
        required >
                

Тестирование HTML

Решение


<!DOCTYPE html>
<html lang="en">
    <head>
        <title>Email</title>
    </head>

    <body>
        <input  id="email"
                type="email"
                placeholder="Ваш email"
                required>
    </body>
</html>
                

Подключение на страницу


<head>
    <!-- подключаем стили, чтобы тесты вяглядели красиво -->
    <link href="path/to/mocha.css" rel="stylesheet" />
</head>
                

<body>
    <!-- относительно этого элемента
         выводится тестовый отчет -->
    <div id="mocha"></div>

    <!-- конфигурируем и запускаем тесты -->
    <script src="path/to/mocha.js"></script>
    <script>mocha.setup('bdd');
            mocha.run();</script>
</body>
                

path/to/mocha


"./node_modules/mocha/mocha.css"
"./node_modules/mocha/mocha.js"
                

"https://cdnjs.cloudflare.com/ajax/libs/mocha/5.1.0/mocha.css"
"https://cdnjs.cloudflare.com/ajax/libs/mocha/5.1.0/mocha.js"
                

Assert?


const assert = require('assert');
                

chaijs ⭐️ 5276


<script src="http://chaijs.com/chai.js"></script>
                

<script>
    chai.assert.equal(1 + 1, 2);
</script>
                

chai.expect


chai.expect(1 + 1).to.equal(2);
chai.expect(Boolean(1)).to.be.true;
                

chai.should


chai.should();

[1, 2, 3].should.deep.equal([1, 2, 3]);
[1, 2, 3].should.have.length(3);
[1, 2, 3].should.be.an('array');
        

Тесты


describe('email', () => {
    it('should success when input correct email', () => {
        const email = document.getElementById('email');

        email.value = 'ivan@example.com';

        chai.assert(email.checkValidity());
    });
});
                

Итог


<!DOCTYPE html>
<html lang="en">
<head>
    <title>Email</title>
    <link href="node_modules/mocha/mocha.css" rel="stylesheet"/>
</head>
<body>
    <input id="email" type="email" placeholder="Ваш email" required>

    <div id="mocha"></div>

    <script src="node_modules/mocha/mocha.js"></script>
    <script src="node_modules/chai/chai.js"></script>
    <script>
        mocha.setup('bdd');
        describe('Email', () => {
            it('should success when input correct email');
        });
        mocha.run();
    </script>
</body>
</html>

                

Запускаем

Плюсы

  • Просто писать
  • Быстро выполняются
  • Запускаются на любом реальном браузере

Минусы

  • Отдельная сборка с тестами
  • Приходится запускать руками
  • Нельзя автоматизировать запуск
  • Сложно совершать действия
  • В рамках одной страницы

Запуск в консоли

puppeteer

puppeteer ⭐️ 30444

Запускаем


npm i mocha-headless-chrome --save-dev

node_modules/.bin/mocha-headless-chrome
    -f path/to/test/file.html

Email
    ✓ should success when input correct email
    ✓ should failed when input incorrect email


  2 passing (22ms)
                

Плюсы

  • Просто писать
  • Быстро выполняются
  • Запуск автоматизируется

Минусы

  • Отдельная сборка с тестами
  • Запускается только в одном браузере
  • Сложно совершать действия
  • В рамках одной страницы

Почитать

Задача «Крестики-нолики»