Functional Core, Imperative Shell:少域逻辑多副作用场景的重构与测试方案
领域逻辑少、副作用多场景下的重构与单元测试方案——以createOrder为例
原函数的问题很明显:业务判断和API调用完全绑在一起,测试时得Mock所有外部依赖,逻辑藏在副作用里,改起来费劲。咱们用Functional Core + Imperative Shell的思路拆解开,既能降低测试复杂度,也能提升代码可维护性。
1. 先把决策逻辑抽成纯函数(Functional Core)
把判断用户/地址该创建还是更新的逻辑单独拎出来,这些函数只处理数据,不碰任何外部API,属于纯函数——输入固定,输出就固定,完全不用Mock就能测试。
示例代码:
// 纯函数:判断用户的操作类型 function determineUserAction(userInfo, userExists) { return userExists ? { type: 'UPDATE', payload: userInfo } : { type: 'CREATE', payload: userInfo }; } // 纯函数:判断地址的操作类型 function determineAddressAction(addressInfo, addressExists) { return addressExists ? { type: 'UPDATE', payload: addressInfo } : { type: 'CREATE', payload: addressInfo }; }
比如测试用户存在时返回UPDATE,不存在返回CREATE,直接给输入断言输出就行,简单得很。
2. 把副作用封装成Shell层(Imperative Shell)
原来的createOrder改成只负责协调:先调用API拿外部状态,再用纯函数做决策,最后根据决策调用对应的API。同时用依赖注入把外部API传进来,方便测试时替换成模拟对象。
重构后的代码:
class OrderService { constructor(userApi, userAddressApi, orderApi) { // 构造函数注入依赖,测试时能轻松替换成模拟实现 this.userApi = userApi; this.userAddressApi = userAddressApi; this.orderApi = orderApi; } createOrder(userInfo, addressInfo, orderInfo) { // 第一步:获取外部状态(副作用操作) const userExists = this.userApi.findUser(userInfo.email); const addressExists = this.userAddressApi.findAddress(addressInfo); // 第二步:用纯函数做决策(无副作用) const userAction = determineUserAction(userInfo, userExists); const addressAction = determineAddressAction(addressInfo, addressExists); // 第三步:根据决策执行对应副作用 if (userAction.type === 'CREATE') { this.userApi.createUser(userAction.payload); } else { this.userApi.updateUser(userAction.payload); } if (addressAction.type === 'CREATE') { this.userAddressApi.createUserAddress(userInfo.email, addressAction.payload); } else { this.userAddressApi.updateUserAddress(userInfo.email, addressAction.payload); } this.orderApi.create(orderInfo); } }
3. 单元测试怎么写?
测试纯函数(Functional Core)
直接给固定输入,断言输出是否符合预期,完全不需要Mock:
// 测试determineUserAction test('用户存在时返回UPDATE操作', () => { const userInfo = { email: 'test@example.com' }; const action = determineUserAction(userInfo, true); expect(action.type).toBe('UPDATE'); expect(action.payload).toEqual(userInfo); }); test('用户不存在时返回CREATE操作', () => { const userInfo = { email: 'test@example.com' }; const action = determineUserAction(userInfo, false); expect(action.type).toBe('CREATE'); expect(action.payload).toEqual(userInfo); });
测试Shell层(协调逻辑)
这里只需要验证Shell层是否正确根据决策调用对应的API,不用测试API本身的逻辑(那是集成测试的事)。用模拟对象追踪调用情况就行:
test('createOrder会根据用户/地址状态执行对应操作', () => { // 创建模拟API,模拟返回值和追踪调用 const mockUserApi = { findUser: jest.fn().mockReturnValue(true), // 模拟用户已存在 createUser: jest.fn(), updateUser: jest.fn() }; const mockAddressApi = { findAddress: jest.fn().mockReturnValue(false), // 模拟地址不存在 createUserAddress: jest.fn(), updateUserAddress: jest.fn() }; const mockOrderApi = { create: jest.fn() }; // 实例化服务 const orderService = new OrderService(mockUserApi, mockAddressApi, mockOrderApi); const userInfo = { email: 'test@example.com' }; const addressInfo = { street: 'Main St' }; const orderInfo = { items: ['item1'] }; // 执行方法 orderService.createOrder(userInfo, addressInfo, orderInfo); // 断言调用是否符合预期 expect(mockUserApi.updateUser).toHaveBeenCalledWith(userInfo); expect(mockUserApi.createUser).not.toHaveBeenCalled(); expect(mockAddressApi.createUserAddress).toHaveBeenCalledWith(userInfo.email, addressInfo); expect(mockAddressApi.updateUserAddress).not.toHaveBeenCalled(); expect(mockOrderApi.create).toHaveBeenCalledWith(orderInfo); });
额外优化建议
- 如果后续业务逻辑变复杂,纯函数可以加更多逻辑,比如用户信息校验、地址格式验证,这些都能轻松写单元测试。
- 还可以把处理用户/地址操作的逻辑再拆成
handleUserAction、handleAddressAction这类小方法,让createOrder更简洁,也方便单独测试这些小协调逻辑。
内容的提问来源于stack exchange,提问作者Gnu
相关产品推荐
相关产品推荐

