You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

解决单元测试中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);

});
});

错误原因

  1. 创建mockProductService时,直接给getAllByCategory赋值了of(PRODUCTS),此时该方法不是Jasmine spy对象,而是直接返回Observable实例
  2. 测试用例中调用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.30 19:09:30