Skip to content

Instantly share code, notes, and snippets.

@hmil
Created November 15, 2017 11:28
Show Gist options
  • Save hmil/8af7e5c14939bec9818b000b6ac86dba to your computer and use it in GitHub Desktop.
Save hmil/8af7e5c14939bec9818b000b6ac86dba to your computer and use it in GitHub Desktop.
Asynchronous testing with jest and typescript
class SyncDB {
// ... attributes omitted for brievety
public remove<T extends ISyncEntity>(entity: ISavedDocument<T>): Promise<void> {
return this.lock.runExclusive( async () => {
await this.db.remove(entity);
const mutation = deleteMutation(entity, this.mutationSeq++);
await this.db.post(mutation);
return entity._id;
});
}
}
// ... definitions omitted for brievety
// This test case tests an asynchronous sequence of events.
// The sequence is precisely controlled and the test itself reads chronologically top to bottom.
describe('.remove', () => {
it('removes a document from the local pouch', async () => {
// "Block" those functions until we manually tell them to resume
const localDBRemove = pauseOn(localDB.remove);
const localDBPost = pauseOn(localDB.post);
// Execute the function under test
const result = syncDB.remove({...savedEntity});
// Wait for the function to hit the first hooked call
await localDBRemove.then((resume) => {
// Here we can make assertions while the function is awaiting on the result of the first call
expect(localDB.remove).toHaveBeenLastCalledWith(savedEntity);
// Resolve the first hooked call with some success
resume.withSuccess({
id: savedEntity._id,
ok: true,
rev: savedEntity._rev
});
});
// Wait for the function under test to hit the second hooked call
await localDBPost.then((resume) => {
expect(localDB.post).toHaveBeenLastCalledWith({
type: 'mutation',
mutationType: 'delete',
seq: 0,
docId: savedEntity._id,
docRev: savedEntity._rev
});
resume.withSuccess({
id: 'mut1',
ok: true,
rev: '1.0'
});
});
// We can now await the function to compute the final result. This will eventually resolve
// because all of our blocking hooks have been dealt with.
expect(await result).toEqual(savedEntity._id);
});
// TODO: test all possible failure scenarios and assert recovery
});
/* These utilities help manipulate time by controlling the order of resolution of promises */
export interface IResume<T> {
withSuccess: (val: T) => void;
withFailure: (reason: any) => void;
}
export function pauseOn<T>(fn: (...args: any[]) => Promise<T>): PromiseLike<IResume<T>> {
const mock = fn as jest.Mock<void>;
return new Promise((resolve) => {
mock.mockImplementationOnce(() => new Promise<T>((success, failure) => {
resolve({
withFailure: failure,
withSuccess: success
});
}));
});
}
export class LatchedPromise<T> extends Promise<T> {
public didFire: boolean = false;
}
export function latch<T>(p: PromiseLike<T>): LatchedPromise<T> {
const ret = new LatchedPromise<T>((resolve, reject) => {
p.then((_) => {
ret.didFire = true;
resolve(_);
}, (_) => {
ret.didFire = true;
reject(_);
});
});
return ret;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment