-
Notifications
You must be signed in to change notification settings - Fork 1.2k
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
transformSync and jest-mock #412
Comments
Hey, could you share your setup, looks interesting? |
I believe esbuild's behavior is correct here because ECMAScript module exports are supposed to not be writable. From the section on "module namespace exotic objects" in the specification:
This matches the actual behavior that you get in node when you try to assign to a property on a module namespace: // export.mjs
export let foo = 123 // import.mjs
import * as ns from './export.mjs'
ns.foo = () => console.log('mock') If you run this code with
|
Hi @evanw, this is all correct. My intent was not to point out something wrong with esbuild (it is great, we use it for everything at cling.com - we bundle our web_app and we transpile all of our Node code with esbuild). We have a huge test-base that would greatly benefit from esbuild's speed (even though ts-jest in transpile mode is not that slow). In the end we just want to use a single build tool, so that we actually test exactly what goes into production. You are correct wrt ECMAScript modules. But my target is To illustrate what I am trying to do, I created a small sample repo at https://github.com/flunderpero/esbuild-jest-example You can check it out and run the tests with either I would suggest that esbuild should mimic what tsc does when it comes to transpiling Typescript to JS. |
@evanw Are you considering this? |
Running into the same issue. Getting this working and replacing ts-jest would be immensely useful & appreciated! 🙏 |
Could we argue that, by specifying cjs format that we do not expect es module behavior? Or would we need to have another format to differentiate between the case where a user wants to have their code work in a commonjs environment, but keep the behavior they would expect when they write their code using ES modules, versus explictly transpiling the code into something that resembles other/older transpilers (e.g. adding a setter or using data descriptors for exports, specifically for mocking). Or is setting format generally just expected to change only the format but not the behavior? |
Yes, that's correct.
Not at the moment, no. There are a lot of current priorities and this is low priority for me given that this wasn't the intended use case for esbuild and it's not a simple change. Something like swc might be a better fit for this. I did an initial test and it looks like their CommonJS transform doesn't follow ECMAScript module semantics and might work with Jest like this. |
@evanw I was toying around with this and I agree that it is not a simple change. If the only use case is mocking with Jest atm, I agree that it should be low priority. I wonder if there is any library out there doing some monkey patching on common/standard libs and would not work out-of-the-box. I don't know of anything like that and perhaps it's just me stretching to find another use case. :-) Feel free to close this issue. |
FWIW I'm currently investigating an esbuild-compatible alternative to a similar testing framework, although it wasn't Jest specifically. I came up with a helper function that looks something like this (not actually tested with Jest): export interface HasSpyOn {
spyOn(): jest.SpyInstance
}
export function enableSpyOn<T extends Function>(fn: T): T & HasSpyOn {
if (TEST) {
let name = fn.name, obj = {[name]: fn};
(fn as any) = function(this: any) { return obj[name].apply(this, arguments) };
(fn as any).spyOn = () => jest.spyOn(obj, name);
}
return fn as any;
} Instead of doing this: // file.ts
export function fn() {}
// file_test.ts
import * as file from './file'
let spy = jest.spyOn(file, 'fn') You should be able to do this instead: // file.ts
import {enableSpyOn} from 'helpers/enableSpyOn'
export const fn = enableSpyOn(function fn() {})
// file_test.ts
import {fn} from './file'
let spy = fn.spyOn() With esbuild you would define |
That would mean you need to compile your code twice. Once for testing and once for production. And since both are different (e.g. the test build will contain There might be side-effects in imports which where tree-shaked in the production build I think.
So the option here would then be, to ship |
Hi @evanw I believe your implementation for es module is all correct, but I think it should not be used as an implementation of the
If the code generated by esbuild cannot be run smoothly in node, how can we call it "node-friendly"? Maybe a different set of helper functions should be applied when platform is set to node. |
This is happening because esbuild's bundler is respecting the semantics of the original code. The underlying problem is that Jest's Being "node friendly" only means that code which works in node when it's not bundled also works in node when it's bundled with esbuild. It doesn't guarantee that code which doesn't work in node without being bundled will work in node after being bundled with esbuild. It is possible to use Jest with esbuild. You just need to write this: const sut = require("../sut.js"); instead of this: import * as sut from "../sut.js"; since Jest's |
Couldn't we introduce a new output format (eg |
this whole topic is very interesting to follow. i like the approach that esbuild is strictly implementing the specification (thus, esm being immutable). it's unfortunate that jest is not compliant with that. would be nice if that workaround worked, but for me it doesn't:
i still get the same "cannot redefine property" error that i got with the esm approach. thing is, i mixed imports, using commonjs instead of |
@evanw I was able to find a fix/workaround on the above issue. If you could review it, it would be helpful. Background:
The syntax When we use the syntax Although using spies is a valid use case from a testing point of view, handling the same within esbuild seems invalid. I found a workaround to address the above issue. Please find code snippet for fix/workaround here. |
Dunno whether this is a similar issue, but there are many third party libraries that shim $.isFunction = $.isFunction || function(obj) {
return typeof obj === "function" && typeof obj.nodeType !== "number";
}; ^ The above will crash in a similar fashion with: EDIT: This particular scenario is produced by trying to |
It works fine for me: import $ from 'jquery'
$.isFunction = $.isFunction || function (obj) {
return typeof obj === "function" && typeof obj.nodeType !== "number";
};
console.log($.isFunction + '') Make sure you use |
This whole conversation is fascinating, and we're now coming across this issue as we attempt to speed up some of our frontend tooling by moving from webpack/babel to esbuild. Suppose one was writing a greenfield project -- is using jest the wrong approach if we want to keep things modern and use only esmodules? If so, what's the alternative? My understanding is pretty thin at the moment, but it doesn't seem like any sort of spying/mocking library would be compatible with the fact that ECMAScript module exports are supposed to not be writable. |
Even though I initially raised this issue, we since then removed all of our
It is not as convenient as simply using Currently, we are not using ESM in our tests because Jest still has some issues with that. @evanw I am fine with closing this issue. |
@flunderpero are the modules you're mocking ESM or CJS? And are they your own code or external modules from I cannot get |
My previous comment contains a workaround for
Makes sense. This is a problem with Jest, not with esbuild. Closing. |
Can you share an example of what figma did? Would love to inspect code because I couldn't see a resolution in this thread. |
I can’t speak for Figma because I don’t work there. This comment documents what I was exploring when I did still work there, however: #412 (comment) |
@evanw, I'm trying to replace on my side the This workaround works, but it isn't what I'm looking for: #412 (comment) |
I have a workaround. It's probably too hacky, as I have to replace the var __export = (target, all) => {
for (var name in all) {
Object.defineProperty(target, name, {
value: all[name],
writable: true,
});
}
}; We want to mock the import * as dependencies from './dependencies';
export function myFunction1(c) {
const imports = dependencies;
return imports.testFn1(c);
} As you can observe, we must re-assign import export const spyOn = (obj, method, returnValue) => {
const stats = {
callCount: 0,
called: false,
calls: [],
returnValue: null,
};
Object.defineProperty(obj, method, {
value: (...args) => {
stats.callCount++;
stats.called = true;
stats.calls.push(...args);
stats.returnValue = returnValue();
return stats.returnValue;
},
writable: true,
});
return stats;
}; Finally, the ...
const stats = spyOn(dependencies, "testFn1", () => "replaced1");
const result = myFunction1("something");
expect(stats.callCount).toBe(2);
expect(stats.called).toBe(true);
expect(stats.calls).toEqual(["ooo", "iii"]);
expect(stats.returnValue).toBe("replaced1");
expect(result).toBe("replaced1");
... |
We try to use esbuild as a transformer for Jest using the
cjs
format. All works fine but we cannot usejest.spyOn()
on any of the transpiled modules. Jest complains withTypeError: Cannot set property foo of #<Object> which has only a getter
, which is correct, because esbuild only defines getters for exported symbols, like in__export
:Typescript exports symbols by simply assigning them to
exports
, like this:The text was updated successfully, but these errors were encountered: