{"id":188,"date":"2023-09-18T15:38:29","date_gmt":"2023-09-18T14:38:29","guid":{"rendered":"https:\/\/mrzebra.co.uk\/code\/?p=188"},"modified":"2023-09-18T15:40:48","modified_gmt":"2023-09-18T14:40:48","slug":"how-to-mock-a-library-with-nodejs-typescript-and-jest","status":"publish","type":"post","link":"https:\/\/zebra-north.com\/code\/2023\/09\/18\/how-to-mock-a-library-with-nodejs-typescript-and-jest\/","title":{"rendered":"How to Mock a Library with NodeJS, TypeScript, and Jest"},"content":{"rendered":"\n<p>The ability to provide a mock implementation of functionality is essential to unit testing, however the method for doing so with Jest and TypeScript is not well documented.  This tutorial aims to elucidate the method for mocking a JavaScript library that is loaded into your TypeScript project.  This is demonstrated with the <code>ws<\/code> WebSocket library but applies equally to any library, including built-in NodeJS modules.<\/p>\n\n\n\n<!--more-->\n\n\n\n<h2 class=\"wp-block-heading\">Applicability<\/h2>\n\n\n\n<p>Please note that the information here only applies to JavaScript modules that you wish to mock.  If you wish to mock a TypeScript class (one defined in a <code>*.ts<\/code> file as <code>export default class MyClass { ... }<\/code> then the procedure is different.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Prerequisites<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li>NodeJS v18+<\/li>\n\n\n\n<li>TS Node: A TypeScript execution environment for NodeJS.<\/li>\n\n\n\n<li>Jest v29+: The unit testing framework.<\/li>\n\n\n\n<li>TS Jest: Allow Jest to test projects written in TypeScript.<\/li>\n\n\n\n<li>Jest types: TypeScript type definitions for Jest functions.<\/li>\n<\/ul>\n\n\n\n<p>Note that I will be using <code>ts-jest<\/code> instead of installing Babel manually.  The requirements can be installed with:<\/p>\n\n\n\n<pre class=\"wp-block-prismatic-blocks\"><code class=\"language-bash\">npm install --save-dev @jest\/globals @types\/jest jest ts-jest ts-node<\/code><\/pre>\n\n\n\n<p>The library I will be mocking is the WebSocket library <code>ws<\/code>.  Modify the examples to suit whichever library you are using.<\/p>\n\n\n\n<pre class=\"wp-block-prismatic-blocks\"><code class=\"language-bash\"># Install the WebSocket library.\nnpm install ws\n\n# ws is JavaScript so type annotations for TypeScript must be installed separately.\nnpm install --save-dev @types\/ws<\/code><\/pre>\n\n\n\n<p>You will need to configure jest by using the creating the file <code>jest.config.js<\/code> as described in the <a href=\"https:\/\/kulshekhar.github.io\/ts-jest\/docs\/getting-started\/installation\">ts-jest documentation<\/a>:<\/p>\n\n\n\n<pre class=\"wp-block-prismatic-blocks\"><code class=\"language-bash\">npx ts-jest config:init<\/code><\/pre>\n\n\n\n<p>You will also need a TypeScript configuration file <a href=\"https:\/\/www.typescriptlang.org\/docs\/handbook\/tsconfig-json.html\">tsconfig.json<\/a>. Here is mine for reference:<\/p>\n\n\n\n<pre class=\"wp-block-prismatic-blocks\"><code class=\"language-json\">{\n    &quot;compilerOptions&quot;: {\n        &quot;lib&quot;: [\n            &quot;es2023&quot;\n        ],\n        &quot;module&quot;: &quot;Node16&quot;,\n        &quot;target&quot;: &quot;ES2022&quot;,\n        &quot;strict&quot;: true,\n        &quot;skipLibCheck&quot;: true,\n        &quot;forceConsistentCasingInFileNames&quot;: true,\n        &quot;moduleResolution&quot;: &quot;Node16&quot;,\n        &quot;esModuleInterop&quot;: true,\n        &quot;noImplicitAny&quot;: false\n    }\n}<\/code><\/pre>\n\n\n\n<p>Finally: create your test file as <code>tests\/my.test.ts<\/code>. You can then run the test using <code>npx jest<\/code>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">TL;DR: A Full Example<\/h2>\n\n\n\n<pre class=\"wp-block-prismatic-blocks\"><code class=\"language-typescript\">\/\/ Import your library using &quot;import * as ...&quot;.\nimport * as ws from &#039;ws&#039;;\n\n\/\/ Tell Jest to mock all the functions and classes in the library.\njest.mock(&#039;ws&#039;);\n\n\/\/ This is how to access a mocked class.\n\/\/ This lets you do things like count the number of calls to the constructor.\nconst MockWebSocket = ws.WebSocket as jest.MockedClass&lt;typeof ws.WebSocket&gt;;\nMockWebSocket.mockClear();\n\n\/\/ This is how to create a mock object.\nconst socket = new ws.WebSocket(&#039;&#039;) as jest.MockedObjectDeep&lt;ws.WebSocket&gt;;\n\n\/\/ Set the implementation of a mocked function.\nsocket.close.mockImplementation(() =&gt; console.log(&#039;Closed the socket&#039;));\n<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Detailed Explanation<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Import<\/h3>\n\n\n\n<p>The first step is to import your library.  In classic NodeJS you would have written <code>const ws = require(&#039;ws&#039;)<\/code>. In TypeScript you must write <code>import * as ws from &#039;ws&#039;<\/code>. Do not forget the <code>* as<\/code> part. This tells it to import everything from the module and make it accessible as properties of the object <code>ws<\/code>.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Replace the Module with a Mock<\/h3>\n\n\n\n<p>Next, tell Jest to replace everything in the library with mock implementations using <code>jest.mock(&#039;ws&#039;)<\/code>. Note that this function call gets hoisted to the top of the file, so it actually runs before the <code>import<\/code> statement.  It hooks into the <code>import<\/code> call and returns mock items instead of real items.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Access the Mock Constructor<\/h3>\n\n\n\n<pre class=\"wp-block-prismatic-blocks\"><code class=\"language-typescript\">const MockWebSocket = ws.WebSocket as jest.MockedClass&lt;typeof ws.WebSocket&gt;;\nMockWebSocket.mockClear();<\/code><\/pre>\n\n\n\n<p>Here, <code>ws.WebSocket<\/code> is the constructor function for the <code>WebSocket<\/code> class. TypeScript does not know that its implementation was replaced when <code>jest.mock(&#039;ws&#039;)<\/code> hooked the <code>import<\/code> statement, so we must tell it by casting it to <code>jest.MockedClass&lt;typeof ws.WebSocket&gt;<\/code>. This cast maintains the constructor function&#8217;s original signature and augments it with Jest&#8217;s mocking functions such as <code>mockClear()<\/code> and <code>mockImplementation()<\/code>.  The result is saved into <code>MockWebSocket<\/code>.<\/p>\n\n\n\n<p>It should be noted that although <code>MockWebSocket<\/code> is itself a constructor function, its return type is <code>WebSocket<\/code>, not <code>MockWebSocket<\/code>. This means that you cannot instantiate a <code>MockWebSocket<\/code> by using <code>new MockWebSocket<\/code>.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Instantiate a Mock Object<\/h3>\n\n\n\n<pre class=\"wp-block-prismatic-blocks\"><code class=\"language-typescript\">const socket = new ws.WebSocket(&#039;&#039;) as jest.MockedObjectDeep&lt;ws.WebSocket&gt;;<\/code><\/pre>\n\n\n\n<p>To create a mock object use <code>new<\/code> to create the object as normal, and then cast it to a <code>MockedObjectDeep<\/code>. As with accessing the constructor, <code>new ws.WebSocket<\/code> actually constructs a mocked <code>WebSocket<\/code> due to <code>jest.mock()<\/code> having swapped out the implementation of the constructor during the import, but TypeScript doesn&#8217;t know this and so the cast is required to tell it.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Implement a Mock Function<\/h3>\n\n\n\n<p>To add functionality to your mock object you can use Jest&#8217;s <code>mockImplementation()<\/code> function.  For example, we may wish to ensure that <code>send()<\/code> is not called after <code>close()<\/code>.<\/p>\n\n\n\n<p>To accomplish this we will add a new property to the mock called <code>isClosed<\/code>, and provide implementations for <code>send()<\/code> and <code>close()<\/code>.<\/p>\n\n\n\n<pre class=\"wp-block-prismatic-blocks\"><code class=\"language-typescript\">\/\/ Extend the mocked WebSocket to give it an &#039;isClosed&#039; property.\ntype MockedSocket = jest.MockedObjectDeep&lt;ws.WebSocket&gt; &amp; { isClosed: boolean }\n\n\/\/ Create the mock socket.\nconst socket = new ws.WebSocket(&#039;&#039;) as MockedSocket;\nsocket.isClosed = false;\n\n\/\/ Implement the close() and send() functions.\nsocket.close.mockImplementation(() =&gt; socket.isClosed = true);\nsocket.send.mockImplementation(() =&gt; { if (socket.isClosed) throw new Error(&#039;Sending after closing&#039;); });\n\n\/\/ Run the code under test, using the mock socket.\n\/\/ testMyCode(socket);<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Constructor Parameters<\/h3>\n\n\n\n<p>It is slightly unfortunate that the mock class constructors take the same parameters as the real class constructors.  This means that you must pass valid and correctly typed arguments to your mock object constructors, even though they will not be used.  This can result in having to create a lot of unnecessary mocks as you need to mock the class&#8217;s dependencies, the dependencies&#8217; dependencies, and so on.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>The ability to provide a mock implementation of functionality is essential to unit testing, however the method for doing so with Jest and TypeScript is not well documented. This tutorial aims to elucidate the method for mocking a JavaScript library that is loaded into your TypeScript project. This is demonstrated with the ws WebSocket library [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[22],"tags":[23,31,25],"class_list":["post-188","post","type-post","status-publish","format-standard","hentry","category-nodejs","tag-jest","tag-typescript","tag-unit-testing"],"_links":{"self":[{"href":"https:\/\/zebra-north.com\/code\/wp-json\/wp\/v2\/posts\/188","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/zebra-north.com\/code\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/zebra-north.com\/code\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/zebra-north.com\/code\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/zebra-north.com\/code\/wp-json\/wp\/v2\/comments?post=188"}],"version-history":[{"count":7,"href":"https:\/\/zebra-north.com\/code\/wp-json\/wp\/v2\/posts\/188\/revisions"}],"predecessor-version":[{"id":195,"href":"https:\/\/zebra-north.com\/code\/wp-json\/wp\/v2\/posts\/188\/revisions\/195"}],"wp:attachment":[{"href":"https:\/\/zebra-north.com\/code\/wp-json\/wp\/v2\/media?parent=188"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/zebra-north.com\/code\/wp-json\/wp\/v2\/categories?post=188"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/zebra-north.com\/code\/wp-json\/wp\/v2\/tags?post=188"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}