There is an issue with unit testing VS Code extensions. The vscode dependency - which is needed to utilise the editor’s features - will error when running unit tests. Essentially it is a third party dependency which is out of your control, so the best thing to do is to mock the API. I will be using Jest and will explain how to use its mocking features to handle the VS Code dependency.

snowy mountain landscape Photo by Zach Lezniewicz on Unsplash

If you are just getting started with building your first VS Code extension, the docs have a simple step-by-step guide to quickly get you coding.

Once you have set up your project, you will notice it has the normal package.json. VS Code extensions will read from the package.json as a config to manage UI elements, so there will be some new properties which you will probably use when developing your extension. Since it’s a standard package.json file you can install dependencies as usual. To set up Jest, install as a dev dependency and update the npm test script to run Jest.

npm i -D jest
{
  "scripts": {
    "test": "jest"
  }
}

Mock VS Code node module

Jest provides a few options for mocking, but because we want to mock the whole of the vscode node module the easiest option is to create a __mocks__ folder on the same level as the node_modules folder (typically the root folder) and add a file with the same name as the module to be mocked (vscode.js).

You won’t need to import the module into your test file, the mock is automatically applied. Jest calls this manual mocks.

The great thing about this approach is it keeps your test files clean from all the mock set-up code, making it easy to reason about. A minor downside is new contributors to your codebase will need to be made aware of the __mocks__ folder, because there no explicit connection to indicate that the VS Code module is mocked.

Below is the mock of the VS Code dependency. It’s not the entire API so you should adjust it to your needs.

// vscode.js

const languages = {
  createDiagnosticCollection: jest.fn()
};

const StatusBarAlignment = {};

const window = {
  createStatusBarItem: jest.fn(() => ({
    show: jest.fn()
  })),
  showErrorMessage: jest.fn(),
  showWarningMessage: jest.fn(),
  createTextEditorDecorationType: jest.fn()
};

const workspace = {
  getConfiguration: jest.fn(),
  workspaceFolders: [],
  onDidSaveTextDocument: jest.fn()
};

const OverviewRulerLane = {
  Left: null
};

const Uri = {
  file: f => f,
  parse: jest.fn()
};
const Range = jest.fn();
const Diagnostic = jest.fn();
const DiagnosticSeverity = { Error: 0, Warning: 1, Information: 2, Hint: 3 };

const debug = {
  onDidTerminateDebugSession: jest.fn(),
  startDebugging: jest.fn()
};

const commands = {
  executeCommand: jest.fn()
};

const vscode = {
  languages,
  StatusBarAlignment,
  window,
  workspace,
  OverviewRulerLane,
  Uri,
  Range,
  Diagnostic,
  DiagnosticSeverity,
  debug,
  commands
};

module.exports = vscode;

Examples of using this VS Code mocked module

I’m going to take code examples from one of my open-source projects, Git Mob for VS Code in which I’ve used this approach.

Below is an example of adjusting the editor’s status bar depending on whether the prepare-commit-msg Git hook is being used. Here you can see I don’t need to import vscode module into my test file to mock it.

// git-mob-hook-status.spec.js

const { hasPrepareCommitMsgTemplate } = require("../prepare-commit-msg-file");
const { gitMobHookStatus } = require("./git-mob-hook-status");

jest.mock("./../prepare-commit-msg-file");

describe("Hook or template status", function() {
  let mockContext;
  beforeAll(function() {
    mockContext = {
      subscriptions: []
    };
  });

  afterEach(function() {
    hasPrepareCommitMsgTemplate.mockReset();
  });

  it("using git template for co-authors", () => {
    hasPrepareCommitMsgTemplate.mockReturnValue(false);
    const statusBar = gitMobHookStatus({ context: mockContext })();
    expect(statusBar).toEqual(
      expect.objectContaining({
        text: "$(file-code) Git Mob",
        tooltip: "Using .gitmessage template"
      })
    );
  });

  it("using git prepare commit msg for co-authors", () => {
    hasPrepareCommitMsgTemplate.mockReturnValue(true);
    const statusBar = gitMobHookStatus({ context: mockContext })();
    expect(statusBar).toEqual(
      expect.objectContaining({
        text: "$(zap) Git Mob",
        tooltip: "Using prepare-commit-msg hook"
      })
    );
  });
});
// git-mob-hook-status.js
const vscode = require("vscode");
const { hasPrepareCommitMsgTemplate } = require("../prepare-commit-msg-file");

function gitMobHookStatus({ context }) {
  const myStatusBarItem = vscode.window.createStatusBarItem(
    vscode.StatusBarAlignment.Left,
    10
  );
  context.subscriptions.push(myStatusBarItem);
  return function() {
    myStatusBarItem.text = "$(file-code) Git Mob";
    myStatusBarItem.tooltip = "Using .gitmessage template";
    if (hasPrepareCommitMsgTemplate()) {
      myStatusBarItem.text = "$(zap) Git Mob";
      myStatusBarItem.tooltip = "Using prepare-commit-msg hook";
    }
    myStatusBarItem.show();
    return myStatusBarItem;
  };
}

exports.gitMobHookStatus = gitMobHookStatus;

You can view the source code here:

Can I check if a vscode method is called?

You can import the vscode mock. Below we want to check that the onDidSaveTextDocument event is subscribed to when a user makes changes to the co-author file.

const vscode = require("../__mocks__/vscode");

// ...
test("Reload co-author list when git-coauthors file saved", () => {
  reloadOnSave(coAuthorProviderStub);
  expect(vscode.workspace.onDidSaveTextDocument).toHaveBeenCalledWith(
    expect.any(Function)
  );
  // ...
});
// ...

As you can see it is the standard Jest mock API, meaning you are not limited in any way with the manual mock approach, for example, you can also modify the implementation using mockImplementation.

See the full source file here for further examples:

One of the key benefits of writing unit tests is the quick feedback, and if you like following the TDD approach this will be helpful for you to build your extension confidently.

Comments below or share on Twitter.