125

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.

Colby Cox
  • 1,639
  • 2
  • 10
  • 16

13 Answers13

165

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})
  }
}
Maloric
  • 5,525
  • 3
  • 31
  • 46
zmanc
  • 5,201
  • 12
  • 45
  • 90
  • 12
    Observable.of does not exists for me! :S – Alejandro Sanz Díaz Dec 26 '16 at 11:47
  • 4
    Import Observable from rxjs/Observable – zmanc Dec 26 '16 at 13:06
  • 6
    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:2605` – mixalbl4 Aug 08 '17 at 16:18
  • 1
    @MixerOID this is chrome eating bugs - put the test in a try-catch and you should see the error. – a3uge Aug 15 '17 at 16:10
  • 1
    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`. – adamdabb Nov 29 '17 at 16:07
  • 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 – danwellman Mar 04 '18 at 11:06
  • { provide: ActivatedRoute, useValue: { snapshot: { params: Observable.of({userName: 'testUser', emailValidationToken: 'token'}) } } } – Michael Dausmann Apr 22 '18 at 01:49
  • 5
    RxJs 6 `of` should be used alone. Also you'd likely use `RouterTestingModule` instead of this answer's code. – Ben Racicot Oct 21 '18 at 20:01
  • 5
    @BenRacicot this answer was given before RxJs 6 existed. Also instead saying "do this instead" provide an answer that can be upvoted directly. – zmanc Nov 01 '18 at 14:13
  • 1
    I would have not guessed that is the way to do it in a million years... Thanks Google... – SDekov Jan 21 '23 at 21:16
44

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.

AlexElin
  • 1,044
  • 14
  • 23
dimitris maf
  • 671
  • 8
  • 17
  • So much better than having to maintain an extra mock and you can easily set different parameters in tests. Thank you! – crackmigg Jan 16 '20 at 13:49
  • 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 ? – speksy Apr 05 '20 at 11:57
  • 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. – dimitris maf Apr 06 '20 at 11:47
  • Thank you , this was informative and helpful ☺ – Ahmed Shehatah Oct 12 '21 at 17:03
  • 1
    This works with Angular 6 – walkeros May 18 '22 at 20:18
20

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!

Colby Cox
  • 1,639
  • 2
  • 10
  • 16
  • 1
    I am facing this right now! However, I'm getting errors when I try to use `super` or `Observable`. Where do these come from? – Aarmora Jul 14 '16 at 18:39
  • `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'`. – oooyaya Mar 22 '17 at 01:53
  • 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? – ruffin Oct 11 '19 at 12:32
11

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'
      })
    }
  }
}
h0b0
  • 1,802
  • 1
  • 25
  • 44
Rady
  • 918
  • 1
  • 11
  • 16
8

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: () => {} } }
  };
}
Francesco Borzi
  • 56,083
  • 47
  • 179
  • 252
4

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!

Diego Herrera
  • 181
  • 2
  • 7
  • 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 Aug 12 '21 at 15:02
  • 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? – rosiejaneenomar Aug 12 '21 at 15:29
4

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

Wy-Xc
  • 51
  • 4
4

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', () => {
      ...
    });
  });
});
fix
  • 1,425
  • 16
  • 27
  • 1
    You saved my day, thanks! I have added `data` attribute also: ```typescript const fakeActivatedRoute = { data: of({ app: 'note' }), }; ``` – Serkan KONAKCI Feb 26 '23 at 23:14
3

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';

0

Added provider in the test class as:

{
  provide: ActivatedRoute,
  useValue: {
    paramMap: of({ get: v => { return { id: 123 }; } })
  } 
}
adelinor
  • 703
  • 9
  • 15
0

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.

mattanja
  • 1,442
  • 14
  • 21
0

In this case if the parameter is being accessed by the get method of the paramMap

in the .ts:

this.id= this.activatedRoute.snapshot.paramMap.get('id');

in the .spec.ts

providers: [
        {
          provide: ActivatedRoute,
          useValue: {
            snapshot: {
              paramMap: {
                get() {
                  return '97dbf5d7';
                }
              }
            }
          }
        }
      ]
Hamada
  • 1,836
  • 3
  • 13
  • 27
0

public 2023

ngOnInit() {
  this.bookId = +this.activatedRoute.snapshot.paramMap.get('bookId');

  this.getBook(this.bookId);
}

for one parameter

providers: [
   {
    provide: ActivatedRoute,
    useValue: {
      snapshot: {
        paramMap: {
          get: () => 1, // represents the bookId
        },
      },
    },
  },
],

for two or more parameter

If we only care about one paramater contained in paramMap, like in the example above, we can return this parameter directly from the get method: get: () => 1. If there are multiple parameters we care about we can use a switch statement instead:

  provide: ActivatedRoute,
  useValue: {
    snapshot: {
      paramMap: {
        get: (key: string) => {
          switch (key) {
            case 'bookId':
              return 2;
            case 'genre':
              return 'fiction'
              default:
              return ''
          }
        }
      },
    },
  },
},

With activated route param observables

ngOnInit() {
  this.activatedRoute.params.subscribe(params => {
    this.bookId = parseInt(params.bookId, 10)

    this.getBookById(this.bookId)
  })
}

We are still able to mock the router params but params will need to return an observable to allow our subscription to work. We can do this by using the RxJs of operator:

import { of } from 'rxjs';

...
{
  provide: ActivatedRoute,
  useValue: {
    params: of({
      bookId: 2,
    }),
  },
},
Mormon SUD
  • 41
  • 4