Подмены. Управление временем. Интеграционные тесты. Тестирование в браузере
Жигалов Сергей
функция, котороя запоминает для каждого вызова: с какими аргументами её вызвали, какое значение вернули и произошло ли исключение.
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(() => {
console.log('Прошла секунда');
}, 1000);
console.log('Длинное предложение');
console.log('в одну строку.');
Длинное предложение
в одну строку.
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();
});
});
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);
});
});
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()
.
тестирование группы взаимодействующих модулей
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, 'Ничья');
});
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 >
<!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"
const assert = require('assert');
<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>
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)