解决单元测试中TypeError: Cannot read properties of undefined (reading 'returnValue')
解决Angular单元测试中
TypeError: Cannot read properties of undefined (reading 'returnValue')问题 问题描述
在Angular组件中定义了loadProducts方法:
loadProducts(): void { this.category = this.activatedRoute.snapshot.paramMap.get('category') as string; this.productService.getAllByCategory(this.category).subscribe(data => { this.userProducts = data; }) }
编写单元测试时出现错误:
TypeError: Cannot read properties of undefined (reading 'returnValue')
测试代码如下:
import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ProductsComponent } from './products.component'; import { HttpClientTestingModule } from '@angular/common/http/testing'; import { RouterTestingModule } from '@angular/router/testing'; import { IProductResponse } from 'src/app/shared/interfaces/IProduct'; import { ProductService } from 'src/app/shared/services/products/product.service'; import { of } from 'rxjs'; describe('ProductsComponent', () => { let mockProductService:any; let component: ProductsComponent; let fixture: ComponentFixture<ProductsComponent>; let PRODUCT: IProductResponse; let PRODUCTS: IProductResponse[]; beforeEach(async () => { mockProductService = jasmine.createSpyObj({'getAllByCategory':of(PRODUCTS)}); PRODUCT = { category: { id: 1, name: 'pizza', path: 'string', imagePath: 'string', }, id: 1, name: 'string', path: 'string', ingredients: 'string', weight: 'string', price: 1, imagePath: 'string', count: 1, }; PRODUCTS =[{ category: { id: 1, name: 'pizza', path: 'string', imagePath: 'string', }, id: 1, name: 'string', path: 'string', ingredients: 'string', weight: 'string', price: 1, imagePath: 'string', count: 1, }, { category: { id: 1, name: 'pizza', path: 'string', imagePath: 'string', }, id: 2, name: 'string', path: 'string', ingredients: 'string', weight: 'string', price: 1, imagePath: 'string', count: 1, }]; await TestBed.configureTestingModule({ imports: [ HttpClientTestingModule, RouterTestingModule ], declarations: [ProductsComponent], providers:[ {provide:ProductService, useValue:mockProductService} ] }) .compileComponents(); fixture = TestBed.createComponent(ProductsComponent); component = fixture.componentInstance; fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); it('should test loadProducts()', ()=>{ let category = 'pizza'; mockProductService.getAllByCategory(category).and.returnValue(of(PRODUCTS)) expect(component.userProducts).toEqual(PRODUCTS); }); });
错误原因
- 创建
mockProductService时,直接给getAllByCategory赋值了of(PRODUCTS),此时该方法不是Jasmine spy对象,而是直接返回Observable实例 - 测试用例中调用
mockProductService.getAllByCategory(category)得到的是Observable,而非spy对象,因此没有.and属性,调用.returnValue时抛出错误
修复后的测试代码
import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ActivatedRoute, ParamMap } from '@angular/router'; import { ProductsComponent } from './products.component'; import { HttpClientTestingModule } from '@angular/common/http/testing'; import { RouterTestingModule } from '@angular/router/testing'; import { IProductResponse } from 'src/app/shared/interfaces/IProduct'; import { ProductService } from 'src/app/shared/services/products/product.service'; import { of } from 'rxjs'; describe('ProductsComponent', () => { let mockProductService: any; let component: ProductsComponent; let fixture: ComponentFixture<ProductsComponent>; let PRODUCTS: IProductResponse[]; beforeEach(async () => { // 1. 创建spyObj时只声明方法名,不提前赋值返回值 mockProductService = jasmine.createSpyObj('ProductService', ['getAllByCategory']); PRODUCTS = [{ category: { id: 1, name: 'pizza', path: 'string', imagePath: 'string', }, id: 1, name: 'string', path: 'string', ingredients: 'string', weight: 'string', price: 1, imagePath: 'string', count: 1, }, { category: { id: 1, name: 'pizza', path: 'string', imagePath: 'string', }, id: 2, name: 'string', path: 'string', ingredients: 'string', weight: 'string', price: 1, imagePath: 'string', count: 1, }]; await TestBed.configureTestingModule({ imports: [ HttpClientTestingModule, RouterTestingModule ], declarations: [ProductsComponent], providers: [ { provide: ProductService, useValue: mockProductService }, // 2. 模拟ActivatedRoute的paramMap { provide: ActivatedRoute, useValue: { snapshot: { paramMap: { get: (key: string) => key === 'category' ? 'pizza' : null } as ParamMap } } } ] }) .compileComponents(); fixture = TestBed.createComponent(ProductsComponent); component = fixture.componentInstance; }); it('should create', () => { expect(component).toBeTruthy(); }); it('should test loadProducts()', () => { // 3. 给spy方法设置返回值 mockProductService.getAllByCategory.and.returnValue(of(PRODUCTS)); // 4. 调用待测试方法 component.loadProducts(); // 5. 检测组件变化,触发subscribe回调 fixture.detectChanges(); // 6. 断言结果 expect(component.userProducts).toEqual(PRODUCTS); // 额外断言:方法是否被正确调用 expect(mockProductService.getAllByCategory).toHaveBeenCalledWith('pizza'); }); });
returnValue的适用场景与禁用情况
适用场景
- 仅用于Jasmine spy对象的方法,包括:
- 通过
jasmine.createSpyObj创建的模拟对象的方法 - 通过
spyOn包装的真实对象的方法
- 通过
- 作用是定义或修改spy方法的返回值,支持同步值、Promise、Observable等类型
不能使用的情况
- 当目标方法不是Jasmine spy时:比如普通类的原生方法、已经直接赋值为实际值的方法
- 当调用方法后得到的是实际返回值(而非spy对象引用)时:比如提前给spy方法赋值了返回值,调用后得到的是Observable/数值等,此时无法访问
.and属性
内容的提问来源于stack exchange,提问作者Vika
相关产品推荐
相关产品推荐

