Last active
June 7, 2018 17:38
-
-
Save Pyrolistical/74a6591394cde390b0be80f603363fcb to your computer and use it in GitHub Desktop.
ES6 module unit testing?
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
The pattern you showed at ployglotconf: | |
import a from './a' | |
import b from './b' | |
import c from './c' | |
export const xyz = (a, b, c) => { | |
return { | |
method() { | |
...use deps a, b, c | |
} | |
} | |
} | |
export default xyz(a, b, c) | |
This makes sense for unit testing xyz, now I can inject mock a, b, c. | |
But what doesn't make sense is how you handle dependencies that have a lifecycle, for example a database connection. | |
Let's say we are using mongodb, and using your pattern we create a db.js: | |
import {MongoClient} from 'mongodb' | |
import {mongoUri} from './config' | |
export const connect(mongoUri) => MongoClient.connect(mongoUri) | |
export default connect(mongoUri) | |
Now a consumer (repository.js) of db.js would look like: | |
import dbPromise from 'db' | |
export const repository = (dbPromise) => { | |
return { | |
async findUserByID(userID) { | |
const db = await dbPromise | |
... use db | |
} | |
} | |
} | |
export default repository(dbPromise) | |
Having to write const db = await dbPromise in every method is annoying, so we can change the export default to a function: | |
import dbPromise from 'mongodb' | |
export const repository = (db) => { | |
return { | |
async findUserByID(userID) { | |
... use db | |
} | |
} | |
} | |
export default (async () => repository(await dbPromise))() | |
But now the consumer of repository.js has a problem that the default export is a promise. This is getting out of hand. | |
How do you solve this? | |
We currently solve by writing everything in the top level entry point (usually server.js), this way we can do this: | |
import {MongoClient} from 'mongodb' | |
import Express from 'express' | |
import {mongoUri} from './config' | |
import Repository from './repository' | |
import Service from './service' | |
import Controller from './controller' | |
async function main() { | |
const db = await MongoClient.connect(mongoUri) | |
const repository = Repository(db) | |
const service = Service(repository) | |
const controller = Controller(service) | |
const application = Express() | |
application.use(controller) | |
application.listen... | |
} | |
main() | |
.catch(...) | |
Then our repository.js looks like: | |
export default (db) => { | |
return { | |
async findUserByID(userID) { | |
..use db | |
} | |
} | |
} | |
We can unit test repository.js by providing our own db. | |
But enough about our solution, how do you handle db in your solution? |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Yes that is pretty close. I think
query.js
can be made a bit better. You'll find it is difficult to test as you can't substitutedbPromise
. Wrappingfind
in a constructor that takesdb
as a parameter and is composed by default withdbPromise
will solve that for you.Also dbPromise is a more complex substitute because of the chaining it requires, so you'll end up needing to define a more complex mock, rather than just using a simple stub substitute. That might be unavoidable here though and by creating the
find
module you can isolate the problem to just one place simplifying your module composition and testing everywhere else.But it is a good example of why methods on objects and chaining aren't a great idea from a testability perspective.