Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Unable to mock the native node:zlib module with TypeScript and Mocha/Chai/Sinon. #185

Closed
SmashingQuasar opened this issue Nov 13, 2022 · 2 comments

Comments

@SmashingQuasar
Copy link

Hey!

I am currently working on a project using Node 18, TypeScript 4.8, purely ESM written using .mts files and the Mocha + Chai + Sinon test framework.

I have a very simple code to test that sets a header to a ServerResponse object and then pipes it through zlib to enable Gzip compression.

The code is as follows:

// import type { IncomingMessage } from "http";
import { ServerResponse } from "node:http";
import { createGzip } from "node:zlib";
import type { Gzip } from "node:zlib";
import type { HTTPStatusCodeEnum } from "../HTTP/HTTPStatusCodeEnum.mjs";

class Response extends ServerResponse
{
	private content: string = "";

	/**
	 * send
	 */
	public send(content?: string|Buffer|undefined): void
	{
		this.setHeader("Content-Encoding", "gzip");

		console.debug("called");

		// @TODO: Make response compression great again
		const ENCODER: Gzip = createGzip();
		ENCODER.pipe(this);
		ENCODER.write(content ?? this.content);
		ENCODER.end();
	}

	/**
	 * getContent
	 */
	public getContent(): string
	{
		return this.content;
	}

	/**
	 * setContent
	 */
	public setContent(content: string): void
	{
		this.content = content;
	}

	/**
	 * setStatusCode
	 */
	public setStatusCode(status_code: HTTPStatusCodeEnum): void
	{
		this.statusCode = status_code;
	}
}

export { Response };

This is part of a class Response that extends ServerResponse. The class does work as intended and has been used successfully even in production.

I wanted to rewrite my entire test base to use the configuration I explained before.
I then realized that ES modules cannot be stubbed using Sinon since that are locked.
I stumbled upon this library that seemed to be exactly what I needed. I briefly considered Proxyquire but it seems to only work with require and nothing else.

Here is my .mocharc.json file:

{
  "extension": [
    "ts"
  ],
  "node-option": [
    "experimental-specifier-resolution=node",
    "loader=ts-node/esm",
    "loader=esmock"
  ],
  "spec": [
    "__tests__/**/*.spec.mts"
  ],
  "timeout": 5000,
  "parallel": true,
  "checkLeaks": true,
  "diff": true,
  "forbidPending": true
}

Here is the tsconfig.json file used for testing:

{
	"compilerOptions": {
		"outDir": "build",
		"allowJs": false,
		"target": "ESNext",
		"alwaysStrict": true,
		"removeComments": true,
		"strict": true,
		"charset": "UTF-8",
		"noImplicitAny": true,
		"noImplicitReturns": true,
		"noImplicitThis": true,
		"strictNullChecks": true,
		"strictFunctionTypes": true,
		"allowUnusedLabels": false,
		"module": "NodeNext",
		"allowUnreachableCode": false,
		"noUnusedLocals": true,
		"noUnusedParameters": true,
		"newLine": "LF",
		"moduleResolution": "NodeNext",
		"useUnknownInCatchVariables": true,
		"lib": ["ESNext"],
		"noUncheckedIndexedAccess": true,
		"strictPropertyInitialization": true,
		"strictBindCallApply": true,
		"forceConsistentCasingInFileNames": true,
		"listFiles": false,
		"listEmittedFiles": true,
		"noErrorTruncation": true,
		"noFallthroughCasesInSwitch": true,
		"noImplicitOverride": true,
		"noPropertyAccessFromIndexSignature": true,
		"pretty": true,
		"sourceMap": true
	},
	"include": [".", "./**/.*", "./**/*.json"],
	"ts-node": {
	  "files": true,
	  "swc": true
	}
}

Here is the test that I wrote:

/* eslint-disable @typescript-eslint/no-unused-expressions, @typescript-eslint/no-empty-function, max-len, max-lines-per-function */

import { IncomingMessage } from "node:http";
import { Socket } from "node:net";
import { expect } from "chai";
import esmock from "esmock";
import { Response } from "../../../../src/main/Web/Server/Response.mjs";


await esmock(
	"../../../../src/main/Web/Server/Response.mts",
	import.meta.url,
	{
		zlib: {
			createGzip: {
				pipe: () =>
				{
					console.debug("pipe called 2");
				}
			}
		}
	}
);

describe(
	"Response",
	(): void =>
	{
		describe(
			"send",
			(): void =>
			{
				it(
					"should call the Gzip.pipe functions from the zlib native module",
					(): void =>
					{
						const SOCKET: Socket = new Socket();
						const INCOMING_MESSAGE: IncomingMessage = new IncomingMessage(SOCKET);
						const RESPONSE: Response = new Response(INCOMING_MESSAGE);

						RESPONSE.send("Test");

						expect(PIPE_STUB.called).to.be.true; // Irrelevant as I previously tried to use SinonStubs (which are still in the test but I removed them from this sample since they are never called anywhere. This expect will always fail but this is not my problem.
					}
				);
			}
		);
	}
);

The thing is, whilst running the test, it properly prints "called" from the send method, but it never prints "pipe called 2" from the mocked module.

I have been scratching my head why for some time now and I have honestly no idea what's going on. I do not get any error from esmock with this syntax. If I change the module path to use the .mjs extension I get an invalid module id error. This means the .mts extension syntax seems to be correct but somehow, the module simply isn't mocked.

I believe I am missing something here. Could you help me solve this issue please? 🙏

Thanks! ❤️

@iambumblehead
Copy link
Owner

iambumblehead commented Nov 13, 2022

@SmashingQuasar thanks for opening this issue. Two things, one: only the esmock-returned Response will use the mocked import tree and so that is the Response definition that should be used in the test,

-import { Response } from "../../../../src/main/Web/Server/Response.mjs";


-await esmock(
+const { Response } = await esmock(
	"../../../../src/main/Web/Server/Response.mts",
	import.meta.url,
	{
		zlib: {
			createGzip: {
				pipe: () =>
				{
					console.debug("pipe called 2");
				}
			}
		}
	}
);

The other thing is that esmock's scripted resolver might not handle the 'mts' extension correctly. If that's the case, run the test process with --experimental-import-meta-resolve to use node's native resolver. If --experimental-import-meta-resolve is needed, please open an issue here and I'll resolve the scripted resolver issue :)

esmock's own unit-tests are run twice for each test folder, one time with and another without --experimental-import-meta-resolve so example calls are seen there, for example this https://github.com/iambumblehead/esmock/blob/master/tests/tests-nodets/package.json#L17

esmock should work for your use-case

@SmashingQuasar
Copy link
Author

@iambumblehead Hey!

Thanks for your super-quick answer!

Actually I figured out what I was doing wrong just before I read your answer. It was indeed because I was not using the returned mocked module and was attempting to use the native one. I probably misread the documentation, my bad!

It works perfectly using .mts and Sinon. Thanks for your work, this library works like a charm, it totally solved my issue!
It's unfortunate that the syntax is this heavy but I understand there is most likely no other way to do this as of today.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants