Cannot set property x of [object Module] which has only a getter
mralbobo opened this issue · 22 comments
Error
debug.js:21 TypeError: Cannot set property AnalyticsService of [object Module] which has only a getter
at <Jasmine>
at new MockManager (mock-manager.js:26)
at Function.push../node_modules/ts-mock-imports/lib/import-mock.js.ImportMock.mockClass (import-mock.js:15)
at UserContext.<anonymous> (fbdb-requests.service.spec.ts:54)
at ZoneDelegate.invoke (zone-evergreen.js:359)
at ProxyZoneSpec.push../node_modules/zone.js/dist/zone-testing.js.ProxyZoneSpec.onInvoke (zone-testing.js:308)
at ZoneDelegate.invoke (zone-evergreen.js:358)
at Zone.run (zone-evergreen.js:124)
at runInTestZone (zone-testing.js:561)
at UserContext.<anonymous> (zone-testing.js:576)
at <Jasmine>
Minimal code
import { ImportMock } from 'ts-mock-imports';
import * as analyticsModule from 'services/AnalyticsService';
analyticsServiceMock = ImportMock.mockClass(analyticsModule, 'AnalyticsService');
It's that mock call that mockClass call that blows up. Similarly blows up with mockOther.
Cut down module attempted to be mocked
import * as utils from "utils";
//in the future this should subscribe to the various state services, plan, project, user, etc
// for now? leach off the old analytics service
export class AnalyticsService{
private globals = {}
multiselect(){ ... }
}
export let analyticsService = new AnalyticsService();
window.analyticsService = analyticsService;
Other things of note
- build: angular-cli v8
- testing framework: jasmine 3.4
Near as I've been able to tell, you're unable to mock es6 imports because they're supposed to be readonly. Which is roughly what the above illustrates. But I figured this package figured out some way around that limitation.
Can you try creating and assigning AnalyticsService
outside of the file that exports it? MockImports
works on the assumption that the class is not instantiated until after it has been mocked. The way this code works will instantiate AnalyticsService
on import of the file.
Revised cut down example
export class AnalyticsService{
private globals = {}
multiselect(){ ... }
}
Same error.
The code using it was also modified to this (whereas before is imported the instance directly):
import { AnalyticsService } from 'services/AnalyticsService';
const analyticsService = new AnalyticsService();
Not particularly surprised given that mockOther on either/ both versions (class and instance) with simple objects or other classes of those had roughly the same result.
Throwing out a concept, since skimming this https://exploringjs.com/es6/ch_modules.html#_imports-are-read-only-views-on-exports seems to indicate to me that this is expected behavior with es6 imports.
It also implies though that there's likely a fairly reasonable workaround that could be done.
Starting from here https://github.com/EmandM/ts-mock-imports/blob/master/src/managers/mock-manager.ts
If I'm reading this correctly... this code (at a really high level) creates a new stub class which gets a mocked representation of each function of the original class added to it. Then the module replaces the real class with the stub class. If I'm accurate, making this work under es6 should just be a matter of ditching the stub class and instead mocking the original module. Potentially as an extra top level function (mockClassInPlace) or something. Sound sane?
This library works on top of ES6 imports. The trick of the library is to assign to that read-only value.
stubClass
is the mock of the original module. It copies all of the functions on the original module and creates no-op functions (functions that return undefined) in their place.
It is possible that your error is caused by your build process. Are you using babel to transpile by any chance? What is the structure of your dependency tree?
Shouldn't be, pretty bog standard angular-cli (it gets fairly arcane behind the scenes though and changes every version. Should boil down to webpack and a custom loader) for the typescript at least. I'll see if I can't setup a cut down repro next week.
which typescript version are you using? If it's 3.9 or higher it's most likely related to microsoft/TypeScript#38568
See jasmine/jasmine#1817 (comment) for a temporary workaround that should work.
Unfortunately, this looks like a change in how typescript works that breaks the behaviour of this library.
@EmandM Could you please add a warning to the Readme about this gotcha? I got bitten by this today and spent half a day bashing my head against the desk trying to figure out why on earth my mocks are no longer doing a thing :-(. (Ranting aside: I really love this library, thanks for creating it! It really hurts to use dependency injection instead…)
So I did a quick and dirty prototype where I basically duplicated MockManager into MockManagerInPlace, bypassed the stubclass and instead directly wrote the mocked functions onto the original class. And it worked to solve my particular issue. Both on my current version of typscript and 3.9.3.
https://github.com/mralbobo/ts-mock-imports/blob/master/src/managers/mock-manager-inplace.ts
Basically the only thing changed was the replace function and commenting out the this.module
replacement in the constructor. Plus some other boilerplate to expose that class.
Naturally that as written completely breaks the "unstub" concept and probably other things. But it does prove that something can be done.
Thanks for this prototype. I might just be able to use this to get everything working again.
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Mark the issue as fresh with /remove-lifecycle stale
.
Thank you for your contributions.
Hit this issue as well, thought I was going mad when my mocks stopped working! @EmandM do you think you'll be able to work around this issue? Would be great to have a solution that doesn't require adding other tweaks to all our projects using ts-mock-imports
:)
/remove-lifecycle stale
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Mark the issue as fresh with /remove-lifecycle stale
.
Thank you for your contributions.
/remove-lifecycle stale
@mralbobo I can't seem to get a repro of this issue working on my machine. Do you mind sharing your tsconfig?
Attached, slightly redacted
{
"compileOnSave": false,
"compilerOptions": {
"baseUrl": "./",
"outDir": "./dist/out-tsc",
"sourceMap": true,
"declaration": false,
"downlevelIteration": true,
"experimentalDecorators": true,
"module": "esnext",
"moduleResolution": "node",
"importHelpers": true,
"target": "es2015",
"paths": {},
"typeRoots": [
"node_modules/@types"
],
"lib": [
"es2018",
"dom"
],
"resolveJsonModule": true,
"allowSyntheticDefaultImports": true
},
"angularCompilerOptions": {
"fullTemplateTypeCheck": true,
"strictInjectionParameters": true
}
}
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Mark the issue as fresh with /remove-lifecycle stale
.
Thank you for your contributions.
ImportMock.mockClassInPlace() has now been added to the library. This will give you the same functionality as the original if you are simply mocking functions on classes.
This new functionality cannot edit the constructor or any variables on the mocked class however, so use with caution.
Hi, it seems like this issue and the limitations that come with TS 3.9 should be at the top of the readme, in large text -- otherwise this risks wasting a lot of developer time. 🙏
Typescript 3.9 introduced new functionality that blocks the key functionality of this library. With certain compilation structures, it is no longer possible to replace module exports.
There is no true workaround for this issue.
Angular 9+ solution:
In the tsconfig.spec.json file, add to compileOptions:
"module": "commonjs",
This solved all the issues with import * as XXX
and spyOn(XXX, "methodName")
The solution that worked for me is this one:
I had a module exporting a static function:
export class TimeProvider {
/**
* return the current date in the system
*/
public static now(): Date {
return new Date();
}
}
and I use to mock it like this:
import * as TimeProviderModule from '@project/fwk-lib-core';
....
let timeProviderMock;
...
beforeEach(() => {
timeProviderMock = ImportMock.mockStaticClass(TimeProviderModule, 'TimeProvider');
}
it('test', async () => {
const nowMock: sinon.SinonStub = timeProviderMock.mock('now', date);
...
expect(nowMock.calledOnce).toBe(true);
})
and I changed it to this:
import * as TimeProviderModule from '@project/fwk-lib-core';
....
let mockStaticF ;
...
beforeEach(() => {
jest.mock('@project/fwk-lib-core/dist/time/time.provider');
mockStaticF = jest.fn().mockReturnValue(date);
TimeProvider.now = mockStaticF;
}
it('test', async () => {
...
expect(mockStaticF).toBeCalledTimes(1);
})
effectively not using ts-mock-imports for static functions exported in external modules...