Thomas Liu October 8, 2015
describe('the formatISODate function', () => {
it('should convert a mm/dd/yyyy date to ISO8601 format', () => {
let date = '09/30/2015';
// more traditional asserts
assert.isEqual(formatISODate(date), '2015-09-30T07:00:00.000Z');
// .should, modifies Object.prototype!
formatISODate(date).should.equal('2015-09-30T07:00:00.000Z');
// nicer than asserts, but without modifying Object.prototype
// like .should does
expect(formatISODate(date)).to.be('2015-09-30T07:00:00.000Z');
});
});
describe('yumm mocha', () => {
let somethingCool;
// runs once at the start of this describe block, does not re-run per nested describe
before(() => { somethingCool = new DbClient(); })
// runs before each test (each "it"), including tests in nested describes
beforeEach(() => { somethingCool.reset(); }); // (1)
afterEach(() => { console.log('a test has finished!'); }
after(() => { /* do some final cleanup */ });
it('should do something cool', () => {});
describe('a submodule', () => {
beforeEach(() => {}); // (2)
it('should do something cooler', () => {}); // both of the (1) and (2) hooks are run
});
});
let userIds = [
'qjwKKjEYbEqgtwD3BYHhww',
'tDPbT6FSn0WyyV_29bI_Zw',
'97TDtHncF0y2Z5oQAi48zQ',
'WA4d7b-kaEy-XNIpw34opQ'
];
for (let i=0; i < usersIds.length; i++) {
let id = userIds[i]
, request = new XMLHttpRequest();
// make a synchronous HTTP GET call
request.open('GET', `/users/${id}`, false /* switches off async */);
// at this point, the entire thread is just sitting around for
// the network IO to finish instead of doing other work!
(request.status == 200) && console.log(request.responseText);
});
let userIds = [
'qjwKKjEYbEqgtwD3BYHhww',
'tDPbT6FSn0WyyV_29bI_Zw',
'97TDtHncF0y2Z5oQAi48zQ',
'WA4d7b-kaEy-XNIpw34opQ'
];
for (let i=0; i < usersIds.length; i++) {
let id = userIds[i]
, request = new XMLHttpRequest();
// make a synchronous HTTP GET call
request.open('GET', `/users/${id}`, false /* switches off async */);
// at this point, the entire thread is just sitting around for
// the network IO to finish instead of doing other work!
(request.status == 200) && console.log(request.responseText);
});
The loop completely freezes the webpage (not just the slideshow) until it finishes all 4 requests!
let userIds = [
'qjwKKjEYbEqgtwD3BYHhww',
'tDPbT6FSn0WyyV_29bI_Zw',
'97TDtHncF0y2Z5oQAi48zQ',
'WA4d7b-kaEy-XNIpw34opQ'
];
userIds.forEach(id => {
let request = new XMLHttpRequest();
// make an async HTTP GET call
request.open('GET', `/users/${id}`, true);
// let the thread deal with something else for now, such as handle the user
// scrolling down the webpage (kind of important right?) and refreshing our slideshow
request.onload(() =>
// an event fired signalling that the server responded,
// only now do we come back to deal with it
(request.status == 200) && console.log(request.responseText)
);
});
let userIds = [
'qjwKKjEYbEqgtwD3BYHhww',
'tDPbT6FSn0WyyV_29bI_Zw',
'97TDtHncF0y2Z5oQAi48zQ',
'WA4d7b-kaEy-XNIpw34opQ'
];
userIds.forEach(id => {
let request = new XMLHttpRequest();
request.open('GET', `/users/${id}`, true);
request.onload(() =>
(request.status == 200) && console.log(request.responseText)
);
});
// define the function
function findUserAsync(id, callback) {
console.log(`I’m Mr. Meeseeks 1`);
// assume this is a db IO op and takes significantly longer than code execution
User.find({id}).exec(user => {
console.log(`I’m Mr. Meeseeks 2`);
callback(user);
});
console.log(`I’m Mr. Meeseeks 3`);
}
// actual things start happening here
console.log(`I’m Mr. Meeseeks 4`);
findUserAsync(23932, user => {
console.log(`I’m Mr. Meeseeks 5`);
});
What gets printed to the console?
Answer (just the ordering): 4, 1, 3, 2, 5
function findUserWithPromises(id) {
console.log(`I’m Mr. Meeseeks 1`);
return User.find({id}).exec();
}
console.log(`I’m Mr. Meeseeks 2`);
let promise = findUserAsync(23932).then(user => {
console.log(`I’m Mr. Meeseeks 3`);
return `I’m Mr. Meeseeks 4`;
}).then(text => {
console.log(text);
return findUserAsync(1032);
})
console.log(`I’m Mr. Meeseeks 5`);
promise.then(user => {
console.log(`I’m Mr. Meeseeks 6`);
return Promise.reject(new Error(`I’m Mr. Meeseeks 7`));
}).then(() => {
console.log(`I’m Mr. Meeseeks 8`);
}.catch(err => {
console.log(err.message);
});
console.log(`I’m Mr. Meeseeks 9`);
What gets printed to the console?
Answer (just the ordering): 2, 5, 9, 1, 3, 4, 1, 6, 7
// with promises
function createUser(email, password) {
return User.find({email}).then(existingUsers => {
if (existingUsers.length > 0) {
return Promise.reject(false);
}
return bcrypt.hashAsync(password, 15);
}).then(hashedPassword => {
let user = new User(email, hashedPassword);
return user.save().then(() => {
console.log(`user with email ${email} created!`);
return user;
});
});
}
// with async/await
async function createUser(email, password) {
// make sure there isn’t already a user with this email
let existingUsers = await User.find({email});
if (existingUsers.length > 0) {
return false;
}
let hashedPassword = await bcrypt.hashAsync(password, 15)
, user = new User(email, hashedPassword);
await user.save();
console.log(`user with email ${email} created!`);
return user;
}
function findUserAsync(id, callback) {
User.find({id}).exec(user => callback(user));
}
// we could try testing like this:
findUserAsync(23430, user =>
expect(user.email).to.equal(`bob@gmail.com`)
);
What's the problem with this?
What if our function never actually calls our callback? The
test wouldn't even run (automatic pass)!
Or imagine that the callback withdraws $50 from your bank account. We want to test
that the callback is only called ONE TIME.
Imagine we had a function that remembers the # of times its been called and any arguments passed to it. How can we implement this?
function simpleSpy() {
// we're referencing the function name here instead of 'this'
// because 'this' refers to the execution context, not the fn;
// alternatively, you could bind 'this' to 'simpleSpy'
simpleSpy.argsHistory = simpleSpy.argsHistory || [];
simpleSpy.timesCalled = simpleSpy.timesCalled || 0;
simpleSpy.timesCalled += 1;
for (let i = 0; i < arguments.length; i++) {
simpleSpy.argsHistory.push(arguments[i]);
}
console.log(`Somebody called me!`);
}
Now we can use this spy to test our function properly.
describe(`the findUserAsync function`, () => {
it(`should be able to find a user by id`, () => {
findUserAsync(23430, simpleSpy);
setTimeout(() => {
expect(simpleSpy.timesCalled).to.equal(1);
expect(simpleSpy.argsHistory[0].email).to.equal(`bob@gmail.com`);
}, 1000);
});
});
Note: this is just an example for simplicity’s sake; don’t do setTimeouts during unit tests in practice
describe(`the findUserAsync function`, () => {
it(`should be able to find a user by id`, () => {
let callback = sinon.spy();
findUserAsync(29034, callback);
expect(callback).to.have.been.calledOnce;
// deep equality check on the argument passed to our callback
expect(callback).to.have.been.calledWithMatch({
email: `bob@gmail.com`,
hashedPassword: `s8984932jdadfghj`, // not a real hash lol
name: `Bob`
};
});
});
// findUser.js
export function findUserWithPromises(id) {
return User.find({id}).exec();
}
// findUser.spec.js
import { findUserWithPromises } from './findUser';
describe(`the findUserWithPromises function`, () => {
it(`should be able to find a user by id`, () => {
let userPromise = findUserWithPromises(53094);
expect(userPromise).to.eventually.deep.equal({
email: `bob@gmail.com`,
hashedPassword: `s8984932jdadfghj`,
name: `Bob`
});
});
});
// myFilters.js
angular.module('myFilters', [])
.filter('formatDate', () => (collection, property) => {
/* cool things here */
});
// myFilters.spec.js
describe('Custom filters module', () => {
// make all components of 'myFilters' available during tests
beforeEach(() => module('myFilters'));
// $filter, $controller, $directive allow you to load the
// respective components by their angular names
it('should have a formatDate function', inject($filter =>
expect($filter('formatDate')).to.not.be.null
));
// the above translates to the following ES5
it('should have a dateFilter function', inject(function($filter) {
expect($filter('formatDate')).to.not.be.null;
}));
});
// myFilters.spec.js
describe('Custom filters module', () => {
// make all components of 'myFilters' available during tests
beforeEach(() => module('myFilters'));
/* truncated */
describe('Filter: formatDate', () => {
/* injecting filterName + 'Filter' will resolve the filterName filter,
a little shortcut to $filter('filterName')
NOTE: this is specific to filters; with services for example,
just the service's name itself suffices */
it('should format dates to be MMMM d, yyyy', inject(formatDateFilter =>
expect(formatDateFilter('09/05/2015')).to.be('September 5, 2015');
);
});
});
Instead of injecting a dependency once for every tests, we can just assign it to a variable
// myFilters.spec.js
describe('The Forms Service', () => {
let FormsService;
// make the forms module available
beforeEach(() => module('forms'));
// inject and save the FormsService; angular lets us lead & trail the dep name
// with _s, so it doesn't shadow our var of the same name from above
beforeEach(inject(_FormsService_ => {
FormsService = _FormsService_;
});
// now every test has access to the service without re-injecting
it('allows me to retrieve the most recent forms', () => {
let forms = FormsService.getForms();
expect(forms).to.have.length.within(3,10);
});
});
Services may rely on unpredictable network I/O, so here comes the mocks.
// just an example, having other service objects wholly passed into
// a generic ModalsService wouldn't be the best design in practice
describe('The ModalsService', () => {
let ModalsService
, FormsService;
beforeEach(() => module('modals'));
beforeEach(inject(_ModalsService_ => ModalsService = _ModalsService_));
// create a fake object containing the mocked FormsService with the fn we need
beforeEach(() => FormsService = {
getData: () => ([
{ name: 'A form name', date: '2015-06-05' },
{ name: 'Another form name', date: '2016-07-03' }
])
});
it('creates a new modal from a service with the expected props', () => {
let modal = ModalsService.createFromService(FormsService);
expect(modal).to.have.all.keys('name', 'date', 'width', 'height');
});
});