首页
/ NgRx SignalStore 测试指南:从基础到高级实践

NgRx SignalStore 测试指南:从基础到高级实践

2025-05-28 02:15:43作者:咎岭娴Homer

前言

在 Angular 状态管理领域,NgRx 一直是最受欢迎的解决方案之一。随着 Angular 16 引入 Signals 特性,NgRx 团队推出了 SignalStore 这一全新状态管理方案,它充分利用了 Signals 的响应式特性。本文将全面介绍如何为 SignalStore 编写有效的测试用例,涵盖从基础到高级的各种测试场景。

SignalStore 测试基础

无依赖测试

对于不依赖外部服务的简单 SignalStore,我们可以直接创建实例进行测试:

import { signalStore, withState } from '@ngrx/signals';

const CounterStore = signalStore(
  withState({ count: 0 })
);

describe('CounterStore', () => {
  it('should initialize with count 0', () => {
    const store = new CounterStore();
    expect(store.count()).toBe(0);
  });
});

这种测试方式简单直接,适合验证 Store 的初始状态和基础功能。

使用 TestBed 测试

大多数情况下,SignalStore 会依赖其他服务,这时我们需要使用 Angular 的测试工具 TestBed:

import { TestBed } from '@angular/core/testing';
import { signalStore, withState, withMethods } from '@ngrx/signals';

const AuthStore = signalStore(
  withState({ user: null }),
  withMethods(({ $user }) => ({
    login: (user) => patchState($user, user)
  }))
);

describe('AuthStore', () => {
  let store: AuthStore;

  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [AuthStore]
    });
    store = TestBed.inject(AuthStore);
  });

  it('should update user on login', () => {
    const testUser = { name: 'Test User' };
    store.login(testUser);
    expect(store.user()).toEqual(testUser);
  });
});

依赖注入与模拟

模拟服务依赖

当 Store 依赖外部服务时,我们需要模拟这些服务:

import { inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';

const DataStore = signalStore(
  withState({ data: null }),
  withMethods((store, http = inject(HttpClient)) => ({
    loadData: () => {
      http.get('/api/data').subscribe(data => {
        patchState(store, { data });
      });
    }
  }))
);

describe('DataStore', () => {
  let store: DataStore;
  let httpMock: jest.Mocked<HttpClient>;

  beforeEach(() => {
    httpMock = {
      get: jest.fn()
    } as any;

    TestBed.configureTestingModule({
      providers: [
        DataStore,
        { provide: HttpClient, useValue: httpMock }
      ]
    });
    
    store = TestBed.inject(DataStore);
  });

  it('should load data from API', () => {
    const testData = { id: 1 };
    httpMock.get.mockReturnValue(of(testData));
    
    store.loadData();
    
    expect(httpMock.get).toHaveBeenCalledWith('/api/data');
    expect(store.data()).toEqual(testData);
  });
});

测试 rxMethod

rxMethod 是 SignalStore 中处理异步操作的重要特性,测试时需要特别注意:

import { rxMethod } from '@ngrx/signals';
import { of } from 'rxjs';

const SearchStore = signalStore(
  withState({ results: [] }),
  withMethods((store) => ({
    search: rxMethod<string>((query$) => {
      return query$.pipe(
        switchMap(query => 
          query ? http.get(`/search?q=${query}`) : of([])
        ),
        tap(results => patchState(store, { results }))
      );
    }))
  })
);

describe('SearchStore', () => {
  let store: SearchStore;
  let httpMock: jest.Mocked<HttpClient>;

  beforeEach(() => {
    httpMock = {
      get: jest.fn()
    } as any;

    TestBed.configureTestingModule({
      providers: [
        SearchStore,
        { provide: HttpClient, useValue: httpMock }
      ]
    });
    
    store = TestBed.inject(SearchStore);
  });

  it('should perform search', () => {
    const testResults = [{ id: 1 }];
    httpMock.get.mockReturnValue(of(testResults));
    
    store.search('test');
    
    expect(httpMock.get).toHaveBeenCalledWith('/search?q=test');
    expect(store.results()).toEqual(testResults);
  });

  it('should handle empty query', () => {
    store.search('');
    expect(store.results()).toEqual([]);
    expect(httpMock.get).not.toHaveBeenCalled();
  });
});

高级测试技巧

测试自定义 Store 特性

对于复杂的自定义 Store 特性,我们可以单独测试其行为:

function withLogger() {
  return (store: SignalStore) => {
    const actions = new Subject<string>();
    
    return {
      ...store,
      logAction: (action: string) => actions.next(action),
      actions$: actions.asObservable()
    };
  };
}

describe('withLogger', () => {
  it('should log actions', () => {
    const TestStore = signalStore(withLogger());
    const store = new TestStore();
    
    const spy = jest.fn();
    store.actions$.subscribe(spy);
    
    store.logAction('test');
    
    expect(spy).toHaveBeenCalledWith('test');
  });
});

测试计算属性

计算属性(computed)是 SignalStore 的重要特性,测试时需要注意其惰性求值特性:

const CartStore = signalStore(
  withState({ items: [] }),
  withComputed(({ items }) => ({
    total: computed(() => 
      items().reduce((sum, item) => sum + item.price, 0)
    )
  }))
);

describe('CartStore', () => {
  it('should calculate total', () => {
    const store = new CartStore();
    expect(store.total()).toBe(0);
    
    patchState(store, { 
      items: [{ price: 10 }, { price: 20 }] 
    });
    
    // 必须访问计算属性才会触发计算
    expect(store.total()).toBe(30);
  });
});

测试最佳实践

  1. 隔离测试:尽量将业务逻辑与状态管理分离,使得 Store 主要处理状态变更,业务逻辑由服务处理。

  2. 小范围测试:每个测试用例只验证一个特定行为,保持测试简洁明确。

  3. 状态验证:测试 Store 时,重点验证状态变更是否符合预期,而不是实现细节。

  4. 异步处理:对于涉及异步操作的测试,确保使用适当的工具(如 fakeAsync 或 waitForAsync)处理异步行为。

  5. 类型安全:充分利用 TypeScript 的类型系统,确保测试代码也能享受类型检查的好处。

结语

SignalStore 作为 NgRx 的新成员,为 Angular 应用状态管理带来了更简洁、更响应式的解决方案。通过本文介绍的各种测试方法,开发者可以确保 SignalStore 在各种场景下都能可靠工作。随着 SignalStore 的不断演进,测试方法也将持续完善,建议开发者关注 NgRx 官方文档获取最新测试实践。

登录后查看全文
热门项目推荐
相关项目推荐