Angular 6 - 如何在单元测试中模拟 router.events url 响应
Posted
技术标签:
【中文标题】Angular 6 - 如何在单元测试中模拟 router.events url 响应【英文标题】:Angular 6 - how to mock router.events url response in unit test 【发布时间】:2019-03-24 20:35:48 【问题描述】:如标题所示,我需要在单元测试中模拟router.events
。
在我的组件中,我做了一些正则表达式来获取 url 中斜线之间的第一个文本实例;例如,/pdp/
constructor(
private route: ActivatedRoute,
private router: Router,
this.router.events.pipe(takeUntil(this.ngUnsubscribe$))
.subscribe(route =>
if (route instanceof NavigationEnd)
// debugger
this.projectType = route.url.match(/[a-z-]+/)[0];
);
构建组件时我的单元测试错误:Cannot read property '0' of null
。当我在调试器中注释时,route
似乎没有正确设置,但我在单元测试本身中设置它。我已经用几种不同的方式完成了(受这篇文章的启发:Mocking router.events.subscribe() Angular2 和其他人)。
第一次尝试:
providers: [
provide: Router,
useValue:
events: of(new NavigationEnd(0, '/pdp/project-details/4/edit', 'pdp/project-details/4/edit'))
,
// ActivatedRoute also being mocked
provide: ActivatedRoute,
useValue:
snapshot: url: [ path: 'new' , path: 'edit' ] ,
parent:
parent:
snapshot:
url: [
path: 'pdp'
]
]
第二次尝试(基于上述帖子):
class MockRouter
public events = of(new NavigationEnd(0, '/pdp/project-details/4/edit', '/pdp/project-details/4/edit'))
providers: [
provide: Router,
useClass: MockRouter
]
第三次尝试(同样基于上述帖子):
class MockRouter
public ne = new NavigationEnd(0, '/pdp/project-details/4/edit', '/pdp/project-details/4/edit');
public events = new Observable(observer =>
observer.next(this.ne);
observer.complete();
);
providers: [
provide: Router,
useClass: MockRouter
]
第四次尝试:
beforeEach(() =>
spyOn((<any>component).router, 'events').and.returnValue(of(new NavigationEnd(0, '/pdp/project-details/4/edit', 'pdp/project-details/4/edit')))
...
第五次尝试:
beforeEach(() =>
spyOn(TestBed.get(Router), 'events').and.returnValue(of( url:'/pdp/project-details/4/edit' ))
...
在上述所有情况下,route
都没有被设置; NavigationEnd
对象等于:
id: 1, url: "/", urlAfterRedirects: "/"
想法?
【问题讨论】:
【参考方案1】:可以这么简单:
TestBed.configureTestingModule(
imports: [RouterTestingModule]
).compileComponents()
...
const event = new NavigationEnd(42, '/', '/');
(TestBed.inject(Router).events as Subject<Event>).next(event);
【讨论】:
TS2339 属性 'next' 不存在于类型 'Observableany
)
这是一个打字稿编译器错误,而不是 linting 错误。虽然返回的 Router.events 类型可以实现 Observer 和 Observable 接口,但 events 属性的公共接口是 Observable ,它不公开 next()。如果将来返回的对象不是 Observer,则将来存在重大更改的风险。使用 Angular 7、TS 3.2.4
@RichardCollette 对于未来发生重大变化的风险,您是对的。关于编译错误:我无法理解 - TestBed.get
方法的返回类型为 any
,因此不能进行任何编译时检查(除非您专门将结果转换为 Router
)..
在我的案例中,组件中的订阅确实被这段代码触发,但是,我的测试不等待订阅解决,fixture.whenStable 永远不会解决,我尝试了一切异步,fakeAsync 等 ...【参考方案2】:
这是我想出的答案。我认为我只是没有将第一种方法(以上)扩展得足够远:
import of from 'rxjs';
import NavigationEnd, Router from '@angular/router';
providers: [
provide: Router,
useValue:
url: '/non-pdp/phases/8',
events: of(new NavigationEnd(0, 'http://localhost:4200/#/non-pdp/phases/8', 'http://localhost:4200/#/non-pdp/phases/8')),
navigate: jasmine.createSpy('navigate')
]
【讨论】:
但是这个路由器不能导航,所以你不能测试例如导航到“”应该重定向到/home的场景【参考方案3】:认为这可能会有所帮助...在模板中有一个带有 routerLinks 的页脚组件。正在获取未提供根的错误....必须使用“真实”路由器...但有一个间谍变量..并将 router.events 指向我的测试 observable 以便我可以控制导航事件
export type Spied<T> = [Method in keyof T]: jasmine.Spy ;
describe("FooterComponent", () =>
let component: FooterComponent;
let fixture: ComponentFixture<FooterComponent>;
let de: DebugElement;
const routerEvent$ = new BehaviorSubject<RouterEvent>(null);
let router: Spied<Router>;
beforeEach(async(() =>
TestBed.configureTestingModule(
providers: [provideMock(SomeService), provideMock(SomeOtherService)],
declarations: [FooterComponent],
imports: [RouterTestingModule]
).compileComponents();
router = TestBed.get(Router);
(<any>router).events = routerEvent$.asObservable();
fixture = TestBed.createComponent(FooterComponent);
component = fixture.componentInstance;
de = fixture.debugElement;
));
// component subscribes to router nav event...checking url to hide/show a link
it("should return false for showSomeLink()", () =>
routerEvent$.next(new NavigationEnd(1, "/someroute", "/"));
component.ngOnInit();
expect(component.showSomeLink()).toBeFalsy();
fixture.detectChanges();
const htmlComponent = de.query(By.css("#someLinkId"));
expect(htmlComponent).toBeNull();
);
【讨论】:
这行得通,但是连同这个解决方案,我所有基于 Router.navigate(...) 的测试都失败了。你怎么能同时测试 Router.navigate 和 router.event.subscribe() ?【参考方案4】:我看到了这个替代方案,它适用于 RouterTestingModule
,就像 @daniel-sc 的回答一样
const events = new Subject<>();
const router = TestBed.get(Router);
spyOn(router.events, 'pipe').and.returnValue(events);
events.next('Result of pipe');
【讨论】:
get() 已弃用。从 v9.0.0 开始,推荐使用 inject()。【参考方案5】:希望这会有所帮助,这就是我所做的:
const eventsStub = new BehaviorSubject<Event>(null);
export class RouterStub
events = eventsStub;
beforeEach(async(() =>
TestBed.configureTestingModule(
declarations: [AppComponent],
imports: [RouterTestingModule],
providers: [
provide: HttpClient, useValue: ,
provide: Router, useClass: RouterStub ,
Store,
CybermetrieCentralizedService,
TileMenuComponentService,
HttpErrorService
],
schemas: [NO_ERRORS_SCHEMA]
).compileComponents();
fixture = TestBed.createComponent(AppComponent);
router = TestBed.get(Router);
tileMenuService = TestBed.get(TileMenuComponentService);
component = fixture.componentInstance;
));
然后在测试中我这样做了:
it('should close the menu when navigation to dashboard ends', async(() =>
spyOn(tileMenuService.menuOpened, 'next');
component.checkRouterEvents();
router.events.next(new NavigationEnd (1, '/', '/'));
expect(tileMenuService.menuOpened.next).toHaveBeenCalledWith(false);
));
这是为了验证在导航到路由“/”后我执行了预期的指令
【讨论】:
【参考方案6】: 使用ReplaySubject<RouterEvent>
模拟router.events
将其导出到服务中,以便您可以更独立于组件对其进行测试,并且其他组件可能也希望使用此服务;)
使用filter
而不是instanceof
源代码
import Injectable from '@angular/core';
import NavigationEnd, Router, RouterEvent from '@angular/router';
import filter, map from 'rxjs/operators';
import Observable from 'rxjs';
@Injectable(
providedIn: 'root'
)
export class RouteEventService
constructor(private router: Router)
subscribeToRouterEventUrl(): Observable<string>
return this.router.events
.pipe(
filter(event => event instanceof NavigationEnd),
map((event: RouterEvent) => event.url)
);
测试代码
import TestBed from '@angular/core/testing';
import RouteEventService from './route-event.service';
import NavigationEnd, NavigationStart, Router, RouterEvent from '@angular/router';
import Observable, ReplaySubject from 'rxjs';
describe('RouteEventService', () =>
let service: RouteEventService;
let routerEventRelaySubject: ReplaySubject<RouterEvent>;
let routerMock;
beforeEach(() =>
routerEventRelaySubject = new ReplaySubject<RouterEvent>(1);
routerMock =
events: routerEventRelaySubject.asObservable()
;
TestBed.configureTestingModule(
providers: [
provide: Router, useValue: routerMock
]
);
service = TestBed.inject(RouteEventService);
);
it('should be created', () =>
expect(service).toBeTruthy();
);
describe('subscribeToEventUrl should', () =>
it('return route equals to mock url on firing NavigationEnd', () =>
const result: Observable<string> = service.subscribeToRouterEventUrl();
const url = '/mock';
result.subscribe((route: string) =>
expect(route).toEqual(url);
);
routerEventRelaySubject.next(new NavigationEnd(1, url, 'redirectUrl'));
);
it('return route equals to mock url on firing NavigationStart', () =>
const result: Observable<string> = service.subscribeToRouterEventUrl();
const url = '/mock';
result.subscribe((route: string) =>
expect(route).toBeNull();
);
routerEventRelaySubject.next(new NavigationStart(1, url, 'imperative', null));
);
);
);
【讨论】:
以上是关于Angular 6 - 如何在单元测试中模拟 router.events url 响应的主要内容,如果未能解决你的问题,请参考以下文章
在单元测试中使用参数模拟 ngrx 存储选择器(Angular)
Angular 2:如何在单元测试时模拟 ChangeDetectorRef