How do you mock ActivatedRoute
Asked Answered
P

12

66

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

Philous answered 28/8, 2017 at 11:26 Comment(0)
M
55

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

Mouthy answered 29/8, 2017 at 12:27 Comment(6)
This doesn't work because the OP is using paramMap and not params.Springer
Instead of importing observable from rxjs/Rx, I used of([{id: 1}]). (import { of } from 'rxjs';)Groundless
Maybe someone will find this useful. I needed to easily set the activatedRoute paramsMap and URL between tests. gist.github.com/rossholdway/89a50d466d55bbed8d402c2d81f44741 provides an easy way to do this by calling route.set('categories/:slug', {slug: 'featured'});. Params are optional.Northamptonshire
For paramMap: useValue: { paramMap: of(convertToParamMap({id: 1})) },Yajairayajurveda
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 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.Rosenthal
For snapshot.paramMap reference this answer https://mcmap.net/q/297371/-angular-mock-activatedroute-using-snapshot-and-parammapCacodyl
B
44

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.

Bern answered 20/2, 2018 at 16:34 Comment(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 moreBusboy
@lohiarahul There must be some kind of type check forcing on your end.Marnie
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čiusElectromagnetism
H
20

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

Hookworm answered 27/11, 2018 at 16:25 Comment(3)
Why was Observable import left then?Olivier
my mistake, removed it . . .Hookworm
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 moreBusboy
D
19

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' })
        }
    }
  ]
});
Demolish answered 14/11, 2017 at 22:18 Comment(0)
L
9

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

Langouste answered 26/6, 2019 at 20:54 Comment(2)
This enabled me to test several different params. ThanksCodd
This is great, helped me with my tests. You can add a couple of lines to support paramMap property as well. Add this to your imports import { convertToParamMap } from '@angular/router'; And to your main class add: public paramMap = this.subject.asObservable().pipe(map(params => convertToParamMap(params)));Typewritten
P
7

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 ...
  }));
});
Pastrami answered 8/10, 2020 at 15:41 Comment(0)
M
5

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.

Maytime answered 14/8, 2020 at 14:28 Comment(0)
M
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'});
Megaphone answered 5/2, 2021 at 16:49 Comment(0)
E
3

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' })
        }
      ]
    });
Elouise answered 31/3, 2023 at 12:6 Comment(1)
thanks! worked great for me for mocking activatedRoute for storybook story!Departmentalize
T
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.

Theona answered 3/8, 2022 at 10:26 Comment(1)
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.Showoff
C
0

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.

Cabriole answered 1/11, 2023 at 22:38 Comment(0)
D
0

For me it worked like this providers: [ { provide: ActivatedRoute, useValue: { params: of({id: "nhdvugufyheiuhio"}) } } ]

Dowdy answered 14/4 at 12:15 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.