Angular 20 + Jasmine单元测试报错:TypeError: Cannot read properties of undefined (reading 'root')
Angular 20 + Jasmine单元测试报错:TypeError: Cannot read properties of undefined (reading 'root')
我之前也遇到过几乎一模一样的问题!这个错误本质上是因为你在测试模块里直接用了生产环境的AppRoutingModule,而Angular的测试环境需要专门的路由测试工具来模拟路由系统,不能直接复用生产的路由配置。下面是具体的解决思路和修改方案:
问题根源
当你导入AppRoutingModule时,Angular会初始化真实的路由系统,包括你配置的默认路由。这时候模板里的<router-outlet>会尝试渲染默认路由对应的组件(比如你的登录组件),但这个组件的依赖并没有在测试模块中声明;同时,Router的内部核心依赖(比如root路由节点)在测试环境中没有被正确初始化,最终就抛出了Cannot read properties of undefined (reading 'root')的错误。
解决方案:用RouterTestingModule替代真实路由模块
Angular专门提供了RouterTestingModule用于路由相关的组件测试,它会自动模拟路由系统的底层依赖,不会加载真实的路由组件,完美解决这个问题。
修改后的完整app.spec.ts代码
import { TestBed } from '@angular/core/testing'; import { App } from './app'; import { AuthService } from './services/auth-service'; import { RouterTestingModule } from '@angular/router/testing'; // 导入测试专用路由模块 import { MatToolbarModule } from '@angular/material/toolbar'; import { MatIconModule } from '@angular/material/icon'; import { MatButtonModule } from '@angular/material/button'; describe('App', () => { let authServiceSpy: jasmine.SpyObj<AuthService>; beforeEach(async () => { authServiceSpy = jasmine.createSpyObj('AuthService', ['isLoggedIn', 'logout']); authServiceSpy.isLoggedIn.and.returnValue(false); await TestBed.configureTestingModule({ imports: [ // 替换生产路由模块为测试专用模块,空路由配置避免加载真实组件 RouterTestingModule.withRoutes([]), MatToolbarModule, MatIconModule, MatButtonModule ], declarations: [App], providers: [ { provide: AuthService, useValue: authServiceSpy } // 不需要手动提供Router了!RouterTestingModule已经自动模拟了Router实例 ] }).compileComponents(); }); // 模拟localStorage,避免测试依赖浏览器环境 beforeEach(() => { spyOn(localStorage, 'getItem').and.returnValue('测试用户'); }); it('should create the app', () => { const fixture = TestBed.createComponent(App); const app = fixture.componentInstance; expect(app).toBeTruthy(); }); // 额外加个场景测试:未登录时应该显示注册/登录按钮 it('should display signin and login buttons when not logged in', () => { const fixture = TestBed.createComponent(App); fixture.detectChanges(); // 触发变更检测渲染模板 const compiled = fixture.nativeElement as HTMLElement; expect(compiled.querySelector('a[routerLink="/signin-form"]')).toBeTruthy(); expect(compiled.querySelector('button[click="logout()"]')).toBeFalsy(); }); // 测试登出逻辑:调用authService.logout并跳转到登录页 it('should call logout and navigate to login-form on logout', () => { const fixture = TestBed.createComponent(App); const app = fixture.componentInstance; const router = TestBed.inject(Router); const navigateSpy = spyOn(router, 'navigate'); app.logout(); expect(authServiceSpy.logout).toHaveBeenCalled(); expect(navigateSpy).toHaveBeenCalledWith(['/login-form']); }); });
关键修改点说明
- 替换路由模块:把
AppRoutingModule换成RouterTestingModule.withRoutes([]),这个模块会模拟路由系统的核心功能,不会加载真实的路由组件,从根源上避免了root属性未定义的错误。 - 移除手动Router provider:
RouterTestingModule已经自动提供了模拟的Router实例,不需要自己再创建spy对象(如果需要测试导航行为,直接从TestBed中注入Router即可)。 - 模拟localStorage:通过spy拦截
localStorage.getItem,让测试不依赖浏览器的本地存储,结果更可控。 - 补充场景测试:增加了未登录状态的UI验证和登出逻辑的功能测试,让测试覆盖更全面。
这样修改后,你的测试应该就能正常运行了,而且测试逻辑也更符合Angular单元测试的最佳实践~




