{"id":15390,"date":"2024-04-12T00:00:00","date_gmt":"2024-04-12T07:00:00","guid":{"rendered":"https:\/\/devblogs.microsoft.com\/ise\/?p=15390"},"modified":"2024-11-03T23:45:32","modified_gmt":"2024-11-04T07:45:32","slug":"testing-vscode-extensions-with-typescript","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/ise\/testing-vscode-extensions-with-typescript\/","title":{"rendered":"Testing VSCode Extensions with TypeScript"},"content":{"rendered":"<h2>Introduction<\/h2>\n<p>Many of the popular Visual Studio (VS) Code extensions like <a href=\"https:\/\/github.com\/prettier\/prettier-vscode\/tree\/main\">prettier<\/a> or <a href=\"https:\/\/github.com\/microsoft\/vscode-docker\/tree\/main\">docker<\/a> lack any true unit tests, which can be run without the <code>vscode<\/code> dependency.\nIn fact, in taking a look at many other extensions and samples, nearly all are missing true unit tests.\nAccording to the <a href=\"https:\/\/code.visualstudio.com\/api\/working-with-extensions\/testing-extension\">VS Code Testing Extensions documentation<\/a>, the way VS Code recommends running and debugging tests for extensions is solely through integration tests that utilize a special test instance of VS Code, the Extension Development Host.\nExtensions can then use this instance of VS Code to write integration and some form of &#8220;unit&#8221; tests in this environment.\nHowever, true unit tests are required for an extension, which should be runnable without a VS Code instance, VS Code provides no resources or guidance in this case.\nThis poses a problem when it is necessary to create production quality code that adheres to strict engineering fundamentals, which require unit tests covering the majority of all components (&gt;90% if possible).\nThis blog will explore potential solutions to this issue and how teams can write sufficient unit tests for their VS Code extensions.<\/p>\n<h2>Potential Solutions<\/h2>\n<p>Under normal circumstances, relying on a dependency should certainly not be a problem for unit testing; the dependency can be mocked using a number of different frameworks.\nHowever, in this case, the <code>vscode<\/code> dependency does not exist as a normal dependency and is not listed in the <code>dependencies<\/code> member of the <code>package.json<\/code>, but rather the <a href=\"https:\/\/code.visualstudio.com\/api\/references\/extension-manifest\"><code>engines<\/code> field<\/a>.\nThus, if the <code>vscode<\/code> Application Programming Interface (API) is mocked as shown below using normal TypeScript Unit Tests with the <a href=\"https:\/\/jestjs.io\/\">Jest Framework<\/a>, a <code>Cannot find module 'vscode' from 'src\/tests\/test1.ts'<\/code> error will appear on the <code>import<\/code> statement:<\/p>\n<pre><code class=\"language-ts\">\/\/ src\/tests\/commands\/command1.test.ts\r\nimport * as vscode from 'vscode';\r\n...\r\n\/\/ mock a part of the vscode API used\r\nvscode.window.showInformationMessage = jest.fn()<\/code><\/pre>\n<p>To address the above issue of mocking the <code>vscode<\/code> API while it not being a normal node dependency, there are at least two viable solutions.<\/p>\n<h3>Option 1: Wrap the VS Code API<\/h3>\n<p>Option 1 is to wrap all methods of the <code>vscode<\/code> API with different classes, as follows:<\/p>\n<pre><code class=\"language-ts\">\/\/ src\/vscode\/windowApi.ts\r\nimport * as vscode from 'vscode';\r\n\r\nexport class WindowAPI {\r\n  ...\r\n  showInformationMessage(message: string): void {\r\n    vscode.window.showInformationMessage(message);\r\n  }\r\n  ...\r\n}<\/code><\/pre>\n<p>These wrapper classes can then be injected into other classes as needed:<\/p>\n<pre><code class=\"language-ts\">\/\/ src\/commands\/command1.ts\r\n\r\nexport class Command1 {\r\n  constructor(windowApi: WindowAPI, ...) {\r\n\r\n  }\r\n  ...\r\n  run() {\r\n    ...\r\n    this.windowApi.showInformationMessage('Hello World!');\r\n  }\r\n  ...\r\n}<\/code><\/pre>\n<p>Then when unit testing, these wrapper classes can be mocked, injected into the constructor of the unit of code under test, and tests can run without issue since the unit under test depends on the abstraction layer above the <code>vscode<\/code> API, and not the <code>vscode<\/code> API itself.<\/p>\n<p>Unfortunately, this means that all calls to the <code>vscode<\/code> API must be wrapped in new classes and it would not be possible to unit test them since they&#8217;d still depend on calls to the <code>vscode<\/code> API.\nCare must also be taken to wrap the methods as closely as possible as to not lose or change functionality.<\/p>\n<h3>Option 2: Use Jest Manual Mocks<\/h3>\n<p>Option 2 involves taking advantage of <a href=\"https:\/\/jestjs.io\/docs\/manual-mocks\">Jest Manual Mocks<\/a> to mock the utilized <code>vscode<\/code> API.\nFollowing the JavaScript process outlined in <a href=\"https:\/\/www.richardkotze.com\/coding\/unit-test-mock-vs-code-extension-api-jest\">this blog post<\/a>, the same can be done in TypeScript. All the basic mocks of each part of the <code>vscode<\/code> API used in the source code can be created and placed in the <code>__mocks__\/vscode.ts<\/code> file:<\/p>\n<pre><code class=\"language-ts\">\/\/ __mocks__\/vscode.ts\r\nconst mockWindow = {\r\n  showInformationMessage: jest.fn(),\r\n  ...\r\n};\r\n...\r\nconst vscode = {\r\n  window: mockWindow,\r\n  ...\r\n}\r\n\r\nmodule.exports = vscode;<\/code><\/pre>\n<p>In the test code, these mocks can then be overridden to provide specific functionality to that test case:<\/p>\n<pre><code class=\"language-ts\">\/\/ src\/tests\/commands\/command1.test.ts\r\n...\r\nvscode.Uri.parse = jest.fn().mockReturnValue(mockUri);\r\n...<\/code><\/pre>\n<p>Assertions can also be made against particular mocked items as required:<\/p>\n<pre><code class=\"language-ts\">\/\/ src\/test\/command1.test.ts\r\n...\r\nexpect(vscode.Uri.parse).toBeCalled();\r\nexpect(vscode.window.showInformationMessage).toBeCalledWith('Hello World!');\r\n...<\/code><\/pre>\n<blockquote><p>NOTE: make sure mocks are cleared after each test by using <code>jest.clearAllMocks()<\/code> in an <code>afterEach<\/code> block:<\/p>\n<pre><code class=\"language-ts\">afterEach(() =&gt; {\r\n  jest.clearAllMocks();\r\n});<\/code><\/pre>\n<\/blockquote>\n<p>One issue using this method arises when a class inherits from a <code>vscode<\/code> class.\nBecause the <code>vscode<\/code> API is manually mocked, the base class constructor and members will not exist.\nA workaround is then needed to test these concrete classes.<\/p>\n<p>For instance, the following excerpt from <a href=\"https:\/\/github.com\/microsoft\/vscode\/blob\/main\/extensions\/microsoft-authentication\/src\/UriEventHandler.ts\">Microsoft&#8217;s vscode source code<\/a> shows a concrete class that extends a <code>vscode<\/code> class.<\/p>\n<pre><code class=\"language-ts\">\/*---------------------------------------------------------------------------------------------\r\n *  Copyright (c) Microsoft Corporation. All rights reserved.\r\n *  Licensed under the MIT License. See License.txt in the project root for license information.\r\n *--------------------------------------------------------------------------------------------*\/\r\nimport * as vscode from 'vscode';\r\n\r\nexport class UriEventHandler extends vscode.EventEmitter&lt;vscode.Uri&gt; implements vscode.UriHandler {\r\n  public handleUri(uri: vscode.Uri) {\r\n    this.fire(uri);\r\n  }\r\n}<\/code><\/pre>\n<p>In order to test this class and assert on the <code>this.fire(uri)<\/code> method in the tests, an <code>EventEmitter<\/code> &#8220;mock&#8221; implementation would need to be added to the rest of the mocks:<\/p>\n<pre><code class=\"language-ts\">\/\/ __mocks__\/vscode.ts\r\n\r\n\/\/ Because we extend the EventEmitter Class we have to create this \"mocked\" EventEmitter Class\r\nclass EventEmitter {\r\n  event = jest.fn();\r\n  fire: () =&gt; void = jest.fn();\r\n  dispose: () =&gt; void = jest.fn();\r\n}\r\n\r\n...\r\n\r\nconst vscode = {\r\n  EventEmitter,\r\n  ...\r\n}\r\n\r\nmodule.exports = vscode;<\/code><\/pre>\n<h3>Other VS Code Testing Solutions<\/h3>\n<p>Even though VS Code provides guidance for integration testing using their special instance of VS Code, in some cases this is not enough to perform sufficient integration tests.\nFor example, when it is necessary to <em>get<\/em> (rather than <em>set<\/em>) some data, like a custom context or the content of an information message, from the <code>vscode<\/code> API.\nHowever, both of these cases are not possible, as there are no <code>vscode<\/code> API methods to <em>get<\/em> this data.\nIn these cases, it is not possible to complete sufficient integration tests as the ability to verify certain results that come from the <code>vscode<\/code> API is missing.\nA potential workaround found was to use an end-to-end (e2e) user-interface (UI) testing framework, such as <a href=\"https:\/\/github.com\/redhat-developer\/vscode-extension-tester\">vscode-extension-tester<\/a>, to achieve the test coverage desired.<\/p>\n<p>The example below uses the <code>vscode-extension-tester<\/code> to open an extension&#8217;s treeview, check the content, and click a treeview item &#8211; all things that cannot be done solely via integration tests in the special VS Code instance.<\/p>\n<pre><code class=\"language-ts\">  import { ActivityBar, VSBrowser, WebDriver, ModalDialog, SideBarView, By } from 'vscode-extension-tester';\r\n\r\n  ...\r\n\r\n  \/\/ Opens extension's custom side bar view\r\n  sideBarView = await (await new ActivityBar().getViewControl('MyExtension'))?.openView();\r\n  \/\/ get access to the Treeview Section named 'General'\r\n  const treeview = await sideBarView!.getContent().getSection('General');\r\n  const title = await treeview.getTitle();\r\n  assert.strictEqual(title, 'General');\r\n  \/\/ Click treeview item that opens Google\r\n  await driver.wait(async () =&gt; await treeview.openItem('General', 'Google'), 2000);\r\n  const dialog = new ModalDialog();\r\n  const message = await dialog.getMessage();\r\n  assert.strictEqual(message, 'Do you want Code to open the external website?');\r\n  const details = await dialog.getDetails();\r\n  assert.strictEqual(details, 'https:\/\/www.bing.com\/');\r\n  await dialog.pushButton('Cancel');<\/code><\/pre>\n<h2>Final Thoughts<\/h2>\n<p>Testing extensions for VS Code poses a lot of challenges.\nThere are not many resources on the topic and often the testing approach VS Code recommends (utilizing the Extension Development Host) falls short of the testing requirements needed for a production-quality extension.\nThis blog post can hopefully help fill in some of the gaps and provided a good starting point for thoroughly testing VS Code extensions.<\/p>\n<h2>Related Resources<\/h2>\n<ul>\n<li><a href=\"https:\/\/www.richardkotze.com\/coding\/unit-test-mock-vs-code-extension-api-jest\">Mocking the VS Code API for Unit Tests with Jest<\/a><\/li>\n<li><a href=\"https:\/\/code.visualstudio.com\/api\/working-with-extensions\/testing-extension\">VS Code Testing Extensions Documentation<\/a><\/li>\n<li><a href=\"https:\/\/vscode.rocks\/testing\/\">An Article on Testing VS Code Extensions<\/a><\/li>\n<\/ul>\n<blockquote><p>Note: Blog Post Featured Image taken from Microsoft PowerPoint stock images.<\/p><\/blockquote>\n","protected":false},"excerpt":{"rendered":"<p>Due to the nature of VS Code, testing extensions can be quite complex. In this blog post, we explore various ways to write unit tests for VS Code extensions, as well as strategies for writing more sufficient integration tests.<\/p>\n","protected":false},"author":118441,"featured_media":15391,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[1,3451],"tags":[3510,3484,3346,3302,367,3509],"class_list":["post-15390","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-cse","category-ise","tag-integration-testing","tag-jest","tag-testing","tag-typescript","tag-unit-testing","tag-vs-code"],"acf":[],"blog_post_summary":"<p>Due to the nature of VS Code, testing extensions can be quite complex. In this blog post, we explore various ways to write unit tests for VS Code extensions, as well as strategies for writing more sufficient integration tests.<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/ise\/wp-json\/wp\/v2\/posts\/15390","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/devblogs.microsoft.com\/ise\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/devblogs.microsoft.com\/ise\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/ise\/wp-json\/wp\/v2\/users\/118441"}],"replies":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/ise\/wp-json\/wp\/v2\/comments?post=15390"}],"version-history":[{"count":0,"href":"https:\/\/devblogs.microsoft.com\/ise\/wp-json\/wp\/v2\/posts\/15390\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/ise\/wp-json\/wp\/v2\/media\/15391"}],"wp:attachment":[{"href":"https:\/\/devblogs.microsoft.com\/ise\/wp-json\/wp\/v2\/media?parent=15390"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/ise\/wp-json\/wp\/v2\/categories?post=15390"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/ise\/wp-json\/wp\/v2\/tags?post=15390"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}