如何对依赖于 ActivatedRoute 参数的组件进行单元测试?
How to unit test a component that depends on parameters from ActivatedRoute?
我正在对用于编辑对象的组件进行单元测试。该对象有一个唯一的 id
,用于从托管在服务中的对象数组中获取特定对象。具体id
是通过路由传递的参数获取的,具体是通过ActivatedRoute
class.
构造函数如下:
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];
我想 运行 对此组件进行基本单元测试。但是,我不确定如何注入 id
参数,组件 需要 这个参数。
顺便说一下:我已经有了 Session
服务的模拟,所以不用担心。
我知道怎么做了!
由于ActivatedRoute
是一个服务,可以为它建立一个mock服务。我们称此模拟服务为 MockActivatedRoute
。我们将在MockActivatedRoute
中扩展ActivatedRoute
,如下:
class MockActivatedRoute extends ActivatedRoute {
constructor() {
super(null, null, null, null, null);
this.params = Observable.of({id: "5"});
}
行super(null, ....)
初始化superclass,它有四个强制参数。然而,在这种情况下,我们不需要这些参数中的任何一个,因此我们将它们初始化为 null
值。我们只需要 params
的值,即 Observable<>
。因此,使用this.params
,我们覆盖params
的值并将其初始化为测试对象所依赖的参数的Observable<>
。
然后,与任何其他模拟服务一样,只需对其进行初始化并覆盖组件的提供程序。
祝你好运!
这是我在 angular 2.0 最新版本中测试它的方式...
import { ActivatedRoute, Data } from '@angular/router';
在供应商部分
{
provide: ActivatedRoute,
useValue: {
data: {
subscribe: (fn: (value: Data) => void) => fn({
yourData: 'yolo'
})
}
}
}
最简单的方法是使用 useValue
属性并提供一个包含您要模拟的值的 Observable。
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})
}
}
只需添加 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: () => {} } }
};
}
对于一些正在 Angular > 5 工作的人来说,if Observable.of();不工作然后他们可以通过导入 import { of } from 'rxjs';
来使用 of()
在 angular 8+ 中有 RouterTestingModule
,您可以使用它来访问组件的 ActivatedRoute
或 Router
。您还可以将路由传递给 RouterTestingModule
并为请求的路由方法创建间谍。
例如在我的组件中我有:
ngOnInit() {
if (this.route.snapshot.paramMap.get('id')) this.editMode()
this.titleService.setTitle(`${this.pageTitle} | ${TAB_SUFFIX}`)
}
在我的测试中我有:
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ ProductLinePageComponent ],
schemas: [NO_ERRORS_SCHEMA],
imports: [
RouterTestingModule.withRoutes([])
],
})
.compileComponents()
}))
beforeEach(() => {
router = TestBed.get(Router)
route = TestBed.get(ActivatedRoute)
})
及后面的 'it' 部分:
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
})
以类似的方式,我认为您可以通过 jasmine 的 spyOnProperty
方法直接测试 paramMap
,返回一个可观察对象或使用 rxjs 弹珠。它可能会节省一些时间,而且它不需要维护额外的模拟 class。
希望它有用并且有意义。
运行 在为路由路径创建测试套件时遇到同样的问题:
{
path: 'edit/:property/:someId',
component: YourComponent,
resolve: {
yourResolvedValue: YourResolver
}
}
在组件中,我将传入的属性初始化为:
ngOnInit(): void {
this.property = this.activatedRoute.snapshot.params.property;
...
}
当 运行 测试时,如果你没有在模拟 ActivatedRoute "useValue" 中传递 属性 值,那么在使用 "fixture.detectChanges()" 检测更改时你将得到未定义.这是因为 ActivatedRoute 的模拟值不包含 属性 params.property。然后,模拟 useValue 需要具有这些参数,以便 fixture 初始化组件中的 'this.property'。您可以将其添加为:
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();
});
});
您可以开始测试,例如:
it('should ensure property param is yourProperty', async () => {
expect(activatedRoute.snapshot.params.property).toEqual('yourProperty');
....
});
现在,假设您想测试一个不同的 属性 值,那么您可以将您的模拟 ActivatedRoute 更新为:
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');
});
希望对您有所帮助!
在测试中添加提供程序 class 作为:
{
provide: ActivatedRoute,
useValue: {
paramMap: of({ get: v => { return { id: 123 }; } })
}
}
到目前为止所有其他答案只提供路由参数的值。如果您想测试路由更改触发器本身怎么办?您可以在测试中为 ActivatedRoute 提供一个 Subject 及其 Observable,这样您就可以使用 source.next().
触发路由更改
被测代码:
constructor(private readonly route: ActivatedRoute) {}
ngOnInit(): void {
this.routeParamSubscription = this.route.params.subscribe((params) => {
if (params['id']) {
this.loadDetails(params['id']);
}
});
}
测试代码:
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);
}));
这会测试路由更改后触发的操作并确保它已激活。
angular 11:
将此添加到您的规范文件中
imports: [
RouterTestingModule.withRoutes([])
],
这只需要一行就可以帮我解决问题,其他的你需要模拟提供者
您可以使用 beforeAll
函数执行此操作。由于 beforeAll
在所有 beforeEach
函数之前被调用,因此您可以在编译组件之前更改成员变量。
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', () => {
...
});
});
});
我正在对用于编辑对象的组件进行单元测试。该对象有一个唯一的 id
,用于从托管在服务中的对象数组中获取特定对象。具体id
是通过路由传递的参数获取的,具体是通过ActivatedRoute
class.
构造函数如下:
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];
我想 运行 对此组件进行基本单元测试。但是,我不确定如何注入 id
参数,组件 需要 这个参数。
顺便说一下:我已经有了 Session
服务的模拟,所以不用担心。
我知道怎么做了!
由于ActivatedRoute
是一个服务,可以为它建立一个mock服务。我们称此模拟服务为 MockActivatedRoute
。我们将在MockActivatedRoute
中扩展ActivatedRoute
,如下:
class MockActivatedRoute extends ActivatedRoute {
constructor() {
super(null, null, null, null, null);
this.params = Observable.of({id: "5"});
}
行super(null, ....)
初始化superclass,它有四个强制参数。然而,在这种情况下,我们不需要这些参数中的任何一个,因此我们将它们初始化为 null
值。我们只需要 params
的值,即 Observable<>
。因此,使用this.params
,我们覆盖params
的值并将其初始化为测试对象所依赖的参数的Observable<>
。
然后,与任何其他模拟服务一样,只需对其进行初始化并覆盖组件的提供程序。
祝你好运!
这是我在 angular 2.0 最新版本中测试它的方式...
import { ActivatedRoute, Data } from '@angular/router';
在供应商部分
{
provide: ActivatedRoute,
useValue: {
data: {
subscribe: (fn: (value: Data) => void) => fn({
yourData: 'yolo'
})
}
}
}
最简单的方法是使用 useValue
属性并提供一个包含您要模拟的值的 Observable。
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})
}
}
只需添加 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: () => {} } }
};
}
对于一些正在 Angular > 5 工作的人来说,if Observable.of();不工作然后他们可以通过导入 import { of } from 'rxjs';
来使用 of()在 angular 8+ 中有 RouterTestingModule
,您可以使用它来访问组件的 ActivatedRoute
或 Router
。您还可以将路由传递给 RouterTestingModule
并为请求的路由方法创建间谍。
例如在我的组件中我有:
ngOnInit() {
if (this.route.snapshot.paramMap.get('id')) this.editMode()
this.titleService.setTitle(`${this.pageTitle} | ${TAB_SUFFIX}`)
}
在我的测试中我有:
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ ProductLinePageComponent ],
schemas: [NO_ERRORS_SCHEMA],
imports: [
RouterTestingModule.withRoutes([])
],
})
.compileComponents()
}))
beforeEach(() => {
router = TestBed.get(Router)
route = TestBed.get(ActivatedRoute)
})
及后面的 'it' 部分:
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
})
以类似的方式,我认为您可以通过 jasmine 的 spyOnProperty
方法直接测试 paramMap
,返回一个可观察对象或使用 rxjs 弹珠。它可能会节省一些时间,而且它不需要维护额外的模拟 class。
希望它有用并且有意义。
运行 在为路由路径创建测试套件时遇到同样的问题:
{
path: 'edit/:property/:someId',
component: YourComponent,
resolve: {
yourResolvedValue: YourResolver
}
}
在组件中,我将传入的属性初始化为:
ngOnInit(): void {
this.property = this.activatedRoute.snapshot.params.property;
...
}
当 运行 测试时,如果你没有在模拟 ActivatedRoute "useValue" 中传递 属性 值,那么在使用 "fixture.detectChanges()" 检测更改时你将得到未定义.这是因为 ActivatedRoute 的模拟值不包含 属性 params.property。然后,模拟 useValue 需要具有这些参数,以便 fixture 初始化组件中的 'this.property'。您可以将其添加为:
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();
});
});
您可以开始测试,例如:
it('should ensure property param is yourProperty', async () => {
expect(activatedRoute.snapshot.params.property).toEqual('yourProperty');
....
});
现在,假设您想测试一个不同的 属性 值,那么您可以将您的模拟 ActivatedRoute 更新为:
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');
});
希望对您有所帮助!
在测试中添加提供程序 class 作为:
{
provide: ActivatedRoute,
useValue: {
paramMap: of({ get: v => { return { id: 123 }; } })
}
}
到目前为止所有其他答案只提供路由参数的值。如果您想测试路由更改触发器本身怎么办?您可以在测试中为 ActivatedRoute 提供一个 Subject 及其 Observable,这样您就可以使用 source.next().
触发路由更改被测代码:
constructor(private readonly route: ActivatedRoute) {}
ngOnInit(): void {
this.routeParamSubscription = this.route.params.subscribe((params) => {
if (params['id']) {
this.loadDetails(params['id']);
}
});
}
测试代码:
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);
}));
这会测试路由更改后触发的操作并确保它已激活。
angular 11: 将此添加到您的规范文件中
imports: [
RouterTestingModule.withRoutes([])
],
这只需要一行就可以帮我解决问题,其他的你需要模拟提供者
您可以使用 beforeAll
函数执行此操作。由于 beforeAll
在所有 beforeEach
函数之前被调用,因此您可以在编译组件之前更改成员变量。
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', () => {
...
});
});
});