Created
November 15, 2017 11:28
-
-
Save hmil/8af7e5c14939bec9818b000b6ac86dba to your computer and use it in GitHub Desktop.
Asynchronous testing with jest and typescript
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
}); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ... 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 | |
}); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* 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