How to unit test a component that depends on parameters from ActivatedRoute?

92,911

Solution 1

The simplest way to do this is to just use the useValue attribute and provide an Observable of the value you want to mock.

RxJS < 6

import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/of';
...
{
  provide: ActivatedRoute,
  useValue: {
    params: Observable.of({id: 123})
  }
}

RxJS >= 6

import { of } from 'rxjs';
...
{
  provide: ActivatedRoute,
  useValue: {
    params: of({id: 123})
  }
}

Solution 2

In angular 8+ there is the RouterTestingModule, which you can use in order to have access to the ActivatedRoute or Router of the component. Also you can pass routes to the RouterTestingModule and create spies for the requested methods of route.

For example in my component I have:

ngOnInit() {
    if (this.route.snapshot.paramMap.get('id')) this.editMode()
    this.titleService.setTitle(`${this.pageTitle} | ${TAB_SUFFIX}`)
}

And in my test I have:

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [ ProductLinePageComponent ],
      schemas: [NO_ERRORS_SCHEMA],
      imports: [
        RouterTestingModule.withRoutes([])
      ],
    })
    .compileComponents()
  }))

  beforeEach(() => {
    router = TestBed.get(Router)
    route = TestBed.get(ActivatedRoute)
  })

and later in the 'it' section:

  it('should update', () => {
    const spyRoute = spyOn(route.snapshot.paramMap, 'get')
    spyRoute.and.returnValue('21')
    fixture = TestBed.createComponent(ProductLinePageComponent)
    component = fixture.componentInstance
    fixture.detectChanges()
    expect(component).toBeTruthy()
    expect(component.pageTitle).toBe('Edit Product Line')
    expect(component.formTitle).toBe('Edit Product Line')
    // here you can test the functionality which is triggered by the snapshot
  })

In a similar way, I think you can test directly the paramMap via the spyOnProperty method of jasmine, by returning an observable or using rxjs marbles. It might save some time & also it does not require to maintain an extra mock class. Hope that it is useful and it makes sense.

Solution 3

I have figured out how to do this!

Since ActivatedRoute is a service, a mock service for it can be established. Let's call this mock service MockActivatedRoute. We will extend ActivatedRoute in MockActivatedRoute, as follows:

class MockActivatedRoute extends ActivatedRoute {
    constructor() {
        super(null, null, null, null, null);
        this.params = Observable.of({id: "5"});
    }

The line super(null, ....) initializes the super class, which has four mandatory parameters. However, in this instance, we need nothing from any of these parameters, so we initialize them to null values. All we need is the value of params which is an Observable<>. Therefore, with this.params, we override the value of params and initialize it to be the Observable<> of the parameter on which the test subject is relying.

Then, as any other mock service, just initialize it and override the provider for the component.

Good luck!

Solution 4

Here is how I tested it in angular 2.0 latest...

import { ActivatedRoute, Data } from '@angular/router';

and in Providers section

{
  provide: ActivatedRoute,
  useValue: {
    data: {
      subscribe: (fn: (value: Data) => void) => fn({
        yourData: 'yolo'
      })
    }
  }
}

Solution 5

Just add a mock of the ActivatedRoute:

providers: [
  { provide: ActivatedRoute, useClass: MockActivatedRoute }
]

...

class MockActivatedRoute {
  // here you can add your mock objects, like snapshot or parent or whatever
  // example:
  parent = {
    snapshot: {data: {title: 'myTitle ' } },
    routeConfig: { children: { filter: () => {} } }
  };
}
Share:
92,911

Related videos on Youtube

Colby Cox
Author by

Colby Cox

Updated on July 08, 2022

Comments

  • Colby Cox
    Colby Cox almost 2 years

    I am unit testing a component that is used to edit an object. The object has an unique id that is used in order to grab the specific object from an array of objects that are hosted in a service. The specific idis procured through a parameter that is passed via routing, specifically through the ActivatedRoute class.

    The constructor is as follows:

    constructor(private _router:Router, private _curRoute:ActivatedRoute, private _session:Session) {}
        
    ngOnInit() {
      this._curRoute.params.subscribe(params => {
        this.userId = params['id'];
        this.userObj = this._session.allUsers.filter(user => user.id.toString() === this.userId.toString())[0];
    

    I want to run basic unit tests on this component. However, I am not sure as to how I can inject the id parameter, and the component needs this parameter.

    By the way: I already have a mock for the Session service, so no worries there.

  • Aarmora
    Aarmora almost 8 years
    I am facing this right now! However, I'm getting errors when I try to use super or Observable. Where do these come from?
  • Michael JDI
    Michael JDI over 7 years
    Can you provide the complete code for the providers section?
  • Rady
    Rady over 7 years
    This is a complete unit test class. plnkr.co/edit/UeCKnJ2sCCpLLQcWqEGX?p=catalogue
  • Alejandro Sanz Díaz
    Alejandro Sanz Díaz over 7 years
    Observable.of does not exists for me! :S
  • zmanc
    zmanc over 7 years
    Import Observable from rxjs/Observable
  • shiva
    shiva over 7 years
    How do you test unsubscribe in ngOnDestroy
  • Quovadisqc
    Quovadisqc about 7 years
    This will break in a real life usecase because you aren't returning a subscription and you won't be able to use call .unsubscribe() in ngOnDestroy.
  • Quovadisqc
    Quovadisqc about 7 years
    data: Observable.of({yourData: 'yolo'}) would work though.
  • oooyaya
    oooyaya about 7 years
    super() is built in. Observable is from rxjs/Observable or just rxjs depending on your version. You'd get it using import {Observable} from 'rxjs'.
  • mixalbl4
    mixalbl4 over 6 years
    This code makes this error in my project: Uncaught NetworkError: Failed to execute 'send' on 'XMLHttpRequest': Failed to load 'ng:///DynamicTestModule/HomeContentComponent.ngfactory.js'. at http://localhost:9876/_karma_webpack_/polyfills.bundle.js:26‌​05
  • a3uge
    a3uge over 6 years
    @MixerOID this is chrome eating bugs - put the test in a try-catch and you should see the error.
  • adamdabb
    adamdabb over 6 years
    I know its 2 years later... but if you're getting the Uncaught NetworkError error that MixerOID posted, make sure you're using the right type of params. My issue was that my code was expecting queryParams, not params.
  • danwellman
    danwellman about 6 years
    To avoid the generic Uncaught NetworkError..., you can also disable sourcemaps by running tests with ng test --sourcemaps=false - then you should see the real error causing the issue
  • Michael Dausmann
    Michael Dausmann about 6 years
    { provide: ActivatedRoute, useValue: { snapshot: { params: Observable.of({userName: 'testUser', emailValidationToken: 'token'}) } } }
  • ThetaSinner
    ThetaSinner almost 6 years
    I agree with @Quovadisqc that you should NOT be mocking classes outside your program. This is a good solution when you replace the subscribe stuff with an Observable.of
  • Ben Racicot
    Ben Racicot over 5 years
    RxJs 6 of should be used alone. Also you'd likely use RouterTestingModule instead of this answer's code.
  • zmanc
    zmanc over 5 years
    @BenRacicot this answer was given before RxJs 6 existed. Also instead saying "do this instead" provide an answer that can be upvoted directly.
  • ruffin
    ruffin over 4 years
    You've accepted one answer and posted another... if this was Highlander (and there could only be one), which one did you "really" pick and why? That is, I think this essentially reduces to the same thing as zmanc's answer, which you accepted. Did you find additional value from setting up this [slightly] more complicated mock?
  • crackmigg
    crackmigg over 4 years
    So much better than having to maintain an extra mock and you can easily set different parameters in tests. Thank you!
  • speksy
    speksy about 4 years
    This helps. Do you know how to spy on different params: const dirName = this.route.snapshot.paramMap.get('dirName'); const actionType = this.route.snapshot.paramMap.get('actionType'); On which of bot will spy spyOn(route.snapshot.paramMap, 'get') ? Can I specify key to listen ?
  • dimitris maf
    dimitris maf about 4 years
    As I mention above, I think you could use spyOnProperty instead of spyOn, for example spyOnProperty(route.snapshot.paramMap.get, 'dirName'). If I haven't answered your question completely, don't hesitate to tell me. Thanks.
  • rosiejaneenomar
    rosiejaneenomar over 2 years
    it's working, i just dont know why it is not working when i only put activatedRoute.snapshot and fixture.detect.. in the it . but when i copied your code its working
  • rosiejaneenomar
    rosiejaneenomar over 2 years
    do you have any idea, why do i have to initialize again the fixture and component in it block even if it has already in the beforeeach?
  • Ahmed Shehatah
    Ahmed Shehatah over 2 years
    Thank you , this was informative and helpful ☺
  • walkeros
    walkeros almost 2 years
    This works with Angular 6