30

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();
});
Raju
  • 319
  • 1
  • 3
  • 7
  • 1
    Probably not the answer you are looking for, but it is a [known issue](https://github.com/jasmine/jasmine/issues/1414) in Jasmine. There are a few workarounds and ideas in the thread that might do the trick for you. – Morphyish Jul 30 '19 at 09:31
  • 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 Jul 30 '19 at 17:45
  • 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 – Omair Nabiel Aug 02 '19 at 10:42

5 Answers5

10

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

Omair Nabiel
  • 1,662
  • 2
  • 12
  • 24
  • 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 Aug 05 '19 at 07:18
  • 1
    rxjs@6.5.2, jasmine-core@3.3.0 and I've got the above error. May be you're using `commonjs` modules, as described in my [answer](https://stackoverflow.com/a/57270236/4858777)? – Valeriy Katkov Aug 05 '19 at 08:30
  • 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. – Valeriy Katkov Aug 05 '19 at 08:45
  • 1
    Yup I'm using common.js – Omair Nabiel Aug 05 '19 at 09:12
  • Tried this getting `TypeError: Object(...) is not a function` – Ranjith Varatharajan Aug 06 '19 at 15:36
  • 1
    @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 https://stackoverflow.com/a/58768076/3070228 – taleb Jan 19 '21 at 20:36
  • @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](https://stackoverflow.com/a/57270236/4858777) as well as some relevant links, hope it will help someone. – Valeriy Katkov Jan 22 '21 at 08:57
8

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');
});
Valeriy Katkov
  • 33,616
  • 20
  • 100
  • 123
5

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!

Alejandro Barone
  • 1,743
  • 2
  • 13
  • 24
  • That is important, since you can't define, for example, `callFake` on `spyOnProperty`. `returnValue` is a way to go. – Neurotransmitter Apr 08 '20 at 15:00
  • 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 Jan 19 '21 at 20:33
3

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');
    
       ...
    })
danwellman
  • 9,068
  • 8
  • 60
  • 88
  • 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.. – Rob Lyndon Sep 06 '21 at 20:52
  • 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. – Pawel Miatkowski Sep 08 '21 at 06:48
1

The consensus seems to be that it is not possible or desirable to spy directly on a function taken from a module; even if you manage to find a way to make it work, the solution will be brittle, and a system update may break the workaround.

Some combination of a wrapper class and DI is necessary, but how can that be done cleanly? It is not ideal to introduce a wrapper class just in order to make the system testable, but it can be done without too much extra boilerplate.

My example is taken from the (newly released at the time of writing) Firebase 9 SDK. The untested code would look like this:

import { Firestore, query, collection, where, collectionData } from '@angular/fire/firestore';

@Injectable({
  providedIn: 'root'
})
export class AccountService {
  constructor(private firestore: Firestore) { }

  public getAccountFromUid(uid: string): Observable<Account[]> {
    const accountsQuery = query(
      collection(this.firestore, 'accounts'), 
      where('firebaseUid', '==', uid)
    );
    return collectionData(accountsQuery);
  }}
}

Unfortunately, this is untestable, because query, collection, where and collectionData are functions imported directly from a module, and the following code does not work, even when using variants involving spyOnProperty:

import * as firestore from '@angular/fire/firestore`

spyOn(firestore, 'collectionData').and.returnValue(of(...));

To work around this, I introduce a service called FirestoreService. It is a fairly straightforward entity:

import { Injectable } from '@angular/core';
import * as firestore from '@angular/fire/firestore';

@Injectable({
  providedIn: 'root'
})
export class FirestoreService {

  constructor() { }

  public get collectionData() { return firestore.collectionData; }
  public get query() { return firestore.query; }
  public get collection() { return firestore.collection; }
  public get where() { return firestore.where; }
}

Then I can inject this into my original class and modify it as follows:

import { Firestore } from '@angular/fire/firestore';
import { FirstoreService } from './services/firestore.service';

@Injectable({
  providedIn: 'root'
})
export class AccountService {
  constructor(
    private firestore: Firestore,
    private firestoreService: FirestoreService,
  ) { }

  public getAccountFromUid(uid: string): Observable<Account[]> {
    const { query, collection, where, collectionData } = this.firestoreServioce; 
    const accountsQuery = query(
      collection(this.firestore, 'accounts'), 
      where('firebaseUid', '==', uid)
    );
    return collectionData(accountsQuery);
  }}
}

And then the properties of firestoreService behave like functions that can be spied on as normal:

import { of } from 'rxjs';
import * as firestore from '@angular/fire/firestore';
import { FirestoreService } from './services/firestore.service';

describe('AccountService', () => {
  let service: AccountService;
  let firestoreSpy: jasmine.SpyObj<firestore.Firestore>;
  let firestoreServiceSpy: jasmine.SpyObj<FirestoreService>

  beforeEach(() => {
    firestoreSpy = jasmine.createSpyObj('firestore.Firestore', ['app']);
    firestoreServiceSpy = jasmine.createSpyObj('FirestoreService',
      ['query', 'collectionData', 'collection', 'where']);
    firestoreServiceSpy.collectionData.and.returnValue(of(...));
  });
});
Rob Lyndon
  • 12,089
  • 5
  • 49
  • 74