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

Unit TestingAngularAngular2 RoutingAngular2 Testing

Unit Testing Problem Overview


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.

Unit Testing Solutions


Solution 1 - Unit Testing

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 - Unit Testing

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 - Unit Testing

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 - Unit Testing

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 - Unit Testing

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: () => {} } }
  };
}

Solution 6 - Unit Testing

For some folks working on Angular > 5, if Observable.of(); is not working then they can use just of() by importing import { of } from 'rxjs';

Solution 7 - Unit Testing

Ran into the same issue while creating test suites for a routing path as:

{
   path: 'edit/:property/:someId',
   component: YourComponent,
   resolve: {
       yourResolvedValue: YourResolver
   }
}

In the component, I initialized the passed property as:

ngOnInit(): void {    
   this.property = this.activatedRoute.snapshot.params.property;
   ...
}

When running the tests, if you do not pass a property value in your mock ActivatedRoute "useValue", then you will get undefined when detecting changes using "fixture.detectChanges()". This is because the mock values for ActivatedRoute does not contain the property params.property. Then, it is required for the mock useValue to have those params in order for the fixture to initialize the 'this.property' in the component. You can add it as:

  let fixture: ComponentFixture<YourComponent>;
  let component: YourComponent;
  let activatedRoute: ActivatedRoute; 
 
  beforeEach(done => {
        TestBed.configureTestingModule({
          declarations: [YourComponent],
          imports: [ YourImportedModules ],
          providers: [
            YourRequiredServices,
            {
              provide: ActivatedRoute,
              useValue: {
                snapshot: {
                  params: {
                    property: 'yourProperty',
                    someId: someId
                  },
                  data: {
                    yourResolvedValue: { data: mockResolvedData() }
                  }
                }
              }
            }
          ]
        })
          .compileComponents()
          .then(() => {
            fixture = TestBed.createComponent(YourComponent);
            component = fixture.debugElement.componentInstance;
            activatedRoute = TestBed.get(ActivatedRoute);
            fixture.detectChanges();
            done();
          });
      });

The you can start testing as for example:

it('should ensure property param is yourProperty', async () => {
   expect(activatedRoute.snapshot.params.property).toEqual('yourProperty');
   ....
});

Now, lets say you would like to test a different property value, then you can update your mock ActivatedRoute as:

  it('should ensure property param is newProperty', async () => {
    activatedRoute.snapshot.params.property = 'newProperty';
    fixture = TestBed.createComponent(YourComponent);
    component = fixture.debugElement.componentInstance;
    activatedRoute = TestBed.get(ActivatedRoute);
    fixture.detectChanges();

    expect(activatedRoute.snapshot.params.property).toEqual('newProperty');
});

Hope this helps!

Solution 8 - Unit Testing

You can do this using the beforeAll function. Since beforeAll is called before all of your beforeEach functions, you can change your member variables before the component is compiled.

describe('MyComponent', () => {

  let fakeActivatedRoute = {
    paramMap: of(convertToParamMap({ id: '123' })),
    queryParamMap: of(convertToParamMap({ query: 'active' }))};


  beforeEach(async() => {
    await TestBed.configureTestingModule({

      providers: [
        ...
        { provide: ActivatedRoute, useValue: fakeActivatedRoute }],
      }).compileComponents();
    });
  });

  describe('id is present in route', () => {
    beforeAll(() => {
      fakeActivatedRoute.paramMap = 
        of(convertToParamMap({ id: '123' }));
      fakeActivatedRoute.queryParamMap = 
        of(convertToParamMap({ query: '' }));
    });

    it('should call service to look up id', () => {
      ...
    });
  });

  describe('id is not present in route', () => {
    beforeAll(() => {
      fakeActivatedRoute.paramMap = 
        of(convertToParamMap({ id: '' }));
      fakeActivatedRoute.queryParamMap = 
        of(convertToParamMap({ query: '' }));
    });

    it('should not call service to look up id', () => {
      ...
    });
  });

  describe('query is present in route', () => {
    beforeAll(() => {
      fakeActivatedRoute.paramMap = 
        of(convertToParamMap({ id: '123' }));
      fakeActivatedRoute.queryParamMap = 
        of(convertToParamMap({ query: 'inactive' }));
    });

    it('should call service to look up the inactive id', () => {
      ...
    });
  });
});

Solution 9 - Unit Testing

angular 11: add this in your spec file

imports: [
   RouterTestingModule.withRoutes([])
],

this help me out with just a single line, with other you need to mock provider

Solution 10 - Unit Testing

Added provider in the test class as:

{
  provide: ActivatedRoute,
  useValue: {
    paramMap: of({ get: v => { return { id: 123 }; } })
  } 
}

Solution 11 - Unit Testing

All other answers so far only do provide the value for the route param. What if you want to test the route change trigger itself? You can provide the ActivatedRoute in your test with a Subject and its Observable, so you can trigger the route change with source.next().

Code under test:

    constructor(private readonly route: ActivatedRoute) {}

    ngOnInit(): void {
      this.routeParamSubscription = this.route.params.subscribe((params) => {
        if (params['id']) {
          this.loadDetails(params['id']);
        }
      });
    }

Testing code:

    let routeChangeSource: BehaviorSubject<Params>;
    // In TestBed.configureTestingMethod
    ...
      providers: [
        {
          provide: ActivatedRoute,
          useValue: {
            params: routeChangeSource.asObservable()
          }
        }
      ]
    ...
    it('loads data on route change', fakeAsync(() => {
      const spy = spyOn(component, 'loadDetails').and.callThrough();
      routeChangeSource.next({ id: 99 });
      tick();
      expect(spy).toHaveBeenCalledOnceWith(99);
    }));

This tests the triggered action after the route change and makes sure it is activated.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionColby CoxView Question on Stackoverflow
Solution 1 - Unit TestingzmancView Answer on Stackoverflow
Solution 2 - Unit Testingdimitris mafView Answer on Stackoverflow
Solution 3 - Unit TestingColby CoxView Answer on Stackoverflow
Solution 4 - Unit TestingRadyView Answer on Stackoverflow
Solution 5 - Unit TestingFrancesco BorziView Answer on Stackoverflow
Solution 6 - Unit TestingShashank SharmaView Answer on Stackoverflow
Solution 7 - Unit TestingDiego HerreraView Answer on Stackoverflow
Solution 8 - Unit TestingfixView Answer on Stackoverflow
Solution 9 - Unit TestingWy-XcView Answer on Stackoverflow
Solution 10 - Unit TestingadelinorView Answer on Stackoverflow
Solution 11 - Unit TestingmattanjaView Answer on Stackoverflow