Error: <spyOn> : fromEvent is not declared writable or has no setter

14,007

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:

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');
    
       ...
    })
Share:
14,007

Related videos on Youtube

Raju
Author by

Raju

Updated on June 04, 2022

Comments

  • Raju
    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
      Morphyish over 4 years
      Probably 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
      Omair Nabiel over 4 years
      Can 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
      Omair Nabiel over 4 years
      You 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
    Valeriy Katkov over 4 years
    Did 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
    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
    Valeriy Katkov over 4 years
    To be precise, the code above compiles, but fromEvent doesn't return of({}). I tried const spy = spyOnProperty(rxjs, 'fromEvent'); console.log(spy.calls.count()) and I've got the above runtime error.
  • Omair Nabiel
    Omair Nabiel over 4 years
    Yup I'm using common.js
  • Ranjith Varatharajan
    Ranjith Varatharajan over 4 years
    we tried this, but getting error on the ...args part in const o=realFormEvent(...args);
  • Ranjith Varatharajan
    Ranjith Varatharajan over 4 years
    Tried this getting TypeError: Object(...) is not a function
  • Valeriy Katkov
    Valeriy Katkov over 4 years
    @RanjithVaradan You're right, thanks, I fixed the example.
  • Ranjith Varatharajan
    Ranjith Varatharajan over 4 years
    mmm. i tried this updated code but getting a new error Uncaught TypeError: Invalid event target thrown
  • Valeriy Katkov
    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
    Neurotransmitter about 4 years
    That is important, since you can't define, for example, callFake on spyOnProperty. returnValue is a way to go.
  • taleb
    taleb over 3 years
    awesome, 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
    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
    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 error fromEvent 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
    Rob Lyndon over 2 years
    I 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
    Pawel Miatkowski over 2 years
    Sorry 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.