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

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

2025-05-28 13:06:29作者:咎岭娴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 官方文档获取最新测试实践。

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

热门内容推荐

最新内容推荐

项目优选

收起
ohos_react_nativeohos_react_native
React Native鸿蒙化仓库
C++
178
262
RuoYi-Vue3RuoYi-Vue3
🎉 (RuoYi)官方仓库 基于SpringBoot,Spring Security,JWT,Vue3 & Vite、Element Plus 的前后端分离权限管理系统
Vue
866
513
ShopXO开源商城ShopXO开源商城
🔥🔥🔥ShopXO企业级免费开源商城系统,可视化DIY拖拽装修、包含PC、H5、多端小程序(微信+支付宝+百度+头条&抖音+QQ+快手)、APP、多仓库、多商户、多门店、IM客服、进销存,遵循MIT开源协议发布、基于ThinkPHP8框架研发
JavaScript
93
15
openGauss-serveropenGauss-server
openGauss kernel ~ openGauss is an open source relational database management system
C++
129
183
openHiTLSopenHiTLS
旨在打造算法先进、性能卓越、高效敏捷、安全可靠的密码套件,通过轻量级、可剪裁的软件技术架构满足各行业不同场景的多样化要求,让密码技术应用更简单,同时探索后量子等先进算法创新实践,构建密码前沿技术底座!
C
261
302
kernelkernel
deepin linux kernel
C
22
5
cherry-studiocherry-studio
🍒 Cherry Studio 是一款支持多个 LLM 提供商的桌面客户端
TypeScript
598
57
CangjieCommunityCangjieCommunity
为仓颉编程语言开发者打造活跃、开放、高质量的社区环境
Markdown
1.07 K
0
HarmonyOS-ExamplesHarmonyOS-Examples
本仓将收集和展示仓颉鸿蒙应用示例代码,欢迎大家投稿,在仓颉鸿蒙社区展现你的妙趣设计!
Cangjie
398
371
Cangjie-ExamplesCangjie-Examples
本仓将收集和展示高质量的仓颉示例代码,欢迎大家投稿,让全世界看到您的妙趣设计,也让更多人通过您的编码理解和喜爱仓颉语言。
Cangjie
332
1.08 K