angularjasminekarma-jasmineangular-routerangular-unit-test

Angular unit testing relative RouterLinks


In my Angular app I need to be able to unit test that routerLink create the correct href attribute. Sometimes they are links relative to the current route, and sometimes they are absolute links from the root.

I am only able to successfully test the absolute links. When I attempt to test the relative links it just shows the href value as '/'

In my component template I do this

@for (item of dataService.list; track item.id){
  @if (item.documents.length === 1) {
    <a [routerLink]="['/', 'documents', item.documents[0].id]">{{ item.id }} (Single)</a>
  } @else {
    <a [routerLink]="item.id">{{ item.id }} (Multiple)</a>
  }
}

And in my *.spec.ts file I have

describe('AppComponent', () => {
  let fixture: ComponentFixture<AppComponent>;
  let mockDataService: DataService;

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      imports: [AppComponent],
      providers: [provideRouter([]), { provide: ActivatedRoute, useValue: {} }],
    }).compileComponents();

    fixture = TestBed.createComponent(AppComponent);
    mockDataService = TestBed.inject(DataService);
  });

  //This test FAILS with "Expected '/' to equal 'One'."
  it('should use a relative link when there are multiple documents', () => {
    spyOnProperty(mockDataService, 'list', 'get').and.returnValue([
      { id: 'One', documents: [{ id: '1' }, { id: '2' }] },
    ]);
    fixture.detectChanges();

    const linkEl = fixture.debugElement.query(By.css('a')).nativeElement;

    expect(linkEl.getAttribute('href')).toEqual('One');
  });

  //This test PASSES
  it('should use an absolute link when there is only one document', () => {
    spyOnProperty(mockDataService, 'list').and.returnValue([
      { id: 'Two', documents: [{ id: '3' }] },
    ]);
    fixture.detectChanges();

    const linkEl = fixture.debugElement.query(By.css('a')).nativeElement;

    expect(linkEl.getAttribute('href')).toEqual('/documents/3');
  });
});

StackBlitz Demo


Solution

  • Directly testing the attribute:

    The provided mock ActivatedRoute was messing up the routerLink created href, remove it and the relative paths work great.

    beforeEach(async () => {
      await TestBed.configureTestingModule({
        imports: [AppComponent],
        providers: [
          provideRouter([]), 
          // { provide: ActivatedRoute, useValue: {} } // <- changed here!
        ],
      }).compileComponents();
    
      fixture = TestBed.createComponent(AppComponent);
      mockDataService = TestBed.inject(DataService);
    });
    

    Full Code:

    import { ComponentFixture, TestBed } from '@angular/core/testing';
    import { DataService } from './data.service';
    import { AppComponent } from './app.component';
    import { ActivatedRoute, provideRouter } from '@angular/router';
    import { By } from '@angular/platform-browser';
    
    describe('AppComponent', () => {
      let fixture: ComponentFixture<AppComponent>;
      let mockDataService: DataService;
    
      beforeEach(async () => {
        await TestBed.configureTestingModule({
          imports: [AppComponent],
          providers: [
            provideRouter([]),
            // { provide: ActivatedRoute, useValue: {} } // <- changed here!
          ],
        }).compileComponents();
    
        fixture = TestBed.createComponent(AppComponent);
        mockDataService = TestBed.inject(DataService);
      });
    
      it('should use a relative link when there are multiple documents', () => {
        spyOnProperty(mockDataService, 'list', 'get').and.returnValue([
          { id: 'One', documents: [{ id: '1' }, { id: '2' }] },
        ]);
        fixture.detectChanges();
        const linkEl = fixture.debugElement.query(By.css('a')).nativeElement;
        console.log(linkEl);
        expect(linkEl.getAttribute('href')).toEqual('/One');
      });
    
      it('should use an absolute link when there is only one document', () => {
        spyOnProperty(mockDataService, 'list').and.returnValue([
          { id: 'Two', documents: [{ id: '3' }] },
        ]);
        fixture.detectChanges();
    
        const linkEl = fixture.debugElement.query(By.css('a')).nativeElement;
    
        console.log('absolute link', linkEl.getAttribute('href'));
    
        expect(linkEl.getAttribute('href')).toEqual('/documents/3');
      });
    });
    

    Stackblitz Demo


    Testing using provideRouter:

    We can first add RouterOutlet and the router-outlet selector.

    Then create dummy routes with dummy components to provide the fake routes. Then we need to get rid of the activated route mock, since it is messing up the routing.

    @Component({ selector: 'dummy' })
    export class DummyComponent {}
    
    @Component({ selector: 'dummy2' })
    export class DummyComponent2 {}
    
    describe('AppComponent', () => {
      let fixture: ComponentFixture<AppComponent>;
      let mockDataService: DataService;
    
      beforeEach(async () => {
        await TestBed.configureTestingModule({
          imports: [AppComponent],
          providers: [
            provideRouter([
              { path: 'documents/:id', component: DummyComponent },
              { path: ':id', component: DummyComponent2 },
            ]),
          ],
        }).compileComponents();
    
        fixture = TestBed.createComponent(AppComponent);
        mockDataService = TestBed.inject(DataService);
      });
    

    Now we simply click the a tag and check the route.

    it('should use a relative link when there are multiple documents', async () => {
      spyOnProperty(mockDataService, 'list', 'get').and.returnValue([
        { id: 'One', documents: [{ id: '1' }, { id: '2' }] },
      ]);
      fixture.detectChanges();
      const linkEl = fixture.debugElement.query(By.css('a')).nativeElement;
      linkEl.click();
      await fixture.whenStable();
      expect(TestBed.inject(Router).url).toEqual('/One');
    });
    

    Full Code:

    import { ComponentFixture, TestBed } from '@angular/core/testing';
    import { DataService } from './data.service';
    import { AppComponent } from './app.component';
    import { ActivatedRoute, provideRouter, Router } from '@angular/router';
    import { By } from '@angular/platform-browser';
    import { Component } from '@angular/core';
    
    @Component({ selector: 'dummy' })
    export class DummyComponent {}
    
    @Component({ selector: 'dummy2' })
    export class DummyComponent2 {}
    
    describe('AppComponent', () => {
      let fixture: ComponentFixture<AppComponent>;
      let mockDataService: DataService;
    
      beforeEach(async () => {
        await TestBed.configureTestingModule({
          imports: [AppComponent],
          providers: [
            provideRouter([
              { path: 'documents/:id', component: DummyComponent },
              { path: ':id', component: DummyComponent2 },
            ]),
          ],
        }).compileComponents();
    
        fixture = TestBed.createComponent(AppComponent);
        mockDataService = TestBed.inject(DataService);
      });
    
      it('should use a relative link when there are multiple documents', async () => {
        spyOnProperty(mockDataService, 'list', 'get').and.returnValue([
          { id: 'One', documents: [{ id: '1' }, { id: '2' }] },
        ]);
        fixture.detectChanges();
        const linkEl = fixture.debugElement.query(By.css('a')).nativeElement;
        linkEl.click();
        await fixture.whenStable();
        expect(TestBed.inject(Router).url).toEqual('/One');
      });
    
      it('should use an absolute link when there is only one document', async () => {
        spyOnProperty(mockDataService, 'list').and.returnValue([
          { id: 'Two', documents: [{ id: '3' }] },
        ]);
        fixture.detectChanges();
        const linkEl = fixture.debugElement.query(By.css('a')).nativeElement;
        linkEl.click();
        await fixture.whenStable();
        expect(TestBed.inject(Router).url).toEqual('/documents/3');
      });
    });
    

    Stackblitz Demo