mockzen
v0.1.7
Published
Make any piece of code testable! Easily mock any dependencies in your code during testing
Downloads
23
Maintainers
Readme
mockzen
Introduction
Make any piece of code testable! Easily mock any dependencies in your code during testing
- doesn't matter what paradigm you are using - no rearchitecture to ioc containers required
- doesn't matter the way you or your NPM dependencies import/export functions, classes, etc.
- doesn't matter if function, class, instance of class, variable, etc.
- guaranteed mocking or immediate failure - no implicit behavior
- requires minimal code changes
Just change your code from:
function getRandomFact() {
const res = await fetch('https://catfact.ninja/fact')
}
to this (wrapping dependency code in "dep"):
function getRandomFact() {
const res = await dep(fetch)('https://catfact.ninja/fact')
}
or using code injection (see below):
function getRandomFact() {
dep.injectable({ fetch })
const res = await fetch('https://catfact.ninja/fact')
}
During runtime, the code will behave exactly as before!
But in the tests, you can overwrite its behavior. For this, register a mock:
function fakeFetch(url) {
return {
async json() {
return { fact: 'hey'}
}
}
}
dep.register(fetch, fakeFetch)
If you did not register the mock, your test will fail, so there's no surprise about whether you correctly mocked something or not!
Get Started
Install:
npm install mockzen --save
In your test or global setup of your tests, turn on the requirement for mocks like this:
import { dep } from 'mockzen'
dep.enableTestEnv()
Alternatively, you can set the environment variable MOCKZEN_TEST_ENV
to true
or 1
for test runners like jest, which lack a global setup function that runs in the same process.
If you want to verify that dep is indeed looking up dependencies, you can do so like this in your tests:
expect(dep.testEnvEnabled).toBe(true)
See below for setting up code injection.
Naming dependencies
There is no need to name dependencies that are functions or classes. For example:
dep(SomeService)
dep(someFunction)
But you need to name dependencies that can't be looked up using shallow comparison:
This won't work as expected:
// code
dep(new Api()).doSomething()
// test
dep.register(new API(), /* */)
But no worries, it still won't affect your runtime code, and your test will still fail to inform you that there was a missing mock.
Give it a name to allow mocking it:
// code
const api = new Api()
dep('Api', api).doSomething()
// test
dep.register('Api', /**/)
Things you can mock
Absolutely anything! While it's recommended to only mock what is absolutely necessary, the library doesn't hinder you in any way.
// in code
const retryDelay = dep('retry delay', 10_000)
// in test
dep.register('retry delay', 1)
// all of this works too:
dep(Api) // to inline it: new (dep(API))()
dep('api', new Api)
dep('download', new Api().download)
dep('checks', [0, 2, 4, 8])
Skip mocking
Mocks are required by default. If you have tests that need something mocked only sometimes, you can disable the mocking requirement in a test like this:
it('...', async () => {
dep.allow('api')
dep.allow(fetch)
// can now execute code without providing mock for api and fetch
await doSomething()
})
Code Injection (experimental)
The current approach has a few downsides such as:
- having to wrap code with dep() can become cumbersome, and syntax isn't clean with things like classes
- you have to wrap the same object each time you interact with it
But we can make dependencies auto-injectable to go from:
function getRandomFact() {
const cachedFact = dep(redis).get('cats:fact') // 👈 dep here
if (cachedFact) {
return cachedFact
}
const { fact } = await dep(fetch)('https://catfact.ninja/fact') // 👈 dep here
dep(redis).set('cats:fact', fact) // 👈 dep here
return fact
}
to this:
function getRandomFact() {
dep.injectable({redis, fetch}) // 👈 This is the only change you need to do
const cachedFact = redis.get('cats:fact')
if (cachedFact) {
return cachedFact
}
const { fact } = await fetch('https://catfact.ninja/fact')
redis.set('cats:fact', fact)
return fact
}
To make this work, add the transformer to your configuration file.
jest
Add the following to your package.json or the respective code to your jest config file:
{
"jest": {
"transform": {
"^.+\\.js$": "mockzen/transformers/jest"
}
}
}
You can also alias fields to register dependencies.
dep.injectable({ MyService })
const apiClient = MyService.createApiClient()
dep.injectable({ 'apiAlias': apiClient }) // 👈 see how you can call dep.injectable multiple times as well.
Then in your tests, you can register mocks like this:
dep.register(MyService, MyServiceMock)
dep.register('apiAlias', MyServiceMock)
While you can also register a mock for 'MyService' using a string, it's not recommended as it will be impacted by variable name changes then. With "apiAlias" this is not a problem because we gave it a dedicated name explicitly.
Testing Utilities
Generally, you can just have custom code to record when a function was called, how many times it was called, what arguments it used, etc. But we can simplify this using the fake API.
fake
You can create a fake function like this:
const fakeApi = dep.fake() // returns undefined when called
const fakeApi = dep.fake(() => true) // returns true when called
const fakeApi = dep.fake(async () => true) // returns a promised value when called
Next, register this fake function and use it in your assertions:
const fakeApi = dep.fake()
dep.register(callApi, fakeApi)
await doTheThing()
expect(fakeApi.called).toBe(true)
expect(fakeApi.callCount).toBe(1)
expect(fakeApi.firstCall.firstArg).toEqual('https://...')
You can access different calls through the following fields:
- calls: an array of all calls
- firstCall: holds details of the first call to the function
- secondCall: holds details of the second call to the function
- lastCall: holds details of the last call to the function
Each call has the following properties:
- args: an array of arguments used to call the function
- firstArg: the first argument
- secondArg: the second argument
- lastArg: the last argument
Emptying the registry
dep.reset()
Writing library code
If you are writing a library that will be integrated into other applications, create your own registry to not interfere with the application code:
// dep.js
import { createRegistry } from 'mockzen'
export const dep = createRegistry()
// now import and use this version of "dep" where ever you need it!
Note that the environment variable MOCKZEN_TEST_ENV
does not affect custom registries. This is again so they don't interfere with application code. Please use the explicit dep.enableTestEnv()
!
Use Cases
Assert function was called
const { dep } = require('mockzen')
const { callApi } = require('services/api')
it('will ...', async () => {
const fakeApi = dep.fake()
dep.register(callApi, fakeApi)
await doTheThing()
expect(fakeApi.called).toBe(true)
})
Mock a (static) class method
const fakeApi = dep.fake()
class FakeClass {
callApi = fakeApi
}
dep.register(RealClass, FakeClass)
Return different mocks depending on the amount of times called
You also have the meta information available inside the callback for such scenarios!
const fakeFetch = dep.fake(() => {
if (fakeFetch.callCount === 1) {
// return for first function call
}
// return for subsequent function calls
})
dep.register(fetch, fakeFetch)
Return different mocks depending on the input arguments:
There is no special function for this, but it's straight forward to write your own:
async function fakeFetch(url) {
if (url.endsWith('/user')) {
// return ...
}
// return ...
}
dep.register(fetch, fakeFetch)
Validate the input arguments
it('will get a random fact', () => {
const fakeFetch = dep.fake()
dep.register(fetch, fakeFetch)
getVideo()
expect(fakeFetch.firstCall.firstArg).toEqual('http://...')
})