You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

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']);
  });
});

关键修改点说明

  1. 替换路由模块:把AppRoutingModule换成RouterTestingModule.withRoutes([]),这个模块会模拟路由系统的核心功能,不会加载真实的路由组件,从根源上避免了root属性未定义的错误。
  2. 移除手动Router providerRouterTestingModule已经自动提供了模拟的Router实例,不需要自己再创建spy对象(如果需要测试导航行为,直接从TestBed中注入Router即可)。
  3. 模拟localStorage:通过spy拦截localStorage.getItem,让测试不依赖浏览器的本地存储,结果更可控。
  4. 补充场景测试:增加了未登录状态的UI验证和登出逻辑的功能测试,让测试覆盖更全面。

这样修改后,你的测试应该就能正常运行了,而且测试逻辑也更符合Angular单元测试的最佳实践~

火山引擎 最新活动