Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Jasmine Unit test - Cannot read property pipe of undefined

I am using Angular 6, NgRx 6, RxJS 6.

I have a route guard that looked like this -

import { CanActivate, ActivatedRouteSnapshot } from '@angular/router';
import { Injectable } from '@angular/core';
import { Observable, of } from 'rxjs';
import { IAppState } from '../../../app.state';
import { Store } from '@ngrx/store';

import { SetTenant } from './../../../store/config/config.actions';

@Injectable()
export default class TenantGuard implements CanActivate {
  constructor(private store: Store<IAppState>) {}

  canActivate(route: ActivatedRouteSnapshot): Observable<boolean> {
    const tenant = route.params['tenant'];

    if (!tenant) {
      return of(false);
    }

    this.store.dispatch(new SetTenant(tenant));
    return of(true);
  }
}

As you can see, I was adding my tenant to the store via this.store.dispatch(new SetTenant(tenant));

This was however causing that action to fire every time a user visited the base route.

In order to combat this, I have added a check to see if the tenant is populated and only fire the action if not -

import { CanActivate, ActivatedRouteSnapshot } from '@angular/router';
import { Injectable } from '@angular/core';
import { Observable, of, combineLatest } from 'rxjs';
import { IAppState } from '../../../app.state';
import { Store, select } from '@ngrx/store';

import { SetTenant } from './../../../store/config/config.actions';
import { getTenant } from '../../../store/config/config.selectors';
import { map } from 'rxjs/operators';

@Injectable()
export default class TenantGuard implements CanActivate {
  constructor(private store: Store<IAppState>) {}

  canActivate(route: ActivatedRouteSnapshot): Observable<boolean> {
    const tenantFromRoute: string = route.params['tenant'];

    return this.store.pipe(select(getTenant)).pipe(
      map(tenantFromStore => {
        if (!tenantFromRoute) {
          return false;
        }

        if (!tenantFromStore) {
          this.store.dispatch(new SetTenant(tenantFromRoute));
        }
        return true;
      })
    );
  }
}

This has however broken my unit tests as I have introduced additional logic and I am now getting the error TypeError: Cannot read property 'pipe' of undefined

My spec file looks like this -

import { TestBed, async } from '@angular/core/testing';
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { Store } from '@ngrx/store';
import { StoreModule } from '@ngrx/store';

import { SetTenant } from './../../../store/config/config.actions';

import TenantGuard from './tenant.guard';

describe('TenantGuard', () => {
  it('should return false if a tenant is not present on the route', async(() => {
    const { tenantGuard, props } = setup({});
    let result: boolean;
    tenantGuard.canActivate(props).subscribe(canActivate => (result = canActivate));
    expect(result).toBeFalsy();
  }));

  it('should return true if a tenant is present on the route', async(() => {
    const { tenantGuard, props } = setup({ tenant: 'main' });
    let result: boolean;
    tenantGuard.canActivate(props).subscribe(canActivate => (result = canActivate));
    expect(result).toBeTruthy();
  }));

  it('should dispatch an action to set the tenant in the store', () => {
    const { store, tenantGuard, props } = setup({ tenant: 'foo' });
    const action = new SetTenant('foo');
    tenantGuard.canActivate(props);

    expect(store.dispatch).toHaveBeenCalledWith(action);
  });

  it('should not dispatch an action to set the tenant in the store if the tenant is missing', () => {
    const { store, tenantGuard, props } = setup({});
    tenantGuard.canActivate(props);

    expect(store.dispatch).not.toHaveBeenCalled();
  });

  const setup = propOverrides => {
    TestBed.configureTestingModule({
      imports: [StoreModule.forRoot({})],
      providers: [
        TenantGuard,
        {
          provide: Store,
          useValue: jasmine.createSpyObj('Store', ['dispatch', 'pipe']),
        },
      ],
      schemas: [CUSTOM_ELEMENTS_SCHEMA],
    }).compileComponents();

    const props = Object.assign({ params: { tenant: null } }, { params: { ...propOverrides } });

    const tenantGuard = TestBed.get(TenantGuard);
    const store = TestBed.get(Store);

    return { tenantGuard, props, store };
  };
});

I have added pipe to my jasmine.createSpyObj however I am unsure how to progress.

I would like to write additional tests around this, but am having trouble mocking out how pipe should / would be used in this case.

Edit - If I do not pass pipe in to my jasmine.createSpyObj I instead get the error ​​TypeError: this.store.pipe is not a function​​

like image 444
Harry Blue Avatar asked Jun 13 '18 15:06

Harry Blue


1 Answers

I had the same error message like you. I injected the router in a component and used the this.router.events.pipe(...)... I use a stub for the router in my test. Before the routerStub looked like this:

routerStub = {
        navigate: (commands: any[]) => { Promise.resolve(true); },
};

So you can see that before I need the navigate method of the router in my component therefore I defined it in the stub. Now I need the events property as well which returns an observable where than .pipe is used. I added following to the routerStub which fixed it for me:

routerStub = {
        navigate: (commands: any[]) => { Promise.resolve(true); },
        events: of(new Scroll(new NavigationEnd(0, 'dummyUrl', 'dummyUrl'), [0, 0], 'dummyString'))
};

In my case I need a Scroll Event for my code to work, but now events is defined in my stub as an Observable and now pipe is known.

Maybe this helps you...

like image 174
Gerros Avatar answered Sep 30 '22 07:09

Gerros