0.1.7 • Published 5 months ago

mockzen v0.1.7

Weekly downloads
-
License
MIT
Repository
github
Last release
5 months ago

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://...')
})
0.1.7

5 months ago

0.1.6

6 months ago

0.1.5

6 months ago

0.1.4

6 months ago

0.1.3

6 months ago

0.1.2

6 months ago

0.1.0

6 months ago

0.0.6

7 months ago

0.0.5

7 months ago

0.0.4

7 months ago

0.0.3

7 months ago

0.0.2

7 months ago

0.0.1

7 months ago