Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Mocking the Backend with node & Codi 🐶 #1884

Open
wants to merge 37 commits into
base: minor
Choose a base branch
from

Conversation

RobAndrewHurst
Copy link
Contributor

@RobAndrewHurst RobAndrewHurst commented Feb 8, 2025

Mocking with node & Codi 🐶

Note

Codi has implemented mock modules/functions that are only available in the nodeJS environment.

Module mocking is a strange and weird concept to get your head around. It is essentially is a way to replace modules or functions with stubs to simulate or 'mock' external entities.

What a mocking module will do is replace the reference of a module in memory with a 'mocked' version of it. This mocked version can then be called from non-test code and receive an output. You can then reset that version of the mocked module to the original export with a reset/restore function.

Codi can mock:

  • functions
  • modules
  • http requests

function (fn) mocking

Just like any function, mocking a function needs to have context. The context can be the scope of a test, imported module or global.

To mock a function you can call the codi.mock.fn() function.
This creates a mock function which you can interface with.

const random = codi.mock.fn((max) => return Math.floor(Math.random() * max););

codi.it({ name: 'random', parentId: 'foo' }, () => {
  codi.assertTrue(random(1) === 0, 'We expect the number to be 0');
  codi.assertTrue(random(3) <= 2), 'We expect the number to less than or equal to 2';
});

You can also mock functions/methods with the codi.mock.method() function that will take an object as a reference and implement a mock function to that objects method.

Important

typically in tests written this methodology isn't used and favoured for the codi.mock.mockImplementation()/mockImplementationOnce() function which can mock a function given to a mocked module. An example of this will be provided in the mock module section.

import fs from "node:fs";

// Mocking fs.readFile() method
codi.mock.method(fs.promises, "readFile", async () => "Hello World");

codi.describe({ name: "Mocking fs.readFile in Node.js", id: "mock" }, () => {
  codi.it(
    {
      name: "should successfully read the content of a text file",
      parentId: "mock",
    },
    async () => {
      codi.assertEqual(fs.promises.readFile.mock.calls.length, 0);
      codi.assertEqual(
        await fs.promises.readFile("text-content.txt"),
        "Hello World",
      );
      codi.assertEqual(fs.promises.readFile.mock.calls.length, 1);

      // Reset the globally tracked mocks.
      mock.reset();
    },
  );
});

module mocking

You can mock modules with the codi.mock.module() function which takes a path and options to mock a module.
The typical practice is that you provide a mocked function that you can implement mocks ontop of making the mocked module more reusable.

Caution

the codi.mock.module() function is still in early development as it comes from the node:test runner, which is still in further development

options you can provide mocked module:

  • cache: If false, each call to require() or import() generates a new mock module. Default: false.
  • defaultExport: An optional value used as the mocked module's default export. If this value is not provided, ESM mocks do not include a default export.
  • namedExports: An optional object whose keys and values are used to create the named exports of the mock module.

Bellow is an example of a mocked module referencing a mocked function

Important

Ensure that your module mocks are top level, as to import the module before the dynamic import to the module we are testing.

const aclFn = codi.mock.fn();
const mockedacl = codi.mock.module('../../acl.js', {
  cache: false,
  defaultExport: aclFn
  namedExports:{
    acl: aclFn
  }
});

codi.describe({name: 'mocked module', id: 'mocked_module'}, () => {
  codi.it({'We should be able to mock a module', parentId: 'mocked_module'}, async () => {

    aclFn.mock.mockImplementation(() => {
      const user = {
        email: '[email protected]',
        admin: true
      };
      return user
    })

    const { default: login } = await import('../../../mod/user/login.js');

    const result = await loing();

    //{ email: 'robert.hurst@geolytix.co.uk', admin: true}
    console.log(result);
  });
});

module & function restore/reset

if you want to return the functionality of a mocked function/module you will want to call the restore function on a mocked module.

Important

You will want to call these restores on mocked modules at the end of a test so that other tests can also mock the same module. If you don't an error will be returned.

const aclFn = codi.mock.fn();
const mockedacl = codi.mock.module('../../acl.js', {
  cache: false,
  defaultExport: aclFn
  namedExports:{
    acl: aclFn
  }
});

codi.describe({name: 'mocked module', id: 'mocked_module'}, () => {
  codi.it({'We should be able to mock a module', parentId: 'mocked_module'}, async () => {
  //...test
  });
});

//Call to the mocked module to restore to original state.
mockedacl.restore();

http mocks

codi has exported functions to help aid in mocking http requests.

codi.mockHttp helps create req & res objects that can be passed to functions in order to simulate a call to the function via an api. You can call the createRequest & createResponse functions respectively. You can also call the createMocks function and perform a destructured assignment on the req & res.

await codi.describe({ name: "Sign: ", id: "sign" }, async () => {
  await codi.it({ name: "Invalid signer", parentId: "sign" }, async () => {
    const { default: signer } = await import("../../../mod/sign/_sign.js");

    const req = codi.mockHttp.createRequest({
      params: {
        signer: "foo",
      },
    });

    const res = codi.mockHttp.createResponse();

    //OR

    const { req, res } = codi.mockHttp.createMocks({
      params: {
        signer: "foo",
      },
    });

    await signer(req, res);

    codi.assertEqual(res.statusCode, 404);
    codi.assertEqual(res._getData(), "Failed to validate 'signer=foo' param.");
  });
});

You can also mock the response from the global fetch function by making use of the MockAgent & setGlobalDispatcher interfaces.

The MockAgent class is used to create a mockpool which can intercept different paths to certain URLs. And based on these paths we can specify a return.

The setGlobalDispatcher will assign the Agent on a global scope so that calls to the fetch function in non-test modules will be intercepted.

Here is an example of this:

await codi.describe({ name: "HTTP Mock", id: "http_test_fun" }, async () => {
  await codi.it(
    { name: "We should get some doggies", parentId: "http_test_fun" },
    async () => {
      const mockAgent = new codi.mockHttp.MockAgent(); //<-- Mockagent we use to get a pool
      codi.mockHttp.setGlobalDispatcher(mockAgent); // <-- Assigning the agent on a global scope.

      const mockPool = mockAgent.get(new RegExp("http://localhost:3000")); //<-- Mock pool listening for the localhost url
      mockPool
        .intercept({ path: "/" })
        .reply(404, [
          "codi",
          "mieka",
          "luci",
        ]); /** <-- When we hit a specific path
                    we get a specified response */

      const response = await fetch("http://localhost:3000");

      codi.assertEqual(response.status, 404, "We expect to get a 404");
      codi.assertEqual(await response.json(), ["codi", "mieka", "luci"]);
    },
  );
});

Running the Tests

The codi test suit will iterate through the tests directory [ignoring the folders specified in codi.json] and log the results from each test suit.

node --run test

You can also call the tests in watch mode where changes to the xyz codebase will retrigger the tests on save.
Running the watched tests will only show the tests that fail.

node --run test-watch

Note

It's better to call the scripts in the package.json with the node --run command as it's faster than npm.
This is part of node v22+

@RobAndrewHurst RobAndrewHurst marked this pull request as draft February 8, 2025 21:24
@RobAndrewHurst RobAndrewHurst marked this pull request as ready for review February 11, 2025 14:25
@RobAndrewHurst RobAndrewHurst changed the title Backend tests Mocking the Backend with node & Codi 🐶 Feb 11, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant