对路由进行测试
对于模版文件中有 的
在TestBed.configureTestingModule()的元数据的imports数据一定要加上"RouterTestingModule";
属于嵌套到组件中的其他组件,并不是单元测试的重点。
第一种处理方式-为创建和声明一些测试桩(无关紧要的组件或指令处理方式相同)
@Component({selector: 'router-outlet', template: ''})
class RouterOutletStubComponent { }
这个RouterOutletStubComponent测试桩的选择器要和其对应的真实组件一致,但其模板和类是空的,然后在TestBed.configureTestingModule的declarations数组中配置
declarations: [
BrowseComponent,
RouterOutletStubComponent,
]
第二种处理方式-把 NO_ERRORS_SCHEMA 添加到 TestBed.schemas 的元数据中。
TestBed.configureTestingModule({
declarations: [
AppComponent,
RouterLinkDirectiveStub
],
schemas: [ NO_ERRORS_SCHEMA ]
})
这里的NO_ERRORS_SCHEMA 会要求 Angular 编译器忽略不认识的那些元素和属性,并且不会报错,Angular只会把它们渲染成空白标签,而浏览器会忽略这些标签。
两种方法可以同时使用
对于在类文件中有通过 'navigateByUrl()‘或’navigate()’ 方法来跳转路由
在类文件中有一个跳转的路由
this.route.navigateByUrl('/Login');
测试代码:
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
import {BrowseComponent} from './browse.component';
import {NgZorroAntdModule} from 'ng-zorro-antd';
import {Router} from '@angular/router';
describe('BrowseComponent', () => {
let component: BrowseComponent;
let router: Router;
let fixture: ComponentFixture<BrowseComponent>;
const routerSpy = jasmine.createSpyObj('Router', ['navigateByUrl']);
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [NgZorroAntdModule],
declarations: [BrowseComponent],
providers: [
{provide: Router, useValue: routerSpy}
]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(BrowseComponent);
router = fixture.debugElement.injector.get(Router);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create and go to login', () => {
const spy = router.navigateByUrl as jasmine.Spy;
const navArgs = spy.calls.first().args[0];
expect(component).toBeTruthy();
expect(navArgs).toBe('/Login');
});
});
- 在这里先声明一个 Router 的router。
let router: Router;
- 创建一个通过jasmine.createSpyObj() 方法创建一个routerSpy,routerSpy是关于Router的间谍(模拟对象)。
const routerSpy = jasmine.createSpyObj('Router', ['navigateByUrl']);
- 用于将 Router 注册到当前的模块中而使用的值。
{provide: Router, useValue: routerSpy}
- 通过TestBed 创建的实例化组件中的debugElement.injector.get(Router)方法进行初始化。
fixture = TestBed.createComponent(BrowseComponent);
router = fixture.debugElement.injector.get(Router);
- 在it()函数中,通过组件调用来执行路由跳转,检测Router.navigateByUrl 曾用所期待的URL调用过。
const spy = router.navigateByUrl as jasmine.Spy;
const navArgs = spy.calls.first().args[0];
expect(component).toBeTruthy();
expect(navArgs).toBe('/Login');
因为这里的跳转路由在 ngOnInit() 方法中,在expect(component).toBeTruthy()时执行了路由的跳转,所以这里只要检测 router.navigateByUrl(’/Login’) 路由是否调用过就可以了。
对于在类文件中有通过操纵注入到组件构造函数中的 ActivatedRoute 来获取路由中参数
有时会在路由中传递参数,如下:
constructor(
private activatedRoute: ActivatedRoute,
private router: Router) {
}
this.activatedRoute.paramMap.subscribe((paramMap: ParamMap) => {
this.userId = Number(paramMap.get('id'));
});
此时测试的组件需要路由的中id这个参数。
ActivatedRoute不能采用和 Router 相似的间谍来注册,这里需要另一种方式,在测试期间,paramMap 会返回一个可能会发出多个值的 Observable。此时路由器的辅助函数 convertToParamMap() 来创建 ParamMap。针对路由目标组件的其它测试需要一个 ActivatedRoute 的测试替身。因为这个可能会有多个组件需要用到在 paramMap 设置参数,所以可以将这个ActivatedRoute 的测试替身(ActivatedRouteStub)写成一个共同的文件,使其他组件在测试的过程中,比较方便。
ActivatedRouteStub 文件内容如下:
import {convertToParamMap, ParamMap, Params} from '@angular/router';
import {ReplaySubject} from 'rxjs';
export class ActivatedRouteStub {
private subject = new ReplaySubject<ParamMap>();
constructor(initialParams?: Params) {
this.setParamMap(initialParams);
}
/** The mock paramMap observable */
readonly paramMap = this.subject.asObservable();
/** Set the paramMap observables's next value */
setParamMap(params?: Params) {
this.subject.next(convertToParamMap(params));
}
}
- 使用 ActivatedRouteStub 进行测试
在路由中增加参数,是在测试组件没有被创建之前,最好在当前测试的组件没有被TestBed动态创建之前设置。
let activatedRoute: ActivatedRouteStub;
let router: Router;
describe('loadRouter', () => {
beforeEach(() => {
activatedRoute = new ActivatedRouteStub();
});
describe('UserComponent', () => {
let component: UserComponent;
let fixture: ComponentFixture<UserComponent>;
});
});
然后在testBed中提供 ActivatedRoute 并且使用的值为:ActivatedRoute的替身activatedRoute;
TestBed.configureTestingModule({
imports: [
NgZorroAntdModule,
SecondaryTitleModule,
BreadcrumbModule,
FormsModule,
ReactiveFormsModule,
HttpClientTestingModule,
TranslateModule.forRoot(),
ButtonModule,
RouterTestingModule,
],
declarations: [UserComponent],
providers: [
TranslateService,
HttpService,
Location,
{provide: Router, useValue: routerSpy},
{provide: ActivatedRoute, useValue: activatedRoute},
]
})
接下来就是设置路由中的参数了
beforeEach(() => {
activatedRoute.setParamMap({id: 1});
fixture = TestBed.createComponent(UserComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
设置好了以后,对相应的代码段进行测试
it('should get activatedRoute params', () => {
router = fixture.debugElement.injector.get(Router);
component.ngOnInit();
expect(component.userId).toBe(1);
});
对于带有 RouterLink 的组件
有时会在模版中使用 RouterLink 指令来跳转路由,
<a href="javascript:void(0)" [routerLink]="'/xxx/www/'">{{userName}}</a>
在实际的 RouterLinkDirective 太复杂了,而且 RouterLinkDirective 与 RouterModule 中的其它组件和指令有着非常复杂的联系,所以化繁为简,在这里使用 RouterLinkDirectiveStub 代替换了真实的指令。
@Directive({
selector: '[routerLink]'
})
export class RouterLinkDirectiveStub {
@Input('routerLink') linkParams: any;
navigatedTo: any = null;
@HostListener('click')
onClick() {
this.navigatedTo = this.linkParams;
}
}
import {NgModule} from '@angular/core';
@NgModule({
declarations: [
RouterLinkDirectiveStub
]
})
export class RouterStubsModule {
}
这里的 URL 被绑定到了 [routerLink] 属性上,它的值流入了该指令的 linkParams 属性,并将宿主元素的点击事件关联到了这里的onClick()方法上。
- 将RouterLinkDirectiveStub 引入到 TestBed.configureTestingModule 的 declarations 的数组中
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
NgZorroAntdModule,
TranslateModule.forRoot(),
],
declarations: [HeaderComponent, RouterLinkDirectiveStub],
providers: [
TranslateService,
{provide: Router, useValue: routerSpy}]
})
.compileComponents();
}));
- 在 describe() 中使用 RouterLinkDirectiveStub
describe('HeaderComponent', () => {
let routerLinks: RouterLinkDirectiveStub[];
});
- 使用By.directive 获取导航链接的引用
beforeEach(() => {
fixture = TestBed.createComponent(HeaderComponent);
component = fixture.componentInstance;
router = fixture.debugElement.injector.get(Router);
translate = fixture.debugElement.injector.get(TranslateService);
nzI18nService = fixture.debugElement.injector.get(NzI18nService);
fixture.detectChanges();
linkDes = fixture.debugElement.queryAll(By.directive(RouterLinkDirectiveStub));
routerLinks = linkDes.map(de => de.injector.get(RouterLinkDirectiveStub));
});
- 使用 By.directive 来定位一个带有附属指令的指令的链接元素。
- queryAll(By.directive(RouterLinkDirectiveStub))查询返回包含了匹配元素的 DebugElement 包装器。
- 每个 DebugElement 都会导出该元素中的一个依赖注入器,其中带有指定的指令实例。
- 验证是否导航正确
it('can get RouterLinks from template', () => {
expect(routerLinks.length).toBe(3, 'should have 3 routerLinks');
expect(routerLinks[0].linkParams).toBe('/Devops/MyWork/Account/AccountView');
expect(routerLinks[1].linkParams).toBe('MyWork');
expect(routerLinks[2].linkParams).toBe('MyProject');
});
it('can click MyProject link in template', () => {
const myProjectLinkDe = linkDes[2];
const myProjectLink = routerLinks[2];
expect(myProjectLink.navigatedTo).toBeNull('should not have navigated yet');
myProjectLinkDe.triggerEventHandler('click', null);
fixture.detectChanges();
expect(myProjectLink.navigatedTo).toBe('MyProject');
});
第一个测试用例是对当前组件模版中的所有带有 routerLink 指令的元素进行汇总查找,第二个是对其中一个进行单独模拟点击的测试。