【问题标题】:Unit testing "glue" code or code without any real logic单元测试“粘合”代码或没有任何真实逻辑的代码
【发布时间】:2012-10-12 13:56:22
【问题描述】:

我的问题将是一个简化的例子(Java):

public class VehicleRepository {
  // DAOs responsible for fetching objects from data store (injected)
  private IChassisDAO chassisDAO;
  private IEngineDAO engineDAO;
  private IWheelDAO wheelDAO;
  private IAccessoryDAO accessoryDAO;

  public Vehicle getLuxuryCar() {
    VehicleBuilder builder = getVehicleBuilder();
    builder.setChassis(chassisDAO.findBySize(ChassisSize.NORMAL));
    builder.setEngine(engineDAO.findByPower(EnginePower.HIGH));
    builder.setWheelTemplate(wheelDAO.findBySize(WheelSize.NORMAL));
    builder.setAccessories(accessoryDAO.findByType(Accessory.LUXURY));
    return builder.build();
  }

  public Vehicle getOffroadCar() {
    VehicleBuilder builder = getVehicleBuilder();
    builder.setChassis(chassisDAO.findBySize(ChassisSize.LARGE));
    builder.setEngine(engineDAO.findByPower(EnginePower.HIGH));
    builder.setWheelTemplate(wheelDAO.findBySize(WheelSize.LARGE));
    return builder.build();
  }

  // Similar operations ...
}

你会如何对这个类进行单元测试? 我对此进行了一番思考,并确定了一些选择:

在不模拟构建器的情况下针对返回的车辆进行测试
从接口的角度来看,这是最好的选择,但它不会是 VehicleRepository 上的独立单元测试。与仅测试 VehicleBuilder 相比,我看不到将 VehicleRepository 与 VehicleBuilder 一起测试的任何真正优势。 VehicleBuilder 还可能具有复杂的逻辑(例如检查存在哪些附件),这会导致多个执行路径,这会使 VehicleRepository 的测试变得非常复杂。

重构代码以测试构建器的状态
例如,将 getLuxuryCar 操作拆分为两个操作:

public Vehicle getLuxuryCar() {
  VehicleBuilder builder = getLuxuryCarBuilder();
  return builder.build();
}

// Visible for testing in the same package
protected VehicleBuilder getLuxuryCarBuilder() {
  VehicleBuilder builder = getVehicleBuilder();
  builder.setChassis(chassisDAO.findBySize(ChassisSize.NORMAL));
  builder.setEngine(engineDAO.findByPower(EnginePower.HIGH));
  builder.setWheelTemplate(wheelDAO.findBySize(WheelSize.NORMAL));
  builder.addAccessory(accessoryDAO.findByType(Accessory.LUXURY));
  return builder;
}

我更喜欢状态测试而不是行为测试,而且测试可能会更有弹性,但我不太喜欢生成的代码(尤其是 getLuxuryCar() 方法,它并没有真正做任何事情)。

模拟 VehicleBuilder 并测试其行为
我相信这样的单元测试只是被测试的实际操作的复制品,不会增加任何真正的好处。它也将非常脆弱,并且完全依赖于被测试操作的内部结构。一个可能的单元测试(使用 JMock)是这样的:

@Test
public void shouldBuildLuxuryCar() {
  context.checking(new Expectations() {{
    oneOf(chassisDAOmock).findBySize(ChassisSize.NORMAL);
    oneOf(vehicleBuilderMock).setChassis(with(any(Chassis.class)));

    oneOf(engineDAOmock).findByPower(EnginePower.HIGH);
    oneOf(vehicleBuilderMock).setEngine(with(any(Engine.class)));

    oneOf(wheelDAOmock).findBySize(WheelSize.NORMAL));
    oneOf(vehicleBuilderMock).setWheelTemplate(with(any(Wheel.class)));

    oneOf(accessoryDAOmock).findByType(Accessory.LUXURY));
    oneOf(vehicleBuilderMock).setAccessories(with(any(Set.class)));

    oneOf(vehicleBuilderMock).build();
  }});

  context.assertIsSatisfied();
}

不要对其进行单元测试
我倾向于这个选项,因为我对任何替代方案都不满意,而且操作似乎不需要任何测试。为了澄清,我仍将单独对 VehicleBuilder 进行单元测试,而不是对 VehicleRepository 进行单元测试。然而,这似乎不是普遍的观点,例如这个问题可以看出:Unit Testing - What not to test

【问题讨论】:

    标签: java unit-testing


    【解决方案1】:

    我认为你的观点都有优点,这只是我会做的建议。

    集成测试更适合此代码,使用生成模拟数据或具有测试数据源的注入 DAO 对象。

    但是您的问题是针对单元测试的,在这种情况下,我认为 针对返回的车辆对象进行测试(不模拟构建器) 是最佳选择。为了避免复杂性,我将继续对返回的车辆进行简单的测试(检查它是否不为空,并且可能是它的类不变量),而不是关于车辆类型的详细信息(此测试留给车辆的制造商/生产商)。

    这个测试的优点是:

    1. 如果车辆/制造商后来扩展到需要额外的属性(例如所有车辆都应该有刹车),但开发人员忽略更新存储库,测试将失败。
    2. 它将充当健全性测试,至少确保代码无异常执行。

    【讨论】:

    • 我同意@Aegis。你的代码看起来很简单,所以我认为所有的模拟代码都是多余的。我喜欢集成测试,因为它们更容易编写并提供良好的代码覆盖率。为复杂的类编写单元测试。当被测试的代码可以执行多个替代流程时,将其与其余代码隔离开来进行测试会更容易。
    • 我还认为针对返回的车辆对象进行测试(不模拟构建器)是一个不错的选择。但是,我不完全同意@Aegis 列出的优势。首先,为了简单起见,我更喜欢这种方法。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-01-31
    • 1970-01-01
    • 1970-01-01
    • 2012-03-26
    • 1970-01-01
    相关资源
    最近更新 更多