如何为监听Angular router.events的changeMetaTags方法编写Jasmine单元测试
前置准备
我们只需要对方法依赖的四个服务做模拟即可,不需要引入真实的路由、翻译服务实现,核心是验证逻辑交互是否符合预期:
- 用Subject模拟Router的events事件流
- 模拟嵌套层级的ActivatedRoute子路由结构
- 模拟TranslateService的翻译查询接口和语言切换事件流
- 用spy监听SeoService的updateTitle调用情况
完整测试代码
import { TestBed } from '@angular/core/testing'; import { Router, NavigationEnd, ActivatedRoute } from '@angular/router'; import { TranslateService, LangChangeEvent } from '@ngx-translate/core'; import { of, Subject } from 'rxjs'; // 替换为你项目中对应的组件、服务路径 import { YourComponent } from './your.component'; import { SeoService } from './seo.service'; class MockSeoService { updateTitle = jasmine.createSpy('updateTitle'); } describe('YourComponent', () => { let component: YourComponent; let routerEventsSubject: Subject<any>; let langChangeSubject: Subject<LangChangeEvent>; let activatedRoute: any; let translateService: jasmine.SpyObj<TranslateService>; let seoService: SeoService; beforeEach(() => { routerEventsSubject = new Subject(); langChangeSubject = new Subject(); TestBed.configureTestingModule({ providers: [ YourComponent, { provide: Router, useValue: { events: routerEventsSubject.asObservable() } }, { provide: ActivatedRoute, useValue: {} }, { provide: TranslateService, useValue: { get: jasmine.createSpy('get').and.returnValue(of('翻译后的标题')), onLangChange: langChangeSubject.asObservable() } }, { provide: SeoService, useClass: MockSeoService } ] }); component = TestBed.inject(YourComponent); activatedRoute = TestBed.inject(ActivatedRoute) as any; translateService = TestBed.inject(TranslateService) as jasmine.SpyObj<TranslateService>; seoService = TestBed.inject(SeoService); }); describe('changeMetaTags', () => { it('导航到携带title元数据的最深层路由时,应调用翻译服务并更新SEO标题', () => { // 模拟嵌套子路由结构,最深层路由携带title元数据 activatedRoute.firstChild = { firstChild: { snapshot: { data: { title: 'PAGE_TITLE' } }, firstChild: null } }; component.changeMetaTags(); // 触发导航结束事件 routerEventsSubject.next(new NavigationEnd(1, '/test', '/test')); expect(translateService.get).toHaveBeenCalledWith('PAGE_TITLE'); expect(seoService.updateTitle).toHaveBeenCalledWith('翻译后的标题'); }); it('导航到无title元数据的路由时,不应调用SEO标题更新方法', () => { activatedRoute.firstChild = { snapshot: { data: { otherParam: 'value' } }, firstChild: null }; component.changeMetaTags(); routerEventsSubject.next(new NavigationEnd(1, '/test', '/test')); expect(translateService.get).not.toHaveBeenCalled(); expect(seoService.updateTitle).not.toHaveBeenCalled(); }); it('导航到无data元数据的路由时,不应调用SEO标题更新方法', () => { activatedRoute.firstChild = { snapshot: {}, firstChild: null }; component.changeMetaTags(); routerEventsSubject.next(new NavigationEnd(1, '/test', '/test')); expect(translateService.get).not.toHaveBeenCalled(); expect(seoService.updateTitle).not.toHaveBeenCalled(); }); it('无激活子路由时,不应调用SEO标题更新方法', () => { activatedRoute.firstChild = null; component.changeMetaTags(); routerEventsSubject.next(new NavigationEnd(1, '/test', '/test')); expect(translateService.get).not.toHaveBeenCalled(); expect(seoService.updateTitle).not.toHaveBeenCalled(); }); it('语言切换时,如果当前路由存在title元数据,应重新获取对应语言的标题并更新', () => { // 先触发一次导航,存储携带title的路由元数据 activatedRoute.firstChild = { snapshot: { data: { title: 'PAGE_TITLE' } }, firstChild: null }; component.changeMetaTags(); routerEventsSubject.next(new NavigationEnd(1, '/test', '/test')); // 重置调用记录,单独验证语言切换逻辑 translateService.get.calls.reset(); seoService.updateTitle.calls.reset(); // 修改翻译返回值,模拟切换语言后的结果 translateService.get.and.returnValue(of('切换语言后的标题')); // 触发语言切换事件 langChangeSubject.next({ lang: 'zh', translations: {} } as LangChangeEvent); expect(translateService.get).toHaveBeenCalledWith('PAGE_TITLE'); expect(seoService.updateTitle).toHaveBeenCalledWith('切换语言后的标题'); }); it('语言切换时,如果当前路由无title元数据,不应调用SEO标题更新方法', () => { // 先触发一次导航,存储无title的路由元数据 activatedRoute.firstChild = { snapshot: { data: {} }, firstChild: null }; component.changeMetaTags(); routerEventsSubject.next(new NavigationEnd(1, '/test', '/test')); // 触发语言切换事件 langChangeSubject.next({ lang: 'en', translations: {} } as LangChangeEvent); expect(translateService.get).not.toHaveBeenCalled(); expect(seoService.updateTitle).not.toHaveBeenCalled(); }); }); });
使用说明
- 替换代码中
YourComponent、SeoService的路径和类名为你项目中的实际值 - 如果你使用的不是
@ngx-translate/core翻译库,对应调整TranslateService的模拟逻辑即可 - 所有分支逻辑均已覆盖:嵌套路由遍历、有无title元数据、语言切换正反场景
内容的提问来源于stack exchange,提问作者Nikunj Mochi
相关产品推荐
相关产品推荐

