我正在为一个小型角度应用程序编写单元测试用例。它使用路由器,因此我正在使用Angle docs中建议的RouterDirectiveStub。
场景:
如果要导航到Bikes/B1
,请点击“自行车”组件测试。
在“汽车”组件中,以前的路线在这里造成了麻烦。
理想情况下,上一个测试不应影响下一个组件。
问题:
我应该如何在afterAll()中重置路由器状态?
还是有其他更好的解决方案可以测试?
错误代码:
START:
AppComponent
✔ should have 2 routerlinks in template
BikesComponent
✔ getBikes api call should be called
✔ should set service data to bikes array
✔ should hide loader when data is successfully fetched from service
✔ should show bike names in template
✔ all the bike names should have routerLinks in template
✔ should navigate to /bike/id when clicked on that bike name
CarsComponent
✖ getCars api call should be called
✔ should set service data to cars array
✔ should hide loader when data is successfully fetched from service
✔ should show car names in template
✔ all the car names should have routerLinks in template
✔ should have approprate routerLinks assigned in template
LOG: ''
✔ should navigate to /car/id when clicked on that car name
DetailsComponent
✔ #getDetails should be called
✔ #getDetails should be called 1 time
✔ should assign service data to details variable
✔ should disable loader when data is received from service
✔ should display details in template
✔ should display image in template
Finished in 1.428 secs / 1.228 secs @ 14:33:14 GMT+0530 (India Standard Time)
SUMMARY:
✔ 19 tests completed
✖ 1 test failed
FAILED TESTS:
CarsComponent
✖ getCars api call should be called
HeadlessChrome 73.0.3683 (Linux 0.0.0)
Uncaught Error: Uncaught (in promise): Error: Cannot match any routes. URL Segment: 'B1'
Error: Cannot match any routes. URL Segment: 'B1'
at ApplyRedirects.push../node_modules/@angular/router/fesm5/router.js.ApplyRedirects.noMatchError (node_modules/@angular/router/fesm5/router.js:2469:1)
at CatchSubscriber.selector (node_modules/@angular/router/fesm5/router.js:2450:1)
at CatchSubscriber.push../node_modules/rxjs/_esm5/internal/operators/catchError.js.CatchSubscriber.error (node_modules/rxjs/_esm5/internal/operators/catchError.js:34:1)
at MapSubscriber.push../node_modules/rxjs/_esm5/internal/Subscriber.js.Subscriber._error (node_modules/rxjs/_esm5/internal/Subscriber.js:79:1)
at MapSubscriber.push../node_modules/rxjs/_esm5/internal/Subscriber.js.Subscriber.error (node_modules/rxjs/_esm5/internal/Subscriber.js:59:1)
at MapSubscriber.push../node_modules/rxjs/_esm5/internal/Subscriber.js.Subscriber._error (node_modules/rxjs/_esm5/internal/Subscriber.js:79:1)
at MapSubscriber.push../node_modules/rxjs/_esm5/internal/Subscriber.js.Subscriber.error (node_modules/rxjs/_esm5/internal/Subscriber.js:59:1)
at MapSubscriber.push../node_modules/rxjs/_esm5/internal/Subscriber.js.Subscriber._error (node_modules/rxjs/_esm5/internal/Subscriber.js:79:1)
at MapSubscriber.push../node_modules/rxjs/_esm5/internal/Subscriber.js.Subscriber.error (node_modules/rxjs/_esm5/internal/Subscriber.js:59:1)
at TapSubscriber.push../node_modules/rxjs/_esm5/internal/operators/tap.js.TapSubscriber._error (node_modules/rxjs/_esm5/internal/operators/tap.js:61:1) thrown
bikes.component.spec.ts
import { async, ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing';
import {RouterTestingModule} from '@angular/router/testing';
import { BikesComponent } from './bikes.component';
import { UrlService } from 'src/app/services/url.service';
import { of } from 'rxjs';
import { Router, RouterModule } from '@angular/router';
import { RouterLinkDirectiveStub, click } from 'src/testing';
import { DebugElement } from '@angular/core';
import { AppModule } from 'src/app/app.module';
import { By } from '@angular/platform-browser';
import { IAutomobile } from 'src/app/models/automobile';
const testData: IAutomobile[] = [
{
'id': 'B1',
'name': 'Neiman Marcus Limited Edition Fighter',
'price': 11,
'image': 'https://i.imgur.com/93sHCh9.jpg',
'desc': 'Despite the $11 million price tag and mean looks, the Neiman Marcus Limited Edition Fighter is completely street-legal, managing the road at a 190 mph top speed, the power coming from a 120ci 45-degree air-cooled V-Twin engine complemented by titanium, aluminum, and carbon fiber body parts.',
'tags': '190 mph | V-Twin engine'
},
{
'id': 'B2',
'name': 'E90 AJS Porcupine',
'price': 7,
'image': 'https://i.imgur.com/44PqbKf.jpg',
'desc': 'The AJS 500 cc Porcupine was a British racing motorcycle built by Associated Motor Cycles (AMC), which débuted in 1945 with a horizontal-engine designated E90S. A later E95 model was developed with an inclined-engine. AMC produced AJS and Matchless brands at the time.',
'tags': '550 cc | 135mph'
},
];
let component: BikesComponent;
let fixture: ComponentFixture<BikesComponent>;
let urlService: jasmine.SpyObj<UrlService>;
let getBikesSpy: jasmine.Spy;
let router: Router;
let routerLinks: RouterLinkDirectiveStub[];
let routerLinksDE: DebugElement[];
describe('BikesComponent', () => {
beforeEach(async(() => {
urlService = jasmine.createSpyObj('UrlService', ['getBikes']);
TestBed.configureTestingModule({
imports: [ AppModule ]
})
// Get rid of app's Router configuration otherwise many failures.
// Doing so removes Router declarations; add the Router stubs
.overrideModule(AppModule, {
remove: {imports: [RouterModule]},
add: {
imports: [RouterTestingModule.withRoutes([{path: 'Bikes/B1', component: BikesComponent}])],
declarations: [ RouterLinkDirectiveStub ],
providers: [{ provide: UrlService, useValue: urlService }]
}
})
.compileComponents()
.then(() => {
fixture = TestBed.createComponent(BikesComponent);
component = fixture.componentInstance;
router = TestBed.get(Router);
urlService = TestBed.get(UrlService);
getBikesSpy = urlService.getBikes.and.returnValue(of(testData));
fixture.detectChanges(); // trigger initial changes // must to detectChanges after creating component instance;
});
}));
beforeEach(() => {
routerLinksDE = fixture.debugElement.queryAll(By.directive(RouterLinkDirectiveStub));
routerLinks = routerLinksDE.map(de => de.injector.get(RouterLinkDirectiveStub));
});
it('getBikes api call should be called', () => {
expect(getBikesSpy.calls.any()).toBe(true, 'should call getBikes function');
});
it('should set service data to bikes array', () => {
expect(component.bikes).toEqual(testData, 'should set bikes value with service data');
});
it('should hide loader when data is successfully fetched from service', () => {
expect(component.loaded).toBe(true, 'loaded should be true');
});
it('should show bike names in template', () => {
const bikeNames: HTMLCollectionOf<Element> = document.getElementsByClassName('item');
expect(bikeNames.length).toBe(testData.length, 'should display all the bike names in template, as received by service');
});
// // ROUTER LINKS TEST
it('all the bike names should have routerLinks in template', async(() => {
expect(routerLinks.length).toBe(testData.length, 'each bike should have routerLink');
}));
// this test triggers navigation, which affects CarsComponent tests
it('should navigate to /bike/id when clicked on that bike name', () => {
const bikeLinkDe = routerLinksDE[0]; // taking first routerLink for test
const bikeLink = routerLinks[0];
fixture.ngZone.run(() => {
click(bikeLinkDe); // trigger click event on the state name
fixture.detectChanges(); // update app to detect changes
expect(bikeLink.navigatedTo).toContain(testData[0].id);
});
});
});
cars.component.spec.ts
import { async, ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing';
import {RouterTestingModule} from '@angular/router/testing';
import { CarsComponent } from './cars.component';
import { UrlService } from 'src/app/services/url.service';
import { of } from 'rxjs';
import { Router, RouterModule } from '@angular/router';
import { RouterLinkDirectiveStub, click } from 'src/testing';
import { DebugElement } from '@angular/core';
import { AppModule } from 'src/app/app.module';
import { By } from '@angular/platform-browser';
import { IAutomobile } from 'src/app/models/automobile';
const testData: IAutomobile[] = [
{
'id': 'C1',
'name': 'Sweptail Rolls Royce',
'price': 13,
'image': 'https://i.imgur.com/LldGUJL.jpg ',
'desc': 'The Rolls-Royce Sweptail is hand-built, and inspired by coachbuilding of the 1920s and 1930s. The Sweptail was commissioned bespoke in 2013 as a one-off automobile, at the request of a super-yacht and aircraft specialist who had a unique idea in mind. Giles Taylor, director of design at Rolls-Royce Motor Cars described the Sweptail as "the automotive equivalent of Haute couture"',
'tags': '6.75 L V12 | 453 Hp | 150 MPH'
},
{
'id': 'C2',
'name': 'Koenigsegg CCXR Trevita',
'price': 4.8,
'image': 'https://i.imgur.com/nrK7N62.jpg',
'desc': 'The Koenigsegg CCX is a mid-engine sports car manufactured by Swedish automotive manufacturer Koenigsegg Automotive AB. The project began with the aim of making a global car, designed and engineered to comply with global safety and environment regulations, particularly to enter the United States car market.',
'tags': '4.7L V8 | 1004 HP | 254 MPH'
},
];
let component: CarsComponent;
let fixture: ComponentFixture<CarsComponent>;
let urlService: jasmine.SpyObj<UrlService>;
let getCarsSpy: jasmine.Spy;
let router: Router;
let routerLinks: RouterLinkDirectiveStub[];
let routerLinksDE: DebugElement[];
describe('CarsComponent', () => {
beforeEach(async(() => {
urlService = jasmine.createSpyObj('UrlService', ['getCars']);
TestBed.configureTestingModule({
imports: [ AppModule ]
})
// Get rid of app's Router configuration otherwise many failures.
// Doing so removes Router declarations; add the Router stubs
.overrideModule(AppModule, {
remove: {imports: [RouterModule]},
add: {
imports: [RouterTestingModule.withRoutes([{path: 'Cars/C1', component: CarsComponent}])],
declarations: [ RouterLinkDirectiveStub ],
providers: [{ provide: UrlService, useValue: urlService }]
}
})
.compileComponents()
.then(() => {
fixture = TestBed.createComponent(CarsComponent);
component = fixture.componentInstance;
router = TestBed.get(Router);
urlService = TestBed.get(UrlService);
getCarsSpy = urlService.getCars.and.returnValue(of(testData));
fixture.detectChanges(); // trigger initial changes // must to detectChanges after creating component instance;
});
}));
beforeEach(() => {
routerLinksDE = fixture.debugElement.queryAll(By.directive(RouterLinkDirectiveStub));
routerLinks = routerLinksDE.map(de => de.injector.get(RouterLinkDirectiveStub));
});
it('getCars api call should be called', () => {
expect(getCarsSpy.calls.any()).toBe(true, 'should call getAllBooks function');
});
it('should set service data to cars array', () => {
expect(component.cars).toEqual(testData, 'should set cars value with service data');
});
it('should hide loader when data is successfully fetched from service', () => {
expect(component.loader).toBe(false, 'loader should be disabled');
});
it('should show car names in template', () => {
const carNames: HTMLCollectionOf<Element> = document.getElementsByClassName('item');
expect(carNames.length).toBe(testData.length, 'should display all the book names in template, as received by service');
});
// ROUTER LINKS TEST
it('all the car names should have routerLinks in template', async(() => {
expect(routerLinks.length).toBe(testData.length, 'each car should have routerLink');
}));
it('should have approprate routerLinks assigned in template', async(() => {
expect(routerLinks[0].linkParams).toContain(`${testData[0].id}`, 'the first routerLink sould have 1st carId');
expect(routerLinks[1].linkParams).toContain(`${testData[1].id}`, 'the second routerLink sould have 2nd carId');
}));
// this test triggers navigation, which affects DetailsComponent tests
it('should navigate to /car/id when clicked on that car name', () => {
const carLinkDe = routerLinksDE[0]; // taking first routerLink for test
const carLink = routerLinks[0];
fixture.ngZone.run(() => {
click(carLinkDe); // trigger click event on the state name
fixture.detectChanges(); // update app to detect changes
expect(carLink.navigatedTo).toContain(testData[0].id);
});
});
});