109

In Angular 2.0.0, I am unit testing a component that uses Router. However I get the 'Supplied parameters do not match any signature of call target.' error. In Visual studio code in spec.ts it is the new Router() that is highlighted in red

What is the correct syntax?

My code as follows:

spec.ts

import { TestBed, async } from '@angular/core/testing';
import { NavToolComponent } from './nav-tool.component';
import { ComponentComm } from '../../shared/component-comm.service';
import { Router } from '@angular/router';

describe('Component: NavTool', () => {
    it('should create an instance', () => {
    let component = new NavToolComponent( new ComponentComm(), new Router());
    expect(component).toBeTruthy();
    });
});

Component constructor

constructor(private componentComm: ComponentComm, private router: Router) {}
starball
  • 20,030
  • 7
  • 43
  • 238
Ka Tech
  • 8,937
  • 14
  • 53
  • 78

5 Answers5

221

You can also just use the RouterTestingModule and just spyOn the navigate function like this

import { TestBed } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { Router } from '@angular/router';

import { MyModule } from './my-module';
import { MyComponent } from './my-component';

describe('something', () => {

    let fixture: ComponentFixture<LandingComponent>;
    let router: Router;

    beforeEach(() => {

        TestBed.configureTestingModule({
            imports: [
                MyModule,
                RouterTestingModule.withRoutes([]),
            ],
        }).compileComponents();

        fixture = TestBed.createComponent(MyComponent);
        router = TestBed.get(Router); // TestBed.inject(Router) for Angular 9+

    });

    it('should navigate', () => {
        const component = fixture.componentInstance;
        const navigateSpy = spyOn(router, 'navigate');

        component.goSomewhere();
        expect(navigateSpy).toHaveBeenCalledWith(['/expectedUrl']);
    });
});
AlexElin
  • 1,044
  • 14
  • 23
Lenny
  • 5,663
  • 2
  • 19
  • 27
  • 11
    Thanks, this works! I also use `router = TestBed.get(Router)` and save my router to a variable alongside fixture, instead of casting component to any, as recommended in https://angular.io/guide/testing#testbedget – Ryan Burbidge Oct 02 '17 at 00:49
  • 2
    Thanks, this solved my issue: cannot read property 'root' of undefined when mocking router. – Juni Brosas Mar 07 '18 at 03:34
  • Hi @ Lenny Can you please explain what is component.goSomewhere(); doing here? – harsh Apr 06 '18 at 10:22
  • 2
    @harsh to pass this test the component would have to have a method called `goSomewhere()` that contains the code `this.router.navigate([/expectedUrl'])` (which would navigate to `/expectedUrl`. – Lenny Jul 24 '18 at 23:21
  • 3
    With this code I see the warn: ```console.warn ../../../node_modules/@angular/core/bundles/core.umd.js:27337 Navigation triggered outside Angular zone, did you forget to call 'ngZone.run()'?``` – adrisons Mar 16 '20 at 13:26
43

It's because the Route has some dependencies it expects passed to its constructor.

If you're using Angular components, you shouldn't be trying to do isolated tests. You should use the Angular testing infrastructure to prepare the test environment. This means letting Angular create the component, letting it inject all the required dependencies, instead of you trying to create everything.

To get you started, you should have something like

import { TestBed } from '@angular/core/testing';

describe('Component: NavTool', () => {
  let mockRouter = {
    navigate: jasmine.createSpy('navigate')
  };
  beforeEach(() => {
    TestBed.configureTestingModule({
      declarations: [ NavToolComponent ],
      providers: [
        { provide: Router, useValue: mockRouter },
        ComponentComm
      ]
    });
  });
  it('should click link', () => {
    let fixture = TestBed.createComponent(NavToolComponent);
    fixture.detectChanges();
    let component: NavToolComponent = fixture.componentInstance;
    component.clickLink('home');
    expect(mockRouter.navigate).toHaveBeenCalledWith(['/home']);
  });
});

Or something like that. You use the TestBed to configure a module from scratch for the testing. You configure it pretty much the same way with an @NgModule.

Here we are just mocking the router. Since we are just unit testing, we may not want the real routing facility. We just want to make sure that it is called with the right arguments. The mock and spy will be able to capture that call for us.

If you do want to use the real router, then you need to use the RouterTestingModule, where you can configure routes. See an example here and here

See Also:

bugs
  • 14,631
  • 5
  • 48
  • 52
Paul Samsotha
  • 205,037
  • 37
  • 486
  • 720
  • 1
    This works like a charm. TestBed.get which was in another solution is deprecated by now. – fegyi001 Jan 12 '22 at 12:30
  • I use this code, and it works. But... I also get an error in the NgTest console: "Can't bind to 'routerLink' since it isn't a known property of 'a'.". I tried importing RouterModule and RoutingModule, but that didn't work. Is it because I use a MockRouter, that Angular can't find the routerlink attribute is available on the a element? – Michel Jan 13 '22 at 11:14
  • `If you do want to use the real router, then you need to use the RouterTestingModule` and `letting Angular create the component, letting it inject all the required dependencies, instead of you trying to create everything` were nice pieces of advice that helped me fix my test which needed to use actual angular router rather than a mocked one inside the service i was testing. – oomer Jun 24 '22 at 07:22
  • I had to just pass the service class ref i-e `HiddenNavGuard` in the providers array in TestBed rather than passing an object of the service `new HiddenNavGuard(router, appServiceStub);` created by myself, and viola, the HiddenNavGuard then had access to the router created by the testing module. – oomer Jun 24 '22 at 07:31
5

Jasmine goes one better with full spy objects...

describe('Test using router', () => {
    const router = jasmine.createSpyObj('Router', ['navigate']);
    ...
    beforeEach(async(() => {
        TestBed.configureTestingModule({
            providers: [  { provide: Router, useValue: router } ],
            ...
    });        
});
MiniGod
  • 3,683
  • 1
  • 26
  • 27
jkyoutsey
  • 1,969
  • 2
  • 20
  • 31
  • how to create fake model and pass route? – Aqdas Apr 28 '22 at 19:40
  • @Aqdas `jasmine.createSpyObj(['navigate'], { someProperty: someValue });` Sometimes "someValue" needs to be a Subject that you can emit values, but this should help you do what you want. – jkyoutsey Mar 30 '23 at 16:07
5

Here an axample if we inject Route service in our component controller:

import { TestBed, async } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing'; // Because we inject service in our component
import { Router } from '@angular/router'; // Just if we need to test Route Service functionality

import { AppComponent } from './app.component';
import { DummyLoginLayoutComponent } from '../../../testing/mock.components.spec'; // Because we inject service in your component

describe('AppComponent', () => {
  let router: Router; // Just if we need to test Route Service functionality

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [
        AppComponent,
        DummyLoginLayoutComponent // Because we inject service in our component
      ],
      imports: [
        RouterTestingModule.withRoutes([
          { path: 'login', component: DummyLoginLayoutComponent },
        ]) // Because we inject service in our component
      ],
    }).compileComponents();

    router = TestBed.get(Router); // Just if we need to test Route Service functionality
    router.initialNavigation(); // Just if we need to test Route Service functionality
  }));

  it('should create the app', async(() => {
    const fixture = TestBed.createComponent(AppComponent);
    const app = fixture.debugElement.componentInstance;
    expect(app).toBeTruthy();
  }));
});

We can also test other functionalitites such as navigate(). Just in case:

it('should call eventPage once with /register path if event is instanceof NavigationStart', fakeAsync(() => {
    spyOn(analyticService, 'eventPage');
    router.navigate(['register'])
      .then(() => {
        const baseUrl = window.location.origin;
        const url = `${baseUrl}/register`;
        expect(analyticService.eventPage).toHaveBeenCalledTimes(1);
        expect(analyticService.eventPage).toHaveBeenCalledWith(url);
      });
}));

My file with all mock components (mock.components.specs.ts)

import { Component } from '@angular/core';

@Component({
    selector: 'home',
    template: '<div>Dummy home component</div>',
    styleUrls: []
})

export class DummyHomeComponent { }
Daniel Delgado
  • 4,813
  • 5
  • 40
  • 48
0

Another way of achieving this is by checking the current URL via the Location object.

In the example below, we ...

  1. search for the component
  2. use fakeAsync and tick to make sure the test runs async operations as intended
  3. trigger the click event
  4. check what the current url is
it('navigates on NavToolComponent button click', fakeAsync(() => {
  // find component
  const button = fixture.debugElement.query(By.directive(NavToolComponent));
  expect(button).withContext('NavToolComponent button exists').toBeTruthy();

  // trigger click event
  button.nativeElement.click();

  // let async operations happen
  fixture.detectChanges();
  tick();

  // check current URL
  const location = TestBed.inject(Location);
  expect(location.path()).toEqual('/portfolio/trading');
}));
Joel Lau
  • 69
  • 1
  • 7