Error: <spyOn> : fromEvent is not declared writable or has no setter
Solution 1
You need to spy on a property of rxjs. Using spyOnProperty will solve the error. Try this
import * as rxjs from 'rxjs'
import { of, fromEvent } from 'rxjs';
spyOnProperty(rxjs, 'fromEvent').and.returnValue(of({}))
you can also add to getter/setters using this like
spyOnProperty(rxjs, 'fromEvent', 'get').and.returnValue(false)
Hope this helps
Solution 2
The problem is that a module namespace object like import * as rxjs
has a specific behavior and doesn't allow to mutate itself in many cases. Here's a few relevant links:
- Cannot spy on individual functions that are individually exported
- Mocking fromEvent in rxjs6
- Spy on jasmine function from an Angular library is not working
- import * as (jasmine spyOn) not writable
- Can webpack 4 modules be configured as to allow Jasmine to spy on their members?
As you can see it's a well known issue but at the moment there's no ultimate solution that works in all cases. The most common workaround is to use spyOnProperty
like Alejandro Barone's answer suggests, I've tried this solution in Angular 8 / TypeScript 3.4 setup and it works well, but in doesn't work in Angular 10 / TypeScript 4 and gives the following error:
fromEvent is not declared configurable
But let's look at the problem from a different angle. As an example you can imagine a class that subscribes to window's resize
event and increments some counter when the event is triggered. The class can use fromEvent
to subscribe to the event or can subscribe directly via window.addEventListener
. In both cases the class will behave the same - the counter will be incremented when the event happens. By spying on fromEvent
you make an assumption that the class uses that function, however the only contract the class gives you is its interface. In the future someone might decide to use window.addEventListener
instead of fromEvent
and the tests will be broken despite the class works the same way. So the right way to test such a class is to trigger window's resize
event and check that the counter is incremented. It's a good practice to test classes like black boxes without any assumptions about its implementation.
If it's still important to you to spy on fromEvent
function, you can create a wrapper on it and mock it in your tests, for example:
import { fromEvent, Observable, of } from 'rxjs';
import { FromEventTarget } from 'rxjs/internal/observable/fromEvent';
class EventObserver {
observe<T>(target: FromEventTarget<T>, eventName: string): Observable<T> {
return fromEvent(target, eventName);
}
}
class MyClass {
constructor(
private eventObserver = new EventObserver()
) {}
doSomething() {
this.eventObserver.observe(window, 'resize').subscribe(() => {
// do something
});
}
}
it("#doSomething should subscribe to window's resize event", () => {
const eventObserver = jasmine.createSpyObj<EventObserver>(EventObserver.name, ['observe']);
eventObserver.observe.and.returnValue(of({}));
const myClass = new MyClass(eventObserver);
myClass.doSomething();
expect(eventObserver.observe).toHaveBeenCalledTimes(1);
expect(eventObserver.observe).toHaveBeenCalledWith(window, 'resize');
});
Solution 3
Complementing Omair's answer. On my case, I need to have a function on the returnValue
statement.
const fromEventSpy = spyOnProperty(rxjs, 'fromEvent').and.returnValue(() => rxjs.of({}));
Cheers!
Solution 4
Angular +10 solution that allows to spyOn modules "import * as XXX from 'abc'"
Add to tsconfig.spec.json
"compilerOptions": {
"module": "commonjs",
"target": "es5",
...
},
Both props module and target are important! This won't work unless target is set to es5 and module is not commonjs;
Now in your spec file you can do something like this:
import * as selectors from '@app/state';
const mockModuleFunc = (importModule: any, methodName: string, returnValue: any = null) => {
let currentVal = importModule[methodName];
const descriptor = Object.getOwnPropertyDescriptor(importModule, methodName);
if(!descriptor.set) {
Object.defineProperty(importModule, methodName, {
set(newVal) {
currentVal = newVal;
},
get() {
return currentVal;
},
enumerable: true,
configurable: true
});
}
/**
* This actually works now and doesn't throw "is not declared writable or has no setter" error.
* Use spyOn as always, example with parameterized ngrx selectors:
*/
return spyOn(importModule, methodName).and.returnValue(() => returnValue);
}
// Usage example
it('Your test description', () => {
const spy = mockModuleFunc(selectors, 'yourFunctionToSpyOn', 'xyz');
...
})
Related videos on Youtube
Raju
Updated on June 04, 2022Comments
-
Raju almost 2 years
The old code uses rxjs v5.5.12, We copied the same code to our new project which uses rxjs v6.4.0. We are getting this error when we tried to run the test case.
Old Code:
import * as ObservableEvents from 'rxjs/Observable/fromEvent'; spyOn(ObservableEvents, 'fromEvent').and.returnValue(new Subject<any>().asObservable());
New Code:
import * as rxjs from 'rxjs'; spyOn(rxjs, 'fromEvent').and.returnValue(new Subject<any>().asObservable());
In both cases we are getting this error:
Error: : fromEvent is not declared writable or has no setter
We couldn't find a valid resource to solve this issue.
Update #1
We tried using
import * as rxjs from 'rxjs'; spyOn(jasmine.createSpyObj(rxjs), 'fromEvent').and.returnValue(new Subject<any>().asObservable());
but this time, we got
createSpyObj requires a non-empty array or object of method names to create spies for thrown
Update #2:
We used the code from @Omair-Nabiel, now getting a new error
TypeError: Object(...) is not a function at XxxPopoverDirective.fromEvent [as createPopover] (http://xxx:xxxx/src/app/shared/xxx/xxx.directive.ts?:113:37) at XxxPopoverDirective.createPopover [as mouseClick] (http://xxx:xxxx/src/app/shared/xxx/xxx.directive.ts?:70:14) at runTest (http://xxx:xxxx/src/app/shared/xxx/xxx.directive.spec.ts?:181:19)
xxx.directive.ts
line 113-> this.componentRef && this.componentRef.destroy(); this.componentRef = null; line 70-> constructor( ... private resolver: ComponentFactoryResolver, ... ) { }
Update #3
Hi Omair Nabiel, Please find the below code we are using, please let me know the solution,
file="popover.directive.ts" Code:
import { fromEvent } from 'rxjs/Observable/fromEvent'; this.clickOutSub = fromEvent(this.documentRef.getDocument(), 'click').subscribe(this.clickOut.bind(this)); file="popover.directive.spec.ts" Code: import * as ObservableEvents from 'rxjs/Observable/fromEvent'; function runTest() { spyOn(ObservableEvents, 'fromEvent').and.returnValue(new Subject<any>().asObservable()); } it('...', () => { expect(ObservableEvents.fromEvent).toHaveBeenCalled(); });
-
Morphyish over 4 yearsProbably not the answer you are looking for, but it is a known issue in Jasmine. There are a few workarounds and ideas in the thread that might do the trick for you.
-
Omair Nabiel over 4 yearsCan you share the component code. It seems like you need to mock the mouseClick or popover that you might be listening to in fromEvent, or try using it with a simple custom function instead of mouseclick etc
-
Omair Nabiel over 4 yearsYou might need to call spyOn().withArgs().and.returnValue() and pass in the stub documentRef.Document and 'click' in withArgs(). Note: WithArgs is avaiable after Jasmine v3.0. And I was looking for the complete component code for which you're writing the tests not the spec file
-
-
Valeriy Katkov over 4 yearsDid you tried your solution? It doesn't work, I just get error:
TypeError: rxjs__WEBPACK_IMPORTED_MODULE_1__.fromEvent is not a function
-
Valeriy Katkov over 4 years[email protected], [email protected] and I've got the above error. May be you're using
commonjs
modules, as described in my answer? -
Valeriy Katkov over 4 yearsTo be precise, the code above compiles, but
fromEvent
doesn't returnof({})
. I triedconst spy = spyOnProperty(rxjs, 'fromEvent'); console.log(spy.calls.count())
and I've got the above runtime error. -
Omair Nabiel over 4 yearsYup I'm using common.js
-
Ranjith Varatharajan over 4 yearswe tried this, but getting error on the ...args part in
const o=realFormEvent(...args);
-
Ranjith Varatharajan over 4 yearsTried this getting
TypeError: Object(...) is not a function
-
Valeriy Katkov over 4 years@RanjithVaradan You're right, thanks, I fixed the example.
-
Ranjith Varatharajan over 4 yearsmmm. i tried this updated code but getting a new error
Uncaught TypeError: Invalid event target thrown
-
Valeriy Katkov over 4 years@RanjithVaradan It has to be a runtime error that rxjs
fromEvent
throws if the target is invalid. Which target do you pass into the function? -
Neurotransmitter about 4 yearsThat is important, since you can't define, for example,
callFake
onspyOnProperty
.returnValue
is a way to go. -
taleb over 3 yearsawesome, you cannot believe how grateful I am. I've been stuck on this for two days until I found your answer. the inline function is very important as I got an error without it
-
taleb over 3 years@ValeriyKatkov look at Alejandro's answer below, he includes an inline function in returnsValue() which should fix the error you're getting. I had the same error and following his answer fixed it stackoverflow.com/a/58768076/3070228
-
Valeriy Katkov over 3 years@taleb thank you for letting me know! Indeed it fixes the
fromEvent is not a function
error and it works well in Angular 8, but unfortunately it doesn't work in Angular 10, it gives another errorfromEvent is not declared configurable
. I've added a reference to Alejandro's answer into my own answer as well as some relevant links, hope it will help someone. -
Rob Lyndon over 2 yearsI tried this, but the setter threw an error. It would have been great if it had worked, but this seems very much dependent on the packages available at the time..
-
Pawel Miatkowski over 2 yearsSorry to hear that. I would double check tsconfig.spec.json module and target props. I have a default angular setup, recently updated to v11 and it works like charm.