如何对无类/可注入的NgRx函数式Effect进行单元测试?
测试NgRx函数式Effect的正确方法
问题背景
我需要对一个无类、无装饰器的NgRx函数式Effect进行单元测试,但尝试的测试代码无法触发Effect,请求有效的测试方案。
待测试的函数式Effect代码
export const loadUsers = createEffect( (actions$ = inject(Actions), usersService = inject(UsersService)) => { return actions$.pipe( ofType(userActions.getUser), exhaustMap(() => { return usersService.getAll().pipe( map((users) => userActions.usersLoadedSuccess({ users })), ); }) ); }, { functional: true } );
失败的测试代码(Effect未触发)
import { HttpClientTestingModule } from '@angular/common/http/testing'; import { Observable, of } from 'rxjs'; import { TestBed } from '@angular/core/testing'; import { provideMockActions } from '@ngrx/effects/testing'; import { MockProvider } from 'ng-mocks'; import { Actions } from '@ngrx/effects'; import { userActions } from './user-store'; import { MockStore, provideMockStore } from '@ngrx/store/testing'; import { UsersService } from './users.service'; import { loadUsers } from './user.effects'; describe('Offer Effects', () => { let actions$: Observable<unknown>; let store: MockStore; let userService: UsersService; beforeEach(() => { TestBed.configureTestingModule({ imports: [HttpClientTestingModule], providers: [loadUsers, MockProvider(UsersService), provideMockStore(), provideMockActions(() => actions$)], }); actions$ = TestBed.inject(Actions); store = TestBed.inject(MockStore); userService = TestBed.inject(UsersService); }); it('should test load users effect', async () => { actions$ = of({ type: userActions.getUser, }); const users = { name: 'Test', surname: 'Data' }; jest.spyOn(userService, 'getAll').mockReturnValue(of(users)); const spy = jest.spyOn(userActions, 'usersLoadedSuccess'); await loadUsers; expect(spy).toHaveBeenCalledWith({ users }); }); });
正确的测试方案
原测试的核心问题:
- 错误地将
loadUsers加入TestBed providers,函数式Effect无需依赖注入,可直接调用 - 直接
await loadUsers不会触发Effect流,需要订阅Effect的输出并手动发射action - 多余引入了MockStore(该Effect未依赖store)
修正后的测试代码
import { Observable, of, Subject } from 'rxjs'; import { TestBed } from '@angular/core/testing'; import { provideMockActions } from '@ngrx/effects/testing'; import { MockProvider } from 'ng-mocks'; import { Actions } from '@ngrx/effects'; import { userActions } from './user-store'; import { UsersService } from './users.service'; import { loadUsers } from './user.effects'; import { take } from 'rxjs/operators'; describe('User Effects', () => { let actions$: Subject<unknown>; let userService: UsersService; beforeEach(() => { actions$ = new Subject(); TestBed.configureTestingModule({ providers: [ MockProvider(UsersService), // 使用Subject来控制action发射,替代原Observable provideMockActions(() => actions$) ] }); userService = TestBed.inject(UsersService); }); it('should dispatch usersLoadedSuccess when getUser is triggered', async () => { // 1. 模拟UsersService的返回值 const mockUsers = { name: 'Test', surname: 'Data' }; jest.spyOn(userService, 'getAll').mockReturnValue(of(mockUsers)); // 2. 调用函数式Effect,传入mock的依赖 const effect$ = loadUsers(actions$ as Actions, userService); // 3. 订阅Effect的输出,获取结果 const resultPromise = effect$.pipe(take(1)).toPromise(); // 4. 发射触发action actions$.next(userActions.getUser()); // 5. 断言输出的action是否正确 const emittedAction = await resultPromise; expect(emittedAction).toEqual(userActions.usersLoadedSuccess({ users: mockUsers })); expect(userService.getAll).toHaveBeenCalled(); }); });
关键优化点
- 使用
Subject替代普通Observable,实现手动控制action的发射时机 - 直接调用
loadUsers函数,传入mock的actions$和usersService,无需依赖注入 - 通过订阅Effect的输出流并转换为Promise,等待结果返回后断言
- 移除无关的
HttpClientTestingModule和MockStore,精简测试环境
内容的提问来源于stack exchange,提问作者Rikhil Arora
相关产品推荐
相关产品推荐

