by Artem Miroshnyk
What to use?
What to use? ;)
const framework1 = {
name: 'name',
githubStars: 1000,
};
const framework2 = {
name: 'name',
githubStars: 1000,
};
// Jasmine and Jest
describe('Difference between js test frameworks', () => {
it('have all the same properties', () => {
expect(framework1).toEqual(framework2);
});
it('are not the exact same can', () => {
expect(framework1).not.toBe(framework2);
});
});
// Mocha with Chai
describe('Difference between js test frameworks', () => {
it('have all the same properties', () => {
expect(framework1).to.eqls(framework2); // Or .to.deep.equal()
});
it('are not the exact same can', () => {
expect(framework1).to.not.equal(framework2);
});
});
// Mocha+Chai+Sinon
const fn = sinon.spy();
const result = fn(1, 2);
expect(fn).to.have.been.calledWith(1, 2);
// Jest
const fn = jest.fn();
fn(1, 2);
expect(fn.mock.calls).toEqual([1, 2]);
// Jasmine
const fn = jasmine.createSpy();
fn(1, 2);
expect(fn.calls.last().args).toEqual([1, 2]); Drupal.t = function (str, args, options) {
options = options || {};
options.context = options.context || '';
// Fetch the localized version of the string.
if (typeof drupalTranslations !== 'undefined' && drupalTranslations.strings
&& drupalTranslations.strings[options.context]
&& drupalTranslations.strings[options.context][str]
) {
str = drupalTranslations.strings[options.context][str];
}
if (args) {
str = Drupal.formatString(str, args);
}
return str;
};External global variables, external function, requires mocking data and functions
Drupal.behaviors.blockSettingsSummary = {
attach: function () {
if (typeof $.fn.drupalSetSummary === 'undefined') {
return;
}
function checkboxesSummary(context) {
var vals = [];
var $checkboxes = $(context).find('input[type="checkbox"]:checked + label');
var il = $checkboxes.length;
for (var i = 0; i < il; i++) {
vals.push($($checkboxes[i]).html());
}
if (!vals.length) {
vals.push(Drupal.t('Not restricted'));
}
return vals.join(', ');
}
$('[data-drupal-selector="edit-visibility-node-type"], [data-drupal-selector="edit-visibility-language"], [data-drupal-selector="edit-visibility-user-role"]').drupalSetSummary(checkboxesSummary);
$('[data-drupal-selector="edit-visibility-request-path"]').drupalSetSummary(function (context) {
var $pages = $(context).find('textarea[name="visibility[request_path][pages]"]');
if (!$pages.val()) {
return Drupal.t('Not restricted');
}
else {
return Drupal.t('Restricted to certain pages');
}
});
}
};External global variables, external function, requires mocking data and functions, jQuery, HTML, jQuery plugins
import fetch from 'whatwg-fetch';
function checkStatus(response) {
if (response.status >= 200 && response.status < 300) {
return response
} else {
var error = new Error(response.statusText)
error.response = response
throw error
}
}
fetch('/user', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
name: 'Hubot',
login: 'hubot',
}))
.then(checkStatus)
.then(response => response.json())
.then(function(data) {
console.log('request succeeded with JSON response', data)
}).catch(function(error) {
console.log('request failed', error)
})ES6, Networking, Promises, mocking, imports
describe('calculator', () => {
describe('add()', () => {
it('should add 2 numbers together', () => {
// assertions here
});
it('should more then 2 numbers together', () => {
// assertions here
});
});
describe('multiply()', () => {
it('should multiply 2 numbers together', () => {
// assertions here
})
});
});Test suit and test case
describe('hooks', function() {
before(function() {
window.testProperty = {};
});
after(function() {
delete window.testProperty;
});
beforeEach(function() {
window.testProperty = {value: 'value'};
});
afterEach(function() {
// runs after each test case.
});
});import chai, {expect} from 'chai';
describe('hooks', function () {
const delayedIncrement = (counter) =>
new Promise(resolve => setTimeout(() => resolve(++counter), 3000));
it('supports timers', (done) => {
let counter = 0;
const promise = delayedIncrement(counter);
expect(counter).to.eqls(0);
promise.then(counter => {
expect(counter).to.eqls(3);
done();
});
});
});import chai, {expect} from 'chai';
import chaiAsPromised from 'chai-as-promised';
chai.use(chaiAsPromised);
describe('hooks', function () {
const delayedIncrement = (counter) =>
new Promise(resolve => setTimeout(() => resolve(++counter), 3000));
it('supports timers', () => {
let counter = 0;
const promise = delayedIncrement(counter);
expect(counter).to.eqls(0);
expect(promise).to.eventually.eqls(1);
});
});import chai, {expect} from 'chai';
import sinon from 'sinon';
import sinonChai from 'sinon-chai';
chai.should();
chai.use(sinonChai);
describe('spy', function () {
it('supports spies', () => {
const fn = (a, b) => a + b + (a * b) + (a / b);
const spy = sinon.spy(fn);
spy.should.not.have.been.called;
expect(spy(2, 1)).to.eqls(7);
spy.should.have.been.calledOnce;
expect(spy).to.have.been.calledWith(2, 1);
spy(3, 4);
expect(spy.getCall(1).args).to.eqls([3, 4]);
expect(spy.getCall(1).exception).to.eqls(undefined);
expect(spy.getCall(1).returnValue).to.eqls(19.75);
});
});Do not change function behaviour, just watches and logs everything
import jQuery from 'jquery';
describe('spy on method', function () {
const apiRequest = url => jQuery.ajax(url);
it('supports spies on methods', () => {
sinon.spy(jQuery, 'ajax');
// jQuery still will do ajax request
apiRequest('/some/url');
expect(jQuery.ajax).to.have.been.calledWith('/some/url');
});
});import jQuery from 'jquery';
describe('spy on method', function () {
const apiRequest = url => jQuery.ajax(url);
it('supports spies on methods', () => {
sinon.stub(jQuery, 'ajax');
jQuery.ajax.withArgs('/path1').returns(1);
jQuery.ajax.withArgs('/path2').throws("TypeError");
// There won't be any real ajax request.
expect(apiRequest('/path1')).to.eqls(1);
expect(jQuery.ajax).to.have.been.calledWith('/path2');
expect(apiRequest('/path2')).to.throw(new TypeError);
expect(apiRequest('/path3')).to.eqls(undefined);
});
});Stubs are like spies, except in that they replace the target function. They can also contain custom behavior, such as returning values or throwing exceptions.
import jQuery from 'jquery';
describe('spy on method', function () {
const apiRequest = url => jQuery.ajax(url);
it('supports spies on methods', () => {
const mock = sinon.mock(jQuery);
mock.expects("ajax").once()
.withExactArgs('/path, {method: 'POST'});
apiRequest('/path')
// It will fail because it misses second argument.
mock.verify();
});
});Mocks are fake methods (like spies) with pre-programmed behavior (like stubs) as well as pre-programmed expectations.
document.body.innerHTML = '<html></html>';import chai, {expect} from 'chai';
import $ from 'jquery';
describe('spy', function () {
it('supports jquery', () => {
document.body.innerHTML =
'<div>' +
'<span id="username">Username</span>' +
'<button id="button">Login</button>' +
'</div>';
$('#username').after('<span id="password">Password</span>');
expect(document.body.innerHTML).to.eqls('<div>' +
'<span id="username">Username</span>' +
'<span id="password">Password</span>' +
'<button id="button">Login</button>' +
'</div>');
});
});sendPing: function () {
if (this.timer) {
clearTimeout(this.timer);
}
if (this.uri) {
var pb = this;
// When doing a post request, you need non-null data. Otherwise a
// HTTP 411 or HTTP 406 (with Apache mod_security) error may result.
var uri = this.uri;
if (uri.indexOf('?') === -1) {
uri += '?';
}
else {
uri += '&';
}
uri += '_format=json';
$.ajax({
type: this.method,
url: uri,
data: '',
dataType: 'json',
success: function (progress) {
// Display errors.
if (progress.status === 0) {
pb.displayError(progress.data);
return;
}
// Update display.
pb.setProgress(progress.percentage, progress.message, progress.label);
// Schedule next timer.
pb.timer = setTimeout(function () { pb.sendPing(); }, pb.delay);
},
error: function (xmlhttp) {
var e = new Drupal.AjaxError(xmlhttp, pb.uri);
pb.displayError('<pre>' + e.message + '</pre>');
}
});
}
},
describe("Drupal get progress test", function () {
let server;
const fakeData = {
status: 1,
data: "",
message: "Everything is fine so far",
label: "Label string",
percentage: 15
};
const url = '/progress-bar';
before(function () {
server = sinon.fakeServer.create();
server.respondWith(
"GET",
url,
[200, { "Content-Type": "application/json" }, JSON.stringify(fakeData)]
);
});
after(function () { server.restore(); });
})describe("Drupal get progress test", function () {
it("should set progress and timer", () => {
const progressBar = new Drupal.ProgressBar(
'uniqueId',
() => (),
'GET',
() => ()
);
const stub = sinon.stub(progressBar.prototype, 'setProgress');
progreesBar.sendPending();
expect(stub.getCall(0).args).to.eqls([
15,
'Everything is fine so far',
'Label string'
]);
});
});$ mocha --compilers js:babel/registeror bootstrap.js file and
require('babel-register')();const Ticket = ({ ticket, inputChange }) => {
const onChange = function (name, value) {
value = typeof(value) == 'boolean' ? (value ? 1 : 0) : value;
inputChange(name, value);
};
let input;
if (ticket.maxUnits === 1) {
input = (<CheckboxInput
name={ticket.serviceId}
onChange={onChange}
value={!!ticket.unitCount}
/>);
}
else {
input = (<NumberInput
loadingWrapClass="loading-wrap-service-input"
name={ticket.serviceId}
onChange={onChange}
value={ticket.unitCount}
max={ticket.maxUnits}
/>);
}
return (
<ul className="service">
<li className="service__name">{ticket.name}</li>
<li className="service__tooltip">
<Tooltip><span>{ticket.description}</span></Tooltip>
</li>
<li className="service__input">{input}</li>
</ul>
);
};import React from 'react';
import {shallow, mount} from 'enzyme';
import chai, {expect} from 'chai';
import sinon from 'sinon';
import sinonChai from 'sinon-chai';
import mergeOptions from 'merge-options';
import NumberInput from '../../components/NumberInput';
import CheckboxInput from '../../components/formComponents/CheckboxInput';
import Ticket from '../../components/Ticket';
import Tooltip from '../../components/Tooltip';
chai.use(sinonChai);
describe('<Ticket />', () => {
const getProps = () => ({
ticket: {
serviceId: "qwer1234",
priceUnitType: "PER_STAY",
maxUnits: 1,
unitCount: 0,
name: "Child chair",
description: "High chair",
unitPrice: {
amount: 100.5,
currencyCode: "EUR"
},
minimumPrice: {
amount: 604.8,
currencyCode: "EUR"
},
},
inputChange: sinon.spy()
});
});
describe('<Ticket />', () => {
it('Renders 4 li with appropriate content', () => {
const props = getProps();
let wrapper = shallow(<Ticket {...props} />);
expect(wrapper.find('li')).to.have.length(4);
expect(wrapper.find('.service__name').text()).to.equal(props.ticket.name);
expect(wrapper.find('.service__tooltip').containsMatchingElement(<Tooltip>
<span>{props.ticket.description}</span>
</Tooltip>)).to.equal(true);
expect(wrapper.find('.service__input').find(CheckboxInput)).to.have.length(1);
});
});
describe('<Ticket />', () => {
it('should handle onChange', () => {
const props = getProps();
const wrapper = mount(<Ticket {...props} />);
props.inputChange.should.not.have.been.called;
wrapper.find('input').simulate('change', { target: { checked: true }});
props.inputChange.should.have.been.calledOnce;
expect(props.inputChange).to.have.been.calledWith(props.ticket.serviceId, 1);
});
});
describe('<Ticket />', () => {
it('should render proper input', () => {
const props = getProps();
let wrapper = shallow(<Ticket {...props} />);
expect(wrapper.find(CheckboxInput).length).to.eqls(1);
expect(wrapper.find(CheckboxInput).prop('name')).to.eqls(props.ticket.serviceId);
expect(wrapper.find(CheckboxInput).prop('value')).to.eqls(!!props.ticket.unitCount);
props.ticket.maxUnits = 10;
wrapper = shallow(<Ticket {...props} />);
expect(wrapper.find(CheckboxInput).length).to.eqls(0);
expect(wrapper.find(LoadableNumberInput).length).to.eqls(1);
expect(wrapper.find(LoadableNumberInput).prop('name')).to.eqls(props.ticket.serviceId);
expect(wrapper.find(LoadableNumberInput).prop('value')).to.eqls(props.ticket.unitCount);
expect(wrapper.find(LoadableNumberInput).prop('max')).to.eqls(props.ticket.maxUnits);
});
});
BTW, Novasol is hiring Drupal 8 developers, Front-end developer and Java developers