npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

mockzen

v0.1.7

Published

Make any piece of code testable! Easily mock any dependencies in your code during testing

Downloads

23

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