🎪 The best way to run and write tests for your VSCode extension
Key Features:
- run tests using Jest
- built-in API making tests simpler to write and read
- zero JS configuration
1 - Install the following packages:
npm install jest jest-environment-vscode-extension @types/jest @types/jest-environment-vscode-extension --save-dev
2 - On .vscode/tasks.json
, add the following within the tasks
array:
{
"label": "create-test-workspace-folder",
"type": "shell",
"command": "mkdir",
"args": ["-p", "test-workspace"],
"presentation": {
"reveal": "silent",
"revealProblems": "onProblem"
}
},
{
"label": "remove-test-workspace-folder",
"type": "shell",
"command": "rm",
"args": ["-rf", "test-workspace"],
"presentation": {
"reveal": "silent",
"revealProblems": "onProblem"
}
},
{
"label": "insert-monkey-patch-allow-mocks",
"type": "shell",
"command": "node ./node_modules/.bin/insert-monkey-patch-allow-mocks ${workspaceFolder}",
"presentation": {
"reveal": "silent",
"revealProblems": "onProblem"
},
},
{
"label": "drop-monkey-patch-allow-mocks",
"type": "shell",
"command": "node ./node_modules/.bin/drop-monkey-patch-allow-mocks ${workspaceFolder}",
"presentation": {
"reveal": "silent",
"revealProblems": "onProblem"
}
},
{
"label": "pre-run-tests",
"dependsOrder": "sequence",
"dependsOn": [
"remove-test-workspace-folder",
"create-test-workspace-folder",
"build",
"insert-monkey-patch-allow-mocks"
],
"presentation": {
"reveal": "silent",
"revealProblems": "onProblem"
}
},
{
"label": "post-run-tests",
"dependsOn": [
"remove-test-workspace-folder",
"drop-monkey-patch-allow-mocks"
],
"presentation": {
"reveal": "silent",
"revealProblems": "onProblem"
}
}
3 - On .vscode/launch.json
, add the following within the configurations
array:
{
"name": "Test Extension - No Workspace",
"preLaunchTask": "pre-run-tests",
"postDebugTask": "post-run-tests",
"type": "extensionHost",
"request": "launch",
"runtimeExecutable": "${execPath}",
"args": [
"/no-workspace",
"--disable-extensions",
"--extensionDevelopmentPath=${workspaceFolder}",
"--extensionTestsPath=${workspaceFolder}/node_modules/.bin/vscode-tests-runner"
],
"env": {
"VSCODE_TESTS_PATH": "${workspaceFolder}/out/tests/no-workspace/"
},
"outFiles": ["${workspaceFolder}/out/tests/**/*.js"]
},
{
"name": "Test Extension - With Workspace",
"preLaunchTask": "pre-run-tests",
"postDebugTask": "post-run-tests",
"type": "extensionHost",
"request": "launch",
"runtimeExecutable": "${execPath}",
"args": [
"${workspaceFolder}/test-workspace",
"--disable-extensions",
"--extensionDevelopmentPath=${workspaceFolder}",
"--extensionTestsPath=${workspaceFolder}/node_modules/.bin/vscode-tests-runner"
],
"env": {
"VSCODE_TESTS_PATH": "${workspaceFolder}/out/tests/with-workspace/"
},
"outFiles": ["${workspaceFolder}/out/tests/**/*.js"]
}
4 - Now, write your tests that depend on a workspace within tests/with-workspace
. And if it doesn't need it, you can write them within tests/no-workspace
.
Setup finished! 🎉
Now you can run the tests using VSCode:
Running by VSCode is great for development since it's quick and can use breakpoints. But we need to do one more step to can run on CI.
1 - On package.json
, add the following within the scripts
object:
"tests:ci:no-workspace": "vscode-electron-starter no-workspace insiders out/tests/no-workspace",
"tests:ci:with-workspace": "vscode-electron-starter with-workspace insiders out/tests/with-workspace"
The penultimate parameter is the VSCode version being used. You can use stable
, insiders
, or a version number (e.g., 1.32.0
). The last parameter is the path of the test.
2 - Now you can call these scripts on CI. Following, a script to run on GitHub actions:
on:
push:
branches:
jobs:
test:
name: Test
strategy:
matrix:
os: [ubuntu-latest]
runs-on: ${{ matrix.os }}
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Setup Node.js
uses: actions/setup-node@v1
with:
node-version: 16
- name: Install dependencies
run: npm install
- name: Build
run: npm run build
- name: Run test - No workspace
uses: GabrielBB/xvfb-action@v1.0
with:
run: npm run tests:ci:no-workspace
- name: Run test - With workspace
uses: GabrielBB/xvfb-action@v1.0
with:
run: npm run tests:ci:with-workspace
It's almost the same idea as writing any other test using Jest, but we have a powerful API focused on VSCode.
Let's do a walkthrough writing a simple test. We want to test if the "go to the definition" works well at the second x
:
const x = 42
console.log(x)
1 - Firstly, our test doesn't depend on a workspace. Then we'll write it at tests/no-workspace/definitions.test.ts
. Usually, a test depends on a workspace if it interacts with other files on the same workspace.
2 - Let's write the test itself:
// get some things from the global variable `vscode`
const { Position, Range } = vscode
describe('#Definition', () => {
it('on message interpolation', () => {
// create a new file
return using({
files: {
'index.js': dedent(`
const x = 42
console.log(x)
`),
}},
async (mapFileToDoc) => {
// on the file `index.js`, take the definitions at 1:12 (the `x` within the `console.log`)
const definitions = await take.definitions(mapFileToDoc['index.js'], new Position(1, 12))
// assert that it's as the expected
expect(definitions).toHaveLength(1)
expect(definitions[0]).toMatchObject({
originSelectionRange: new Range(new Position(1, 12), new Position(1, 13)),
targetRange: new Range(new Position(0, 12), new Position(0, 0)),
targetSelectionRange: new Range(new Position(0, 6), new Position(0, 7)),
})
})
})
})
On the above test, we used some variables injected by jest-environment-vscode-extension
: vscode
, using
, dedent
, and take
. Think of them as the Jest's describe
or it
, but focused on helping you while working with VSCode.
Let's talk about them!
Our most useful function.
It creates the files and, optionally, can mock VSCode's functions. It receives a callback and, when it's finished, clear the files and mocks.
You can create as many files as needed, and their TextDocument
is sent to the callback:
using(
{
files: {
'index.js': '"example";',
'foo.js': '1;',
'bar.js': '2;',
},
},
async (mapFileToDoc) => {
mapFileToDoc['index.js'] // TextDocument
mapFileToDoc['foo.js'] // TextDocument
mapFileToDoc['bar.js'] // TextDocument
}
)
There are some VSCode features in which we can't manipulate, such as the window.showQuickPick
. But no worries! We can easily mock it:
using(
{
files: {
'index.js': '"example";',
},
mocks: {
'window.showQuickPick': async () => 'My Option',
},
},
async (mapFileToDoc) => {
}
)
Now, if the extension calls window.showQuickPick
it'll return Promise<'My Option'>
.
But there is a rule to use mocks: You should ensure that the extension is initialized. For example, let's say that your extension is initialized only when there is a .ml
file in the workspace:
"activationEvents": [
"workspaceContains:**/*.ml"
]
So you should run the tests using workspace and create at least one .ml
file:
using(
{
files: {
'main.ml': 'let hello () = print_endline "hey there"',
},
mocks: {
'window.showQuickPick': async () => 'My Option',
},
},
async (mapFileToDoc) => {
}
)
Function to remove indentation. Helpful with using
.
It's the same vscode
used by the extension itself. So you can use it to manipulate the VSCode.
For example, if you want to open and show a document, you should do:
const { workspace, window } = vscode
const doc = await workspace.openTextDocument(mapFileToDoc['index.js'])
await window.showTextDocument(doc)
It doesn't export the types. If you want them, you should do:
import type { Position } from 'vscode'
const printPosition = (position: Position) => {
console.log({
line: position.line,
character: position.character,
})
}
It exposes many helper functions to take values from the VSCode. Just use TypeScript's intellisense to explore what it has.
It exposes a helper function to wait for something.
For example, if your extension takes time to initialize, it can be useful:
const waitForDocumentSymbols = async (doc, position) => {
return await waitFor(async () => {
const hovers = await take.hovers(doc, position)
expect(hovers).toHaveLength(1)
return hovers
})
}
describe("#Document Symbol", () => {
it("includes function declaration", () => {
return using(
{
files: {
'main.ml': 'let hello () = print_endline "hey there"',
},
},
async (mapFileToDoc) => {
const symbols = await waitForDocumentSymbols(mapFileToDoc['main.ml'])
expect(symbols[0]).toMatchObject({
name: 'hello',
detail: 'unit -> unit',
})
}
)
})
})
vscode-fluent, extension for Fluent, the correct-by-design l10n programming language |
Add your project here |