You have to refactor both test and production code. This means that you will move some of the test code that was on the Random class tests to the PromiseHelper tests.
Before refactoring:
describe('Random', () => {
beforeEach(() => {
jasmine.clock().install();
});
afterEach(() => {
jasmine.clock().uninstall();
});
it('should toss a value after a while', (done) => {
spyOn(Math, 'random').and.returnValue(0.5);
const tossCb = jasmine.createSpy('toss callback'); new Random().toss().then(tossCb);
expect(tossCb).not.toHaveBeenCalled();
jasmine.clock().tick(200);
// Promises are asynchronous,
// you need another delay before checking the result
Promise.resolve().then(() => {
expect(tossCb).toHaveBeenCalledWith(0.5);
done();
});
});
});
After refactoring:
describe('Random', () => {
let timeoutSpy;
beforeEach(() => {
const promiseMock = val => ({
then(cb) {
const newVal = cb(val);
return promiseMock(newVal);
}
});
timeoutSpy = spyOn(PromiseHelper, 'timeout')
.and.returnValue(promiseMock());
});
it('should use PromiseHelper.timeout(delay) to toss', () => {
new Random().toss();
expect(timeoutSpy).toHaveBeenCalledWith(200);
});
it('should toss a random number', () => {
spyOn(Math, 'random').and.returnValue(0.5);
const tossCb = jasmine.createSpy('toss callback'); new Random().toss().then(tossCb);
expect(tossCb).toHaveBeenCalledWith(0.5);
});
});
describe('PromiseHelper', () => {
beforeEach(() => {
jasmine.clock().install();
});
afterEach(() => {
jasmine.clock().uninstall();
});
it('should resolve after delay', async () => {
const timeoutCb = jasmine.createSpy('timeout'); PromiseHelper.timeout(100).then(timeoutCb);
expect(timeoutCb).not.toHaveBeenCalled();
jasmine.clock().tick(100);
// Promises are asynchronous,
// you need another delay before checking the result
await Promise.resolve();
expect(timeoutCb).toHaveBeenCalled();
});
});
What I did is creating a PromiseHelper.timeout(…) mock in order to test the Random class. This mock allowed me to make the tests about Random class synchronous. Test should be synchronous when possible and all the dependencies should be mocks.
To test PromiseHelper.timeout I used the typical jasmine.clock, useful whenever there is a timeout in the code. If you notice, because I failed to mock the Promise system, the test was asynchronous and I needed an hack (await Promise.resolve()) to make it work. Anyway I think this is acceptable, because mocking promises may make tests too complicate to write and read.
One last thing: timeouts and promises are difficult to test, “normal” unit tests are simpler.