vscode-test: unable to import vscode in test using jest

Hi, I’m trying to run some integration tests for my vscode extension using jest. Unit tests work fine since I’m able to mock vscode however, when I’m trying to run an integration test I’m getting Cannot find module 'vscode' in the files when i’m using vscode api (import * as vscode from 'vscode')

jest config looks like this:

module.exports = {
  moduleFileExtensions: ['js'],
  testMatch: ['<rootDir>/out/test/**/*.int.test.js'],
  verbose: true,
};

runTest.ts

import * as path from 'path';
import { runTests } from 'vscode-test';

async function main() {
	try {
		// The folder containing the Extension Manifest package.json
		// Passed to `--extensionDevelopmentPath`
		const extensionDevelopmentPath = path.resolve(__dirname, '../../');

		// The path to the extension test script
		// Passed to --extensionTestsPath
		const extensionTestsPath = path.resolve(__dirname, './suite/index');

		// Download VS Code, unzip it and run the integration test
		console.log(`running tests... ext path: ${extensionTestsPath}`);
		await runTests({
			extensionDevelopmentPath,
			extensionTestsPath,
			launchArgs: ['--disable-extensions']
		});
	} catch (err) {
		console.error('Failed to run tests', err);
		process.exit(1);
	}
}

main();

index.ts

import * as path from 'path';
import { runCLI } from 'jest-cli';
import * as vscode from 'vscode';

export function run(): Promise<void> {

  return new Promise((resolve, reject) => {
    const projectRootPath = path.join(__dirname, '../../../');
    const config = path.join(projectRootPath, 'jest.e2e.config.js');

    vscode.window.showInformationMessage('Run suite');

    runCLI({ config } as any, [projectRootPath])
      .then(jestCliCallResult => {
        console.log(`complete: ${JSON.stringify(jestCliCallResult)}`);
        console.log('print results');
        jestCliCallResult.results.testResults
          .forEach(testResult => {
            testResult.testResults
              .filter(assertionResult => assertionResult.status === 'passed')
              .forEach(({ ancestorTitles, title, status }) => {
                console.info(`  ● ${ancestorTitles} › ${title} (${status})`);
              });
          });

        console.log('check results');
        jestCliCallResult.results.testResults
          .forEach(testResult => {
            if (testResult.failureMessage) {
              console.error(testResult.failureMessage);
            }
          });
        resolve();
      })
      .catch(errorCaughtByJestRunner => {
        console.error('error in test runner', errorCaughtByJestRunner);
        reject(errorCaughtByJestRunner);
      });
  });
}

I installed both vscode-test and @types/vscode as dev dependencies. I’m not sure why my tests are unable to find the ‘vscode’ dependency. Vscode is the only dependency that has this issue, other modules work fine. Can anyone help?

Thanks!

About this issue

  • Original URL
  • State: open
  • Created 5 years ago
  • Reactions: 8
  • Comments: 27 (1 by maintainers)

Commits related to this issue

Most upvoted comments

I found a very slim workaround!

I was also struggling like hell with that. I found that article which got me on the right track: https://www.richardkotze.com/coding/unit-test-mock-vs-code-extension-api-jest

However to make it run in the default TypeScript extension workflow you need to put the __mocks__ folder (yes plural, not singular as stated in the article) under your rootDir as defined in the tsconfig.json. In the default extension boilerplate it’s ./src.

Within the __mocks__ folder you create a vscode.ts (not .js as described in the article) with the following content.

export const vscode = {
  // mock the vscode API which you use in your project. Jest will tell you which keys are missing.
};

In my case an empty vscode object was enough since I only use types and interfaces from the vscode namespace. I would recommend the same for all of you for the major parts of the extension logic. Interacting with the vscode API can be limited to the extension.ts which is not unit tested but covered by e.g. end-to-end-tests.

So I actually manage to inject vscode into jest, and it’s not that ugly. This works for me, hope it works for someone else too!

First of all, create your own vscode environment file

// vscode-environment.js
const NodeEnvironment = require('jest-environment-node');
const vscode = require('vscode');

class VsCodeEnvironment extends NodeEnvironment {
  async setup() {
    await super.setup();
    this.global.vscode = vscode;
  }

  async teardown() {
    this.global.vscode = {};
    await super.teardown();
  }

  runScript(script) {
    return super.runScript(script);
  }
}

module.exports = VsCodeEnvironment;

next, make jest use this environment, I did it by adding it into my jest config

// jest config

module.exports = {
  // more config
  testEnvironment: './vscode-environment.js',
  // more config
};

now we are just missing the tricky and most important part. Create a file, call it whatever you want, I called it vscode.js and copy this line

// vscode.js
module.exports = global.vscode;

Final step, go to your jest config, and define a module mapper like this

const path = require('path');

module.exports = {
  testEnvironment: './vscode-environment.js',
  modulePaths: ['<rootDir>'],
  moduleNameMapper: {
    vscode: path.join(__dirname, 'test-jest', 'vscode.js')  // <----- most important line
  },
};

my vscode file is in test-jest/vscode.js place yours wherever you need!

By doing this, any line that is importing vscode will really import it into jest, so you are able to run methods, mock them or do whatever you want because you have the real vscode object!

I hope this helps someone!!

[EDITED]

Forgot to mention you also need to define a custom Vscode jest runner if someone needs this too I’ll add it to this comment!

I was able to reference vscode instance in my tests only through process variable, maybe I was doing something wrong with the globals setup but vscode package couldn’t be required there as @octref proposed.

So to make it work with process variable in typescript you have to define your typing anywhere in your project as a separate file ex. process.d.ts Preview:

declare namespace NodeJS {
	export interface Process {
		vscode?: any
	}
}

Then in suite/index.ts import vscode and set process.vscode, before run function definition Preview:

import * as path from 'path';
import { runCLI } from 'jest-cli';
import * as vscode from 'vscode';

process.vscode = vscode;

export function run(): Promise<void> {
  return new Promise((resolve, reject) => {
       ...

And finally in your test file you can reference process.vscode Preview:

describe('Extension Test Suite', () => {
	beforeEach(() => {
		process.vscode.window.showInformationMessage('Start all tests.');
	});

	it('Sample test', () => {
		process.vscode.commands.executeCommand('extension.helloWorld');
		expect(1).toBe(1);
	});
});

It is very hacky way of accessing vscode instance, so try to find a better solution. I just post my findings FYI.

To address this issue, I created jest-mock-vscode. It can help with testing logic using unit tests. It is not designed for E2E/UI testing. The vscode-test runner is best for that.

Adding a mocks folder for vscode per blog article here is a clean/efficient solution: https://www.richardkotze.com/coding/unit-test-mock-vs-code-extension-api-jest

https://github.com/rkotze/git-mob-vs-code/tree/baf86ae15bf359bf409a6a3bdc7ac74850640433/__mocks__

The problem here is you are using stubs/mocks, so you can’t do actual integration/E2E/UI tests and verify things work in vscode instance itself, or verify the API hasn’t changed. That’s the real benefit of using vscode-test here vs. just mocking/stubbing out all the external calls.

@vinnichase You have a flaw there. The way vscode extensions are meant to be tested is IN vscode precisely so that you test vscode fully integrated. This is also the way microsoft introduces you to extension testing… Otherwise any vscode update may blow up your extension because you forgot to update your mocks…

You also won’t be able to properly test by passing via the process module, because jest runs everything via the node “vm” module ( https://nodejs.org/api/vm.html ) which prevents you from selectively modifying the vscode object if the vscode object is not part of the vms global object (the vm context). That is you will be able to modify it IN the test but your modifications to the vscode module will NOT propagate outwards to your extension code, because of the vm. It does not matter that the process PID is the same. The key point is the “vm” module that jest uses.

The correct way to use jest inside a vscode host is you do


class VsCodeEnvironment extends NodeEnvironment {
  async setup() {
      
    await super.setup();
     this.global.vscode = vscode; // puts vscode into global scope of the vm, but more importantly makes vscode object native to the vm, (allowing you to mock parts of it)
  }

  async teardown() {
      
       delete this.global.vscode;
       await super.teardown();
  }


}

module.exports = VsCodeEnvironment;

and then


await runCLI({

                        testEnvironment:path.join(__dirname,'testenv.js'),
			
		},[jestProject]);

You still wont be able to IMPORT vscode like normal in your tests, which is annoying with those import quickfixes, but vscode is available as a global now and now you can mock those pesky extensions error messages etc. and changes will propagate outwards

Thanks @MarioCadenas, but just a few hours after posting that I started to consider other options (e.g. just using Mocha instead). I have little experience in this area, so I think I’ll do some reading of my own before coming back with more specific questions. If I never reply, it’s because I went with an alternative 😃 Thanks anyway!

Thanks for the hint! However this breaks my understanding of unit tests which I was doing in my case. Your approach would apply better in an integration test.

I didn’t even mock vscode functionality but only imported a class and used it as an interface and an enum.

There used to be a special vscode typings package on npm for that purpose but maintenance stopped and it deviated from the ones exposed in the vscode extension context. So thats ridiculous that you can not use the interace description (which is by definition the abstraction of the running code) separately from the runtime context.