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

如何测试使用Redux的Angular(5) SPA组件(单元测试)?

嘿,我来帮你搞定Angular 5结合Redux的服务单元测试!针对你提到的getTodos()方法,咱们先把完整的服务代码补全(你之前的代码没写完,我补充了常见的成功/错误处理逻辑),再一步步拆解怎么写靠谱的单元测试。


先补全todo.service.ts的完整代码

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { NgRedux } from '@angular-redux/store';
import { SHOW_LOADER, HIDE_LOADER, SET_TODOS, FETCH_TODOS_ERROR } from './action-types';

@Injectable()
export class TodoService {
  constructor(private http: HttpClient, private ngRedux: NgRedux<any>) {}

  getTodos(): Observable<any> {
    this.ngRedux.dispatch({ 
      type: SHOW_LOADER, 
      message: "Loading TODOs ..." 
    });
    
    let obs = this.http.get<Array<any>>("api/SampleData/GetTodoList");
    
    obs.subscribe(
      result => {
        console.log("Got todos:", result);
        this.ngRedux.dispatch({
          type: SET_TODOS,
          payload: result
        });
        this.ngRedux.dispatch({ type: HIDE_LOADER });
      },
      error => {
        console.error("Failed to fetch todos:", error);
        this.ngRedux.dispatch({
          type: FETCH_TODOS_ERROR,
          payload: error.message
        });
        this.ngRedux.dispatch({ type: HIDE_LOADER });
      }
    );
    
    return obs;
  }
}

单元测试核心思路

我们需要验证getTodos()的几个关键行为:

  • 调用时是否正确触发SHOW_LOADER action
  • 是否发起了正确的HTTP GET请求
  • 请求成功时,是否依次触发SET_TODOS和HIDE_LOADER
  • 请求失败时,是否触发FETCH_TODOS_ERROR和HIDE_LOADER
  • 是否正确返回HTTP请求的Observable

完整的测试文件todo.service.spec.ts

import { TestBed } from '@angular/core/testing';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { NgRedux } from '@angular-redux/store';
import { TodoService } from './todo.service';
import { SHOW_LOADER, HIDE_LOADER, SET_TODOS, FETCH_TODOS_ERROR } from './action-types';

// 手动Mock NgRedux,避免依赖真实Store
class MockNgRedux {
  dispatch = jasmine.createSpy('dispatch');
}

describe('TodoService', () => {
  let service: TodoService;
  let httpMock: HttpTestingController;
  let ngReduxMock: MockNgRedux;

  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [HttpClientTestingModule],
      providers: [
        TodoService,
        { provide: NgRedux, useClass: MockNgRedux }
      ]
    });

    service = TestBed.inject(TodoService);
    httpMock = TestBed.inject(HttpTestingController);
    ngReduxMock = TestBed.inject(NgRedux) as unknown as MockNgRedux;
  });

  afterEach(() => {
    // 确保所有HTTP请求都被处理,防止内存泄漏
    httpMock.verify();
    // 重置dispatch的调用记录,保证测试间互不干扰
    ngReduxMock.dispatch.calls.reset();
  });

  it('should be created', () => {
    expect(service).toBeTruthy();
  });

  it('should dispatch SHOW_LOADER and send GET request on getTodos() call', () => {
    // 调用目标方法
    service.getTodos();

    // 验证SHOW_LOADER是否被正确触发
    expect(ngReduxMock.dispatch).toHaveBeenCalledWith({
      type: SHOW_LOADER,
      message: "Loading TODOs ..."
    });

    // 捕获HTTP请求,验证URL和请求方法
    const req = httpMock.expectOne('api/SampleData/GetTodoList');
    expect(req.request.method).toBe('GET');
  });

  it('should dispatch SET_TODOS and HIDE_LOADER when request succeeds', () => {
    const mockTodos = [{ id: 1, title: 'Finish unit test' }, { id: 2, title: 'Review code' }];

    // 调用方法并订阅结果
    service.getTodos().subscribe(todos => {
      expect(todos).toEqual(mockTodos);
    });

    // 模拟后端成功响应
    const req = httpMock.expectOne('api/SampleData/GetTodoList');
    req.flush(mockTodos);

    // 验证actions的触发顺序和内容
    expect(ngReduxMock.dispatch).toHaveBeenCalledTimes(3);
    expect(ngReduxMock.dispatch.calls.argsFor(0)).toEqual([{
      type: SHOW_LOADER,
      message: "Loading TODOs ..."
    }]);
    expect(ngReduxMock.dispatch.calls.argsFor(1)).toEqual([{
      type: SET_TODOS,
      payload: mockTodos
    }]);
    expect(ngReduxMock.dispatch.calls.argsFor(2)).toEqual([{ type: HIDE_LOADER }]);
  });

  it('should dispatch FETCH_TODOS_ERROR and HIDE_LOADER when request fails', () => {
    const mockErrorMsg = 'Internal Server Error';

    // 调用方法并订阅错误回调
    service.getTodos().subscribe({
      error: err => {
        expect(err.error).toBe(mockErrorMsg);
        expect(err.status).toBe(500);
      }
    });

    // 模拟后端错误响应
    const req = httpMock.expectOne('api/SampleData/GetTodoList');
    req.flush(mockErrorMsg, { status: 500, statusText: 'Server Error' });

    // 验证错误场景下的actions
    expect(ngReduxMock.dispatch).toHaveBeenCalledTimes(3);
    expect(ngReduxMock.dispatch.calls.argsFor(0)).toEqual([{
      type: SHOW_LOADER,
      message: "Loading TODOs ..."
    }]);
    expect(ngReduxMock.dispatch.calls.argsFor(1)).toEqual([{
      type: FETCH_TODOS_ERROR,
      payload: mockErrorMsg
    }]);
    expect(ngReduxMock.dispatch.calls.argsFor(2)).toEqual([{ type: HIDE_LOADER }]);
  });
});

关键测试细节说明

  1. Mock NgRedux:我们用自定义的MockNgRedux类替代真实的Redux Store,用Jasmine的spy跟踪dispatch方法的调用,完全隔离Redux依赖。
  2. HttpClientTestingModule:Angular官方提供的测试模块,用来模拟HTTP请求,避免实际调用后端API,保证测试的独立性。
  3. 测试覆盖:覆盖了服务初始化、正常请求、错误请求三个核心场景,确保所有关键逻辑都被验证。

内容的提问来源于stack exchange,提问作者Alexei - check Codidact

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.25 07:02:54