Angular组件单元测试报错:无法读取undefined的paramMap属性
Angular单元测试报错:TypeError: Cannot read properties of undefined (reading 'paramMap')
报错信息
TypeError: Cannot read properties of undefined (reading 'paramMap')
组件代码(product-list.component.ts)
import { Component, OnInit } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; import { CartItem } from 'src/app/common/cart-item'; import { Product } from 'src/app/common/product'; import { AuthService } from 'src/app/services/auth.service'; import { CartService } from 'src/app/services/cart.service'; import { ProductService } from 'src/app/services/product.service'; @Component({ selector: 'app-product-list', templateUrl: './product-list.component.html', styleUrls: ['./product-list.component.css'] }) export class ProductListComponent implements OnInit { products: Product[] = []; currentCategoryId: number = 0; previousCategoryId: number = 1; currentCategoryName: String | null = null; searchMode: boolean = false; pageNumber: number = 1; pageSize: number = 10; totalElements: number = 10; isAdmin: boolean = false; constructor( private productService: ProductService, private route: ActivatedRoute, private cartService: CartService, private router: Router, private authService: AuthService ) { } ngOnInit(): void { this.route.paramMap.subscribe(() => { this.productsList(); }); this.authService.isAdmin.subscribe((res) => { this.isAdmin = res; }) } productsList() { this.searchMode = this.route.snapshot.paramMap.has("keyword"); if (this.searchMode) { this.handleSearchProductsList(); } else { this.handleProductsList(); } } handleSearchProductsList() { const keyword: string = this.route.snapshot.paramMap.get("keyword")!; this.productService.getSearchProducts(keyword).subscribe((res) => { this.products = res; }); } handleProductsList() { const hasCategoryId: boolean = this.route.snapshot.paramMap.has("id"); if (hasCategoryId) { this.currentCategoryId = +this.route.snapshot.paramMap.get("id")!; this.currentCategoryName = this.route.snapshot.paramMap.get("name"); this.productService.getProductListByCategoryId(this.currentCategoryId).subscribe((res) => { this.products = res; }) } else { this.productService.getProductList().subscribe((res) => { this.products = res; }) } } addToCart(product: Product) { this.cartService.addToCart(new CartItem(product)) } deleteProduct(product: Product) { this.productService.deleteProduct(product.id).subscribe((res) => { if (res.success) { this.handleProductsList() } }) } }
修复后的测试代码(product-list.component.spec.ts)
import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ActivatedRoute, convertToParamMap } from '@angular/router'; import { of, Subject } from 'rxjs'; import { Product } from 'src/app/common/product'; import { AuthService } from 'src/app/services/auth.service'; import { ProductService } from 'src/app/services/product.service'; import { ProductListComponent } from './product-list.component'; describe('ProductListComponent', () => { let PRODUCTS: Product[]; let mockProductService: any; let mockAuthService: any; let mockActivatedRoute: any; let component: ProductListComponent; let fixture: ComponentFixture<ProductListComponent>; beforeEach(async () => { // 先初始化所有mock对象 mockAuthService = jasmine.createSpyObj('AuthService', [], { isAdmin: new Subject() }); mockProductService = jasmine.createSpyObj('ProductService', ['getProductList', 'getProductListByCategoryId', 'getSearchProducts', 'deleteProduct']); // 完整模拟ActivatedRoute的paramMap订阅和snapshot参数 mockActivatedRoute = { paramMap: of(convertToParamMap({})), snapshot: { paramMap: convertToParamMap({}) } }; PRODUCTS = [ { id: 1, sku: "test1", name: "test1", description: "test1", unitPrice: 1, imageUrl: "test1", active: true, unitsInStock: 1, category: 1, dateCreated: new Date(), lastUpdated: new Date() }, { id: 2, sku: "test2", name: "test2", description: "test2", unitPrice: 2, imageUrl: "test2", active: true, unitsInStock: 2, category: 2, dateCreated: new Date(), lastUpdated: new Date() } ]; // 配置TestBed,传入已初始化的mock对象 await TestBed.configureTestingModule({ declarations: [ ProductListComponent ], providers: [ { provide: ProductService, useValue: mockProductService }, { provide: ActivatedRoute, useValue: mockActivatedRoute }, { provide: AuthService, useValue: mockAuthService } ] }).compileComponents(); fixture = TestBed.createComponent(ProductListComponent); component = fixture.componentInstance; mockAuthService.isAdmin.next(false); fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); expect(component.isAdmin).toBe(false); }); it("should call handleProductsList after deleting product successfully", () => { spyOn(component, 'handleProductsList'); component.products = PRODUCTS; mockProductService.deleteProduct.and.returnValue( of({ success: true, status: 200, message:"your product has been deleted" }) ); component.deleteProduct(PRODUCTS[0]); expect(component.handleProductsList).toHaveBeenCalled(); }); });
核心修复要点
- 调整mock初始化顺序:原代码先配置TestBed再创建mock,导致注入的
ActivatedRoute为undefined,必须先初始化mock再传入TestBed的providers。 - 完整模拟ActivatedRoute:组件同时使用了
route.paramMap.subscribe()和route.snapshot.paramMap,需用convertToParamMap创建符合Angular规范的ParamMap对象,同时模拟两个属性。 - 补充必要的spy方法:添加
getProductListByCategoryId和getSearchProducts的spy,避免组件调用时出现未定义错误;删除测试中需手动监听handleProductsList方法调用。
内容的提问来源于stack exchange,提问作者kyubei
相关产品推荐
相关产品推荐

