Автотесты

Sinon fake timer. Интеграционные тесты. Supertest. Тестирование в браузере

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

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

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

setTimeout


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

console.log


console.log('Несколько');
console.log('строк');
        

Несколько
строк
        

process.stdout.write


process.stdout.write('Одна');
process.stdout.write('строка');
        

Однастрока
        

Решение


// crawline.js

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

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

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

Тест


// tests/crawline-test.js

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

describe('Crawline', () => {
    it('shoult ptint text', done => {
        crawline('I don’t always bend time and ' +
            'space in unit tests, but when I do, ' +
            'I use Buster.JS + Sinon.JS', done);
    });
});
        

npm test

setTimeoutFailed

npm test -- --timeout=30000

setTimeoutLong

Faking time


beforeEach(() => clock = sinon.useFakeTimers());
afterEach(() => {
    clock.restore();
    process.stdout.write.restore();
});
        

it('shoult ptint text', done => {
    const write = sinon.spy(process.stdout, 'write');

    crawline('I don’t always bend time and ' +
        'space in unit tests, but when I do, ' +
        'I use Buster.JS + Sinon.JS', () => {
            assert.equal(write.callCount, 91);
            done();
        });

    clock.tick(10000);
});
        

    Crawline
I don’t always bend time and space in unit tests,
but when I do, I use Buster.JS + Sinon.JS
    ✓ shoult ptint text


    1 passing (32ms)
        

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

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


// tests/formatDate-test.js
const sinon = require('sinon');

describe('Format date', () => {
    let clock;

    before(function () {
        const startDate = new Date(2017, 3, 25).getTime();
        clock = sinon.useFakeTimers(startDate);
    });
    after(() => clock.restore());

    /* ... */
});
        

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


it('should return only time', function () {
    var actual = formatDate(new Date(2017, 3, 25, 6, 17, 6));

    expect(actual).to.equal('06:17');
});

it('should return `вчера` with time', function () {
    var actual = formatDate(new Date(2017, 3, 24, 8, 17, 6));

    expect(actual).to.equal('вчера в 08:17');
});
        

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

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

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

marsRover

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


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

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


// tests/poker-test.js
it('should print success result', () => {
    const log = sinon.stub(console, 'log');
    const error = sinon.stub(console, 'error');

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

    assert(log.calledOnce);
    assert(log.calledWith('Ничья'));
    assert(!error.called);
});
        

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


 function weather(cb) {
     request(url, (requestError, res, body) => {
         if (requestError || res.statusCode !== 200) {
             return cb('Request error');
         }

         try {
             const data = JSON.parse(body);
             cb(null, data.fact.temp);
         } catch (parseError) {
             cb(parseError.message);
         }
     });
 }
         

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


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

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


// tests/weather-test.js

it('should print temperature', done => {




    weather((error, actual) => {
        assert(Number.isInteger(actual));
        done(error);
    });
});
        

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

  • Нужные
  • Медленные
  • Нестабильные

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

Конвертер Условие

Написать универсальный конвертер величин.

Конвертер Примеры

  • 1000mm → 1m
  • 1m → 3.281ft
  • 1.5week → 15120min

Конвертер Библиотека


            npm install convert-units --save
        

const convert = require('convert-units')

convert(1.5)
    .from('week')
    .to('min'); // 15120
        

express


npm install express --save
        

const express = require('express');
const app = express();

app.get('/convert', function (req, res) {
    res.json({ result: 1 });
});

app.listen(3000);
        

Конвертер Решение


const express = require('express');
const convert = require('convert-units')
const app = express();

app.get('/convert', (req, res) => {
    const result = convert(req.query.value)
        .from(req.query.from)
        .to(req.query.to);
    res.json({ result });
});

app.listen(3000);
        

Конвертер Результат

Результат работы конвертера

supertest


npm install supertest --save-dev
        

Конвертер Разделить приложение


const express = require('express');
const convert = require('convert-units')
const app = express();

app.get('/convert', (req, res) => {
    const result = convert(req.query.value)
        .from(req.query.from)
        .to(req.query.to);
    res.json({ result });
});

app.listen(3000);
        

Конвертер Разделить приложение


// app.js
const express = require('express');
const convert = require('convert-units')
const app = express();

app.get('/convert', (req, res) => {
    const result = convert(req.query.value)
        .from(req.query.from)
        .to(req.query.to);
    res.json({ result });
});

module.exports = app;
        

Конвертер Разделить приложение


// index.js

require('./app').listen(3000);
        

Конвертер Тестирование результата


// tests/convertor-test.js

const request = require('supertest');
const app = require('../app');

describe('Convertor controller', () => {
    it('should return result', () => {
        /* ... */
    });
});
        

Конвертер Тестирование результата


it('should return result', () => {
    return request(app)
        .get('/convert')
        .query({ value: 1.5, from: 'week', to: 'min' })

        .expect(200)
        .expect('Content-Type', /json/)
        .expect({ result: 15120 });
});
        

Тестирование в браузере

Web-конвертер Условие

Реализовать веб-версию универсального конвертера величин.

Web-конвертер Решение


// app.js
const express = require('express');
const convert = require('convert-units');
const app = express();

app.use(express.static('public'));
app.get('/convert', (req, res) => {
    const result = convert(req.query.value)
        .from(req.query.from)
        .to(req.query.to);
    res.json({ result });
});

app.listen(3000);
        

Web-конвертер Общая сщема

Общая сщема

Web-конвертер Решение

DEMO

Подход I. Модульное тестирование

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


<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')</script>
    <script src="../tests/convertor-test.js"></script>
    <script>mocha.run();</script>
</body>
        

Подход I. Путь до mocha


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

"https://cdn.rawgit.com/mochajs/mocha/2.2.5/mocha.css"
"https://cdn.rawgit.com/mochajs/mocha/2.2.5/mocha.js"
        

Подход I. Assert


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

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

Подход I. Assert


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

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

Подход I. Тесты


describe('Convertor controls', () => {
    it('should enable `convert` button for correct value', () => {
        const send = document.getElementById('send');

        chai.assert.equal(send.getAttribute('disabled'), null);
    });
});
        

Подход I. Тесты


describe('Convertor controls', () => {
    it('should disable `convert` button for invalid value', () => {
        const from = document.getElementById('from');
        const send = document.getElementById('send');

        from.value = 'invalid value';
        from.dispatchEvent(new Event('input'));

        chai.assert.equal(send.getAttribute('disabled'), 'disabled');
    });
});
        

Подход I. Плюсы

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

Подход I. Минусы

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

Подход II. PhantomJS

phantomjs

PhantomJS is a headless WebKit scriptable with a JavaScript API.

mocha-phantomjs


npm install mocha-phantomjs --save-dev

node_modules/.bin/mocha-phantomjs
    -p node_modules/.bin/phantomjs
    https://urfu-2016-convertor.herokuapp.com/convert-test.html

Convertor controls
  ✓ should enable `convert` button for correct value
  ✓ should disable `convert` button for invalid value


2 passing (8ms)
        

Подход II. Плюсы

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

Подход II. Минусы

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

Подход III. Karma

Spectacular Test Runner for JavaScript

Karma. Установка


npm install karma --save-dev
        

npm install karma-mocha karma-chai --save-dev
        

npm install karma-chrome-launcher --save-dev
        

Karma. Конфигурация


node_modules/.bin/karma init karma.config.js
        
karma

Karma. подготовить скрипты


// convertor.js

const value = document.getElementById('value');
const from = document.getElementById('from');
const to = document.getElementById('to');
const send = document.getElementById('send');
const result = document.getElementById('result');

function isValid() { /* ... */ }
function change() { /* ... */ }
function convert()  { /* ... */ }
        

Karma. подготовить html


describe('Convertor', function () {
    beforeEach(function() {
        var fixture = `
from to
`; document.body.insertAdjacentHTML('afterbegin', fixture); }); });

Karma. тесты


it('should enable `convert` button for correct value', function() {
    const send = document.getElementById('send');

    chai.assert.equal(send.getAttribute('disabled'), null);
});

it('should disable `convert` button for invalid value', function() {
    const from = document.getElementById('from');
    const send = document.getElementById('send');

    from.value = 'invalid unit';
    from.dispatchEvent(new Event('input'));

    chai.assert.equal(send.getAttribute('disabled'), 'disabled');
});
        

Подход III. Плюсы

  • Запуск автоматизируется
  • В настоящих браузерах

Подход III. Минусы

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

Почитать