How do you mock ActivatedRoute

90.5k views Asked by At

I'm learning Angular and I want to do tests, but I'm stuck. I've got a function:

ngOnInit(): void {
    this.route.paramMap
    .switchMap((params: ParamMap) => 
        this.SomethingService.getSomething(params.get('id')))
        .subscribe(something => {
            this.something = something;
            this.doSomethingElse();
    });
}

where

route: ActivatedRoute

and I want to test it but I don't know how to mock ActivatedRoute

11

There are 11 answers

6
Nicolas DINQUEL On

A simple way to mock ActivatedRoute is this one:

    TestBed.configureTestingModule({
      declarations: [YourComponenToTest],
      providers: [
        {
          provide: ActivatedRoute,
          useValue: {
            params: Observable.from([{id: 1}]),
          },
        },
      ]
    });

Then in your test it will be available and your function should work with this (at least the ActivatedRoute part)

You can get it with TestBed.get(ActivatedRoute) in your it functions if you want to stock it in a variable.

Don't forget to import Observable from rxjs/Rx and not from rxjs/Observable

0
simonepachera On

I had this problem since forever and I find out that this way it works as I want. No need to spy on the get for me for example.

Given:

ngOnInit() {
  this.some = this.activatedRoute.snapshot.paramMap.get('some') === 'some';
  this.else = this.activatedRoute.snapshot.paramMap.get('else');
}

Then:

describe('SomeComponent', () => {
  let component: SomeComponent;
  let fixture: ComponentFixture<SomeComponent>;

  let activatedRouteSpy;

  beforeEach(async(() => {
    activatedRouteSpy = {
      snapshot: {
        paramMap: convertToParamMap({
          some: 'some',
          else: 'else',
        })
      }
    };

    TestBed.configureTestingModule({
      declarations: [ SomeComponent ],
      providers: [
        { provide: ActivatedRoute, useValue: activatedRouteSpy },
      ]
    })
      .compileComponents();
  }));

  beforeEach(() => {
    fixture = TestBed.createComponent(SomeComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });

  it('should create', () => {
    expect(component).toBeTruthy();
  });

  it('should load correct data (original stuff)', fakeAsync(() => {
    component.ngOnInit();
    tick(2000);
    
    // ... do your checks ...
  }));

  it('should load correct data (different stuff)', fakeAsync(() => {
    activatedRouteSpy.snapshot.paramMap = convertToParamMap({
      some: 'some',
      else: null,
    });
    fixture.detectChanges();
    component.ngOnInit();
    tick(2000);

    // ... do your checks ...
  }));
});
0
Javier Guerrero On

I have another way to mock a param or queryParam on ActivatedRoute.

To mock a param: 'http:://localhost/web/param'

beforeEach(async () => {

const params = new BehaviorSubject({ param: 'valueOf' });
const activatedRoute = { params, snapshot: { params } };
await TestBed.configureTestingModule({
  declarations: [Component],
  imports: [RouterTestingModule],
  providers: [
    someproviders...,
    {
      provide: ActivatedRoute, useValue: activatedRoute
    }
  ]
})
  .compileComponents();

});

this allow to change in distinct test on your file spec.ts the value of 'param'. To do this:

const params = new BehaviorSubject({ param: 'newValueOf' });
component['activeRoute'].params = params; //activeRoute is the public property in the constructor that instances ActivatedRoute

In this example, the url with the mock of 'param' is: 'http:://localhost/web/valueOf'

To queryParams: 'http:://localhost/web?queryParam=some'

do this:

beforeEach(async () => {
const queryParams = new BehaviorSubject({ queryParam: 'valueOf' });
const activatedRoute = { queryParams, snapshot: { queryParams: queryParams.value } };
await TestBed.configureTestingModule({
  declarations: [ Component ],
  providers: [
    someproviders...,
    {
      provide: ActivatedRoute, useValue: activatedRoute
    }
  ],
  imports: [RouterTestingModule]
})
.compileComponents();

});

In this case, to set queryParams, you have to add .value.

3
schuno On

Modifying Andrus' answer above . . .

For RxJS 6+:

import { convertToParamMap } from '@angular/router';
import { of } from 'rxjs';


export class ActivatedRouteMock {
    public paramMap = of(convertToParamMap({ 
        testId: 'abc123',
        anotherId: 'd31e8b48-7309-4c83-9884-4142efdf7271',          
    }));
}

https://www.learnrxjs.io/operators/creation/of.html

0
Ferdinand On

I use RxJS Subject instead of Observable for the paramMap. This way I can trigger the paramMap to emit new values during testing.

The ActivatedRoute is mocked directly in the providers-section of Testbed config:

providers: [{ provide: ActivatedRoute, useValue: { paramMap: new Subject() } }]

Then, i get the activated route from the testbed:

activatedRoute = TestBed.inject(ActivatedRoute);

And trigger the paramMap to emit new values whenever required in the test procedure:

activatedRoute.paramMap.next({ get: (key: string) => 'value1'});
1
DvaJi On

In my case, I had to create a new class to handle this type of test, this class will allow you to handle snapshot, queryParams, and params.

import { Params } from '@angular/router';
import { BehaviorSubject } from 'rxjs';

export class MockActivatedRoute {
  private innerTestParams?: any;
  private subject?: BehaviorSubject<any> = new BehaviorSubject(this.testParams);

  params = this.subject.asObservable();
  queryParams = this.subject.asObservable();

  constructor(params?: Params) {
    if (params) {
      this.testParams = params;
    } else {
      this.testParams = {};
    }
  }

  get testParams() {
    return this.innerTestParams;
  }

  set testParams(params: {}) {
    this.innerTestParams = params;
    this.subject.next(params);
  }

  get snapshot() {
    return { params: this.testParams, queryParams: this.testParams };
  }
}

this is how the test should look

import { MockActivatedRoute } from './mock-active-router';

describe('MyComponent', () => {
  let component: MyComponent;
  let fixture: ComponentFixture<MyComponent>;
  let activatedRouteStub: MockActivatedRoute;

  beforeEach(async(() => {
    activatedRouteStub = new MockActivatedRoute();
    TestBed.configureTestingModule({
      declarations: [MyComponent],
      providers: [
        { provide: ActivatedRoute, useValue: activatedRouteStub }
      ]
    }).compileComponents();
  }));

  it('should change params', () => {
    expect(component.myparam).toBeUndefined();
    expect(component.paramTwo).toBeUndefined();

    activatedRouteStub.testParams = {
      myparam: 'value',
      paramTwo: 1
    };
    fixture.detectChanges();

    expect(component.myparam).toEqual('value');
    expect(component.paramTwo).toEqual(1);
  });

https://gist.github.com/dvaJi/cf552bbe6725535955f7a5eeb92d7d2e

1
Elias Fyksen On

I think the easiest way to run tests with a test using ActivatedRoute is to mock it using BehaviorSubjects (https://www.learnrxjs.io/learn-rxjs/subjects/behaviorsubject).

In your test bed setup code:

let activatedRouteMock = {
  queryMap: new BehaviorSubject<ParamMap>(
    convertToParamMap({ paramName: 'default value' })
  )
};

await TestBed.configureTestingModule({
      providers: [
        { provide: ActivatedRoute, useValue: activatedRouteMock }
      ],
      // ..rest of test bed 
    }).compileComponents();

// ...rest of setupcode

Then, test without supplying custom parameter

it('some test', () => {
   // run your test code that expects default parameters
})

Then, test with special parameters

it('some other test', () => {
  activatedRouteMock.queryMap.next(convertToParamMap({ paramName: 'special value' }));

  // ...test that your component does what it's supposed to
});

This can obviously be constructed into a mock class that extends ActivatedRoute, or you can do a similar setup for params or queryParams instead of paramMap.

3
Andrius Naruševičius On

For anyone interested on how to properly do it with multiple properties, this is the way you define your mock class:

import { convertToParamMap } from '@angular/router';
import { Observable } from 'rxjs/Observable';

export class ActivatedRouteMock {
    public paramMap = Observable.of(convertToParamMap({ 
        testId: 'abc123',
        anotherId: 'd31e8b48-7309-4c83-9884-4142efdf7271',          
    }));
}

This way you can subscribe to your paramMap and retrieve multiple values - in this case testId and anotherId.

1
Hrvoje Matic On

Create mock class first:

import { Params } from '@angular/router';
import { Observable, of } from 'rxjs';

export class ActivatedRouteMock {
  snapshot: {
    params: Params;
  };

  paramMap: Observable<Params>;

  constructor(params: Params) {
    const extendedParams = {
      ...params,
      get(paramName: string) {
        return params[paramName];
      }
    };
    this.snapshot = {
      params: extendedParams
    };
    this.paramMap = of(extendedParams);
  }
}

Usage:

    TestBed.configureTestingModule({
      providers: [
        {
          provide: ActivatedRoute,
          useValue: new ActivatedRouteMock({ deliveryId: 'test' })
        }
      ]
    });
0
Flaid On

I was facing the same problem using paramMap instead of params. This got it working for me, at least for the simplest case:

import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { Observable } from 'rxjs';
import 'rxjs/add/observable/of';
import { ComponentToTest } from './component-to-test.component';
import { ActivatedRoute } from '@angular/router';

TestBed.configureTestingModule({
  declarations: [ComponentToTest],
  providers: [
    { 
        provide: ActivatedRoute, 
        useValue: {
            paramMap: Observable.of({ get: (key) => 'value' })
        }
    }
  ]
});
0
Stephen G Tuggy On

In recent Angular versions, a project's aot setting will be on by default (for better compile-time type checking). If this is the case with your project, then you probably need to at least stub out all the properties of ActivatedRoute and ActivatedRouteSnapshot. Something like this:

import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { Type } from '@angular/core';
import { Location } from '@angular/common';
import { MockPlatformLocation } from '@angular/common/testing';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { ActivatedRoute, ActivatedRouteSnapshot, Params, ParamMap, convertToParamMap } from '@angular/router';
import { of, BehaviorSubject } from 'rxjs';

import { HeroDetailComponent } from './hero-detail.component';
import { Hero } from '../hero';


export class MockActivatedRouteSnapshot implements ActivatedRouteSnapshot {
  private innerTestParams?: Params;

  constructor(params?: Params) {
    if (params) {
      this.testParams = params;
    } else {
      this.testParams = null;
    }
  }

  private get testParams() {
    return this.innerTestParams;
  }

  private set testParams(params: Params) {
    this.innerTestParams = params;
  }

  get paramMap() {
    return convertToParamMap(this.testParams);
  }

  get queryParamMap() {
    return this.paramMap;
  }

  get url() {
    return null;
  }

  get fragment() {
    return null;
  }

  get data() {
    return null;
  }

  get outlet() {
    return null;
  }

  get params() {
    return this.innerTestParams;
  }

  get queryParams() {
    return this.innerTestParams;
  }

  get component() {
    return null;
  }

  get routeConfig() {
    return null;
  }

  get root() {
    return null;
  }

  get parent() {
    return null;
  }

  get firstChild() {
    return null;
  }

  get children() {
    return null;
  }

  get pathFromRoot() {
    return null;
  }
}


export class MockActivatedRoute implements ActivatedRoute {
  private innerTestParams?: Params;
  private subject?: BehaviorSubject<Params> = new BehaviorSubject(this.testParams);
  private paramMapSubject?: BehaviorSubject<ParamMap> = new BehaviorSubject(convertToParamMap(this.testParams));

  constructor(params?: Params) {
    if (params) {
      this.testParams = params;
    } else {
      this.testParams = null;
    }
  }

  private get testParams() {
    return this.innerTestParams;
  }

  private set testParams(params: Params) {
    this.innerTestParams = params;
    this.subject.next(params);
    this.paramMapSubject.next(convertToParamMap(params));
  }

  get snapshot() {
    return new MockActivatedRouteSnapshot(this.testParams);
  }

  get params() {
    return this.subject.asObservable();
  }

  get queryParams() {
    return this.params;
  }

  get paramMap() {
    return this.paramMapSubject.asObservable();
  }

  get queryParamMap() {
    return this.paramMap;
  }

  get url() {
    return null;
  }

  get fragment() {
    return null;
  }

  get data() {
    return null;
  }

  get outlet() {
    return null;
  }

  get component() {
    return null;
  }

  get routeConfig() {
    return null;
  }

  get root() {
    return null;
  }

  get parent() {
    return null;
  }

  get firstChild() {
    return null;
  }

  get children() {
    return null;
  }

  get pathFromRoot() {
    return null;
  }
}


describe('HeroDetailComponent', () => {
  let component: HeroDetailComponent;
  let fixture: ComponentFixture<HeroDetailComponent>;
  let httpMock: HttpTestingController;
  let routeMock: MockActivatedRoute;
  let initialMockParams: Params;
  let locationMock: MockPlatformLocation;

  beforeEach(async(() => {
    initialMockParams = {id: 11};
    routeMock = new MockActivatedRoute(initialMockParams);
    locationMock = new MockPlatformLocation;
    TestBed.configureTestingModule({
      imports: [
        HttpClientTestingModule
      ],
      declarations: [ HeroDetailComponent ],
      providers: [
        {
          provide: ActivatedRoute, useValue: routeMock,
        },
        {
          provide: Location, useValue: locationMock,
        }
      ]
    })
    .compileComponents();
  }));

  beforeEach(() => {
    fixture = TestBed.createComponent(HeroDetailComponent);
    component = fixture.componentInstance;
    httpMock = TestBed.inject<HttpTestingController>(HttpTestingController as Type<HttpTestingController>);
    fixture.detectChanges();
  });

  afterEach(() => {
    httpMock.verify();
  });

  it('should be created', () => {
    fixture.detectChanges();

    expect(component).toBeTruthy();

    const dummyHero: Hero = { id: 11, name: 'dummyHero' };
    const req = httpMock.expectOne('api/details/11');
    req.flush(dummyHero);
  });
});

See also this answer.