Consider the faux account creation code below:
export const accounts = [];
export const emails = [];
export async function createAccount(name, email) {
accounts.push({name, email});
void sendEmail(email);
}
async function sendEmail(email) {
setTimeout(0, () => emails.push(`Account created to ${email}`));
}
The createAccount()
function "saves" an account in the array, and then "sends an email" reporting the account was created. Note that createAccount()
does not wait for the email to be sent, since it can take some time.
Now, we write a test to ensure the email is sent:
import {assert} from 'chai';
import {createAccount, emails} from './index.js';
describe('test', async () => {
it('test', async() => {
await createAccount('John Doe', '[email protected]');
assert(emails[0], 'Account created to [email protected]');
});
});
Of course, though, the test does not pass…
$ npm run test
> [email protected] test
> mocha test.js
test
1) test
0 passing (5ms)
1 failing
1) test
test:
AssertionError: Account created to [email protected]
at Context.<anonymous> (file:///home/me/sandbox/foo/test.js:7:5)
…because the sendEmail()
function, being called asynchronously, did not yet "send" the email.
But I really want to test if the email was eventually sent!
How could I test that?
Ideally, it would not:
- involve exposing the
sendEmail()
return value; - involve sleeps/timeouts.
Any ideas?
Appendix
To make things easier to test, you can use this package.json
:
{
"name": "foo",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "mocha test.js"
},
"type": "module",
"author": "",
"license": "ISC",
"devDependencies": {
"chai": "^5.1.1",
"mocha": "^10.6.0"
}
}
2
Answers
emailSentPromise
property separate from the main returned Promise.sendEmail
instead of the default one.Make
createAccount
return a promise for the result of the account creation. That result contains another promise for the result of the email being sent. This follows thefetch
pattern where the response object represents only the headers and one needs to wait for the contents of the body separately.Alternatively, instead of having a promise on the result which someone "should" await, you can have a
notify
method that the caller can (but doesn’t need to) call and can await or forget.This puts more responsibility on the caller to not forget
.notify()
of course. If you need, provide another helper method wrapping this behaviour.If you don’t want to expose those internals at all, you can of course still test these internals separately. Export
saveAccount
andsendEmail
from the implementation module, test them separately. Then for testing the publiccreateAccount
, just mock them and check they are being called. This way, you can even test thatcreateAccount
does in fact not wait forsendEmail
.