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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.01 19:23:26