Skip to content

Unit Test Structure

Billie Simmons edited this page Apr 21, 2023 · 2 revisions

In this article we will try to explain the expected way to write Unit Tests for the project. Generally the entire work starts from the test file, so we will start from there and go to closer scale. So once you create (be sure that you keep the original file position, but inside unit folder, and use [filename].unit.test.ts name pattern) or find the existing test file which you would like to extend.

File Structure

So you have the target file, but what to do next? Logically the test file has 3 levels, let's take a look to the chart: Test File Structure

So far you're expected to go level by level implementing the test.

Level #1

Here we expect you to do 3 things mainly:

  • jest.mock nodejs fs module if it's used inside of the tested functionality;
  • Define createGlobalMocks() function (this one includes overrides for definitions of functions/methods in global packages of the app);
  • Define describe() blocks (simple, right? but there're still some rules);

createGlobalMocks() is a function which has only one purpose - give you the centralised point to mock global dependencies. If you want to change the actual implementation of some function to jest.fn(), it's the right place to do it. There are two ways of doing it, though.

First, with transit object to access your mocks in the actual test:

function createGlobalMocks() {
    const newVariables = {
        showInfoMessage: jest.fn()
    };

    Object.defineProperty(vscode.window, "showInformationMessage", { value: newVariables.showInfoMessage });

    return newVariables;
}

So you use this function in the actual it block and the returned object now is a source of all global definitions which you want to interact with.

Second, without transit object:

function createGlobalMocks() {
   Object.defineProperty(vscode.window, "showInformationMessage", { value: jest.fn() });
}

It is a shorter definition, but it contains one problem: the TypeScript compiler needs to be told about the mocked function, so after the definition of the function you need to define another one:

const mocked = <T extends (...args: any[]) => any>(fn: T): jest.Mock<ReturnType<T>> => fn as any;

So you can wrap vscode.window.showInformationMessage and use its mocked functionality.

Notice! Only one way is possible for the file, so if you extend existing tests, please, use the standard which is already implemented. If you add new tests you're free to choose yourself, but still keep in mind: one file - one standard.

For describe() blocks we have only one requirement - naming standard. It's fairly simple: <module name> Unit Tests - Function <function name>. Normally module name is related to the file where you do work.

Level #2

In this section the whole work is going to happen inside the describe block. Here we do two things:

  • Define createBlockMocks;
  • (Optionally) restore function spies using jest.restoreAllMocks();
  • Define it() blocks;

createBlockMocks() is a sibling of createGlobalMocks() but it always returns an object with mocks and you generate things which are related to the scope of the current test only. Let's say you need the Imperative Profile mock and Dataset Tree to be used as dependencies.

function createBlockMocks() {
   const imperativeProfile = createIProfile();
   const treeView = createTreeView();

   return {
      treeView,
      imperativeProfile,
      testDatasetTree: createDatasetTree(datasetSessionNode, treeView)
   };
}

After you can just use your function inside of it() block and utilise those mocks. Also you can see that we use so called creators; they are meant to be used inside of the above function, so if you need some mock check in creators folder first.

Also if you used the jest.spyOn() method inside of your test it would good idea to add:

afterAll(() => jest.restoreAllMocks());

So your other tests won't be affected after the current describe() block is finished.

Level #3

So now you have all of your mock functions defined for both the global and block scopes. The only thing which is left is the actual test in it. To get guidance about the actual test, you can read Best Practices: Unit Test guideline. The only thing which I would like you to pay attention is the structure:

it('Some test', async () => {
  const globalMocks = createGlobalMocks();
  const blockMocks = createBlockMocks();
  // Next you can define your mocks reliable only for this test, like test nodes, spies and etc.
  
  someFunctionWhichYouTest();

  globalMocks.showInfoMessage.toBeCalled();
});

Notice that we used transit object for global mocks here. In the case of usage without this object, the test will look like the following:

it('Some test', async () => {
  createGlobalMocks();
  const blockMocks = createBlockMocks();
  // Next you can define your mocks reliable only for this test, like test nodes, spies and etc.
  
  someFunctionWhichYouTest();

  mocked(vscode.window.showInformationMessage).toBeCalled();
});

If you are not sure about something, feel free to contact the community and ask about your tests!