60

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

Rob
  • 14,746
  • 28
  • 47
  • 65
Daria Domagała
  • 703
  • 1
  • 6
  • 7

10 Answers10

49

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

Yuri
  • 4,254
  • 1
  • 29
  • 46
Nicolas DINQUEL
  • 496
  • 3
  • 6
  • 4
    This doesn't work because the OP is using `paramMap` and not `params`. – Splaktar Sep 29 '17 at 21:53
  • 13
    Instead of importing observable from rxjs/Rx, I used `of([{id: 1}])`. (`import { of } from 'rxjs';`) – ANeves Jan 02 '19 at 18:31
  • 1
    Maybe someone will find this useful. I needed to easily set the activatedRoute paramsMap and URL between tests. https://gist.github.com/rossholdway/89a50d466d55bbed8d402c2d81f44741 provides an easy way to do this by calling `route.set('categories/:slug', {slug: 'featured'});`. Params are optional. – Ross Mar 15 '19 at 14:37
  • 7
    For `paramMap`: `useValue: { paramMap: of(convertToParamMap({id: 1})) },` – Bart Verkoeijen Dec 17 '19 at 05:59
  • Adding to what Ross says above. You can get the activated route mock/stub and add the the params object and replay subject to solve your problem (just follow the pattern for paramMap. Alternatively, download the finished one from here https://github.com/adrobson/activatedroutestub. Then just before you create the component in your test, call mockActivatedRoute.set and pass in your params for the route. – Frank Cannon Apr 07 '20 at 11:13
  • For snapshot.paramMap reference this answer https://stackoverflow.com/a/49921090/7660196 – Brendan Sluke Mar 25 '21 at 21:43
40

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.

Andrius Naruševičius
  • 8,348
  • 7
  • 49
  • 78
  • 3
    Argument of type 'ActivatedRouteMock' is not assignable to parameter of type 'ActivatedRoute'. Type 'ActivatedRouteMock' is missing the following properties from type 'ActivatedRoute': url, params, queryParams, fragment, and 11 more – lohiarahul Apr 08 '19 at 00:41
  • @lohiarahul There must be some kind of type check forcing on your end. – Andrius Naruševičius Apr 08 '19 at 09:53
  • how does one set the desired paramID in the specific test? like lets say one test needs testId and the other needs anotherId? @AndriusNaruševičius – Fiehra Jan 14 '21 at 16:24
18

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' })
        }
    }
  ]
});
tschumann
  • 2,776
  • 3
  • 26
  • 42
Flaid
  • 181
  • 1
  • 4
17

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

schuno
  • 545
  • 6
  • 11
  • 1
    Why was `Observable` import left then? – Eugene Feb 05 '19 at 14:28
  • my mistake, removed it . . . – schuno Feb 06 '19 at 15:19
  • 2
    Argument of type 'ActivatedRouteMock' is not assignable to parameter of type 'ActivatedRoute'. Type 'ActivatedRouteMock' is missing the following properties from type 'ActivatedRoute': url, params, queryParams, fragment, and 11 more – lohiarahul Apr 08 '19 at 00:41
8

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

DvaJi
  • 81
  • 2
  • 3
6

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 ...
  }));
});
simonepachera
  • 79
  • 2
  • 6
4

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.

Stephen G Tuggy
  • 991
  • 10
  • 16
4

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'});
Ferdinand
  • 79
  • 5
1

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.

Elias Fyksen
  • 879
  • 9
  • 12
  • I really like this approach as you can have separate tests in the same spec with different params. I had to tweak the above from queryMap to paramMap for my scenario but otherwise it worked great. – Ben Thomson May 25 '23 at 00:58
1

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' })
        }
      ]
    });
Hrvoje Matic
  • 1,207
  • 15
  • 12