13

I have to test a function that uses the fromEvent observable function. Before the upgrade to 'lettable' operators, I was just doing this:

spyOn(Observable, 'fromEvent').and.callFake(mockFromEventFunction)

But now, Rxjs have changed, and Observable.fromEvent is just a function named fromEvent, that is imported like this: (and used the same way)

import { fromEvent } from 'rxjs/observable/fromEvent';

My question is, how I can mock that function with Jasmine spy utilities without knowing it's parent context?

I advise that this doesn't work:

import * as FromEventContext from 'rxjs/observable/fromEvent';
...
spyOn(FromEventContext , 'fromEvent').and.callFake(mockFromEventFunction)

Now I have a workaround wrapping that fromEvent in one Object which I know the context. But I am wondering how I can solve this cleanly.

Thanks in advance.

4 Answers4

3

After some investigation I discovered that the fact that we can or cannot mock this single exported functions is directly dependen in how our bundler resolves the modules when testing.

So for example, you may stumble to this error or similarly:

Error: : myFunctionName is not declared writable or has no setter

Caused because the bundler just wrapped those lonely exported functions into a getter property, making them impossible to mock.

The solution that I ended using is compile modules in 'commonjs' when testing.

For example, if you are working with typescript, you would need to change change your tsconfig.spec.ts to use commonjs module:

"compilerOptions": {
     ....
      // force commonjs module output, since it let mock exported members on modules to anywhere in the application (even in the same file)
      "module": "commonjs",
  },

The resultant output of any exported member of a module in commonjs would be like: exports.myFunc = function() {}. This led use spyOn without worries since it is wrapped on the 'exports' object. One great use case of that, is that it would be mocked anywhere, including the usages in its own file!

Example:

// some-module.js
export function functionToMock() {
     return 'myFuncToMock';
}
export function functionToTest() {
     return functionToMock();
}

// testing-module.spec.js
import * as SomeModule from ./some-module
spyOn(SomeModule, 'functionToMock').and.returnValue('mockedCorrectly');
SomeModule.functionToTest().toBe('mockedCorrectly')
1

You are right. FromEventContext does not work for that.

Instead you could use jasmine-auto-spies, that would make it easier to handle such things.

import { Spy, createSpyFromClass } from 'jasmine-auto-spies';

In your test-code you can now create spies on Observables:

fakeObservable = createSpyFromClass(Observable);
fakeObservable.fromEvent.and.returnValue(Observable.empty()) // or whatever you want to return

Maybe this helps?!

  • 1
    Hi André, in my scenario, there isn't the Observable class. fromEvent is just a function, it doesn't come from the Observable class like Observable.fromEvent... so this solution will not work. If I am not wrong, this is the same as spyOn. – Llorenç Pujol Ferriol Aug 22 '18 at 08:13
1

I also stumbled upon this problem of testing my component in jasmine which had fromEvent rxjs operator. My component looked something like this:

@ViewChild('searchBar') searchBarRef: ElementRef;
@Input() courses$: any[];
filteredCourses$: any[];

ngAfterViewInit(): void {
    this.filteredCourses$ = fromEvent(this.searchBarRef.nativeElement, 'keyup').pipe(
          map((event: any) => event.srcElement.value),
          startWith(''),
          distinctUntilChanged(),
          switchMap((searchText) =>
            this.courses$.pipe(
              map((courses) =>
                courses.filter(
                  (course) =>
                     course.code.toLowerCase().includes(searchText.toLowerCase()) ||
                     course.name.toLowerCase().includes(searchText.toLowerCase())
                )
              )
            )
          ),
          shareReplay(1)
        );
}

To test ngAfterViewInit, I have create a mock input element and assigned it to the nativeElement:

it('ngAfterViewInit():', () => {
    const mockInputElement = document.createElement('input');
    component.searchBarRef = {
        nativeElement: mockInputElement
    };
    const mockCourses = of([
      {
        code: 'PHY',
        name: 'Physics',
      }, 
      { 
        code: 'MAT',
        name: 'Mathematics',
      }, 
      {
        code: 'BIO',
        name: 'Biology',
      }
    ]);
    component.courses$ = mockCourses;
    component.ngAfterViewInit();
    component.filteredCourses$.pipe(take(1)).subscribe((res) => {
        expect(res).toEqual(mockCourses);
    });
    mockInputElement.value = 'mat' // pass the input to test
    mockInputElement.dispatchEvent(new Event('keyup'));
    component.filteredCourses$.pipe(take(1)).subscribe((res) => {
        expect(res).toEqual([{
          code: 'MAT',
          name: 'Mathematics',
        }]);
    });
});

You can use this logic to test your component.

suyashpatil
  • 141
  • 1
  • 3
0

Don't know if its your case, but if you spyOnProperty on fromEvent it may work.

Like this:

spyOnProperty(rxjs, 'fromEvent').and.returnValue(() => rxjs.of({}));

In my case I import the entire rxjs library

import * as rxjs from 'rxjs';

Hope it helps, Cheers!

Alejandro Barone
  • 1,743
  • 2
  • 13
  • 24