Your code isnt untestable, your testing tools are too rigid.
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 DI and 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, NPM library, 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 variable 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!
Install:
npm install mockzen
In your test or global setup of your tests, turn on the requirement for mocks like this:
import { dep } from 'mockzen'
dep.enableTestEnv()
Alternatively, 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 mockzen is indeed looking up dependencies, add this assertion to your tests:
expect(dep.testEnvEnabled).toBe(true)
See below for setting up code injection.
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:
For example, this won't work because the variable "api" is not equal to "testApi":
// code
const api = new Api()
dep(api).doSomething()
// test
const testApi = new Api()
dep.register(testApi, /* */)
But your runtime code will still work just fine, and your test will still throw an error to inform you that there was a missing mock.
In such cases, give the dependency a custom name:
// code
const api = new Api()
dep('Api', api).doSomething()
// test
const testApi = new Api()
dep.register('Api', testApi)
Absolutely anything! While it's recommended to only mock what is 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])
Mocks are required by default. If you have tests that need something mocked only sometimes, 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()
})
You need to apply dep() each time you interact with the dependency. To reduce the amount of wraps needed, apply "dep" at a lower level.
For example, instead of:
function handler1() {
dep(factService).call()
}
function handler2() {
dep(factService).call()
}
apply it in the FactService:
class FactService {
call() {
dep(fetch)('https://....')
}
}
Alternatively, 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 experimental feature work, add the transformer to your configuration file.
Add the following to your package.json or the respective code to your jest config file:
{
"jest": {
"transform": {
"^.+\\.js$": "mockzen/transformers/jest"
}
}
}
Aliasing fields is also possible here:
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, register mocks like this:
dep.register(MyService, MyServiceMock)
dep.register('apiAlias', MyServiceMock)
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.
let apiCalled = false
async function fakeCallApi() {
apiCalled = true
return true
}
dep.register(callApi, fakeCallApi)
await someCode()
expect(apiCalled).toBe(true)
But we can simplify this using the fake API:
const fakeCallApi = dep.fake(async () => true) // returns the promised value when called
dep.register(callApi, fakeCallApi)
await someCode()
expect(fakeCallApi.called).toBe(true)
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
dep.reset()
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()
!
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)
})
const fakeApi = dep.fake()
class FakeClass {
callApi = fakeApi
}
dep.register(RealClass, FakeClass)
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)
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)
it('will get a random fact', () => {
const fakeFetch = dep.fake()
dep.register(fetch, fakeFetch)
getVideo()
expect(fakeFetch.firstCall.firstArg).toEqual('http://...')
})