Java 我应该在不可变的单元测试中使用真实对象还是模拟?

Java 我应该在不可变的单元测试中使用真实对象还是模拟?,java,unit-testing,object,mockito,immutables-library,Java,Unit Testing,Object,Mockito,Immutables Library,如果我必须测试一个使用可变实体的服务,我会构建我需要的最小对象(真实对象),并将其传递给我的服务。例如: User joe = new User(); joe.setEmail("joe@example.com"); resetPasswordService.resetPassword(joe); verif(emailServiceMock).sendEmail("joe@example.com", "Your password has been reset!"); 显然,用户有很多字段

如果我必须测试一个使用可变实体的服务,我会构建我需要的最小对象(真实对象),并将其传递给我的服务。例如:

User joe = new User();
joe.setEmail("joe@example.com");

resetPasswordService.resetPassword(joe);

verif(emailServiceMock).sendEmail("joe@example.com", "Your password has been reset!");
显然,用户有很多字段,但我没有设置它们,因为
resetPasswordService
不需要它们。这对重构非常友好,因为如果我重命名一个不是电子邮件的用户字段,这个测试将不会更改

当我尝试对对象执行相同操作时,问题出现了。我将继续使用相同的示例,并将用户从一个实体变成一个不变的实体

@Value.Immutable
public abstract class User {
    public abstract String getEmail();
    public abstract PostalAddress getPostalAddress();
    //more fields
}

User joe = new ImmutableUserBuilder().email("joe@example.com").build();

resetPasswordService.resetPassword(joe);

verif(emailServiceMock).sendEmail("joe@example.com", "Your password has been reset!");
java.lang.IllegalStateException:无法生成用户,未设置某些必需的属性[postalAddress、signupDate、city等]

当构建器尝试构建对象时,它会失败。那我该怎么办

  • 为用户使用mock,并让它返回mock,即使
  • 创建一个测试DSL,并使用某种工厂来构建整个用户树结构,其中包含我不需要的所有字段?看起来很重,而且对重构不太友好。这使得测试的要求不那么透明
  • 将User
    @Nullable
    中的所有字段设置为空,并且让生成器不验证对象?这将使我面临生产中存在不完整对象的风险,对吗
  • 我错过了其他的选择吗

我知道用户应该是实体,而不是不变的值对象。我在这个例子中使用了User,因为它很容易理解。

简单的回答:如果必须,您只能使用mock

含义:当您需要以“真实”类不支持的方式控制对象的行为时。或者当您必须验证模拟上的调用时

所以:当您可以编写一个测试用例来完成您希望它做的事情而不使用模拟时——那么就开始吧

模拟框架是工具。你使用它们不是因为你可以,而是因为它们为你解决了一个你无法(轻松)解决的问题

除此之外:如前所述,默认设置应该是避免模拟。另一方面,编程总是要平衡努力和“投资回报”。这就是为什么我在上面很容易地使用这个词。当使用模拟结果是写下2、3行易于理解的代码时。。。但是使用“真实”类要复杂得多(或者依赖于关于该类如何工作的某些隐含假设),那么使用mock可能是更好的选择


从这个意义上说,答案是:不要把答案和规则当作黄金标准。最后,这总是关于人类的判断

您的测试当前依赖于密码重置功能的实现细节

这是您要测试的行为:

  • 给定用户
  • 当该用户请求重置密码时
  • 然后发送一封电子邮件
假设您稍后决定更改密码重置功能,以便电子邮件中包含他们的姓名:

亲爱的乔:

您已请求密码重置

您的测试现在将失败,出现
NullPointerException
,因为您的测试策略基于
User
实例永远不需要名称的假设。一个完全无害的变化导致我们的测试在应该通过的时候失败了

解决方法:使用真实对象。如果您发现自己在不同的测试中创建了大量用户,请将用户创建重构为自己的功能:

private User getUser()
{
    User joe = new User();
    joe.setEmail("joe@example.com");
    joe.setName("Joe");
    joe.setAge(20);
    joe.setHeight(180);
    return joe;
}

用一些数据填充它。您始终可以创建一个测试生成器,其中包含一些有效数据。我强烈反对模拟数据传输对象,因为如果这样做,测试getter/setter的唯一地方将是在高级验收测试中,并且以后可能很难进行调试(请参阅更多:)。我很惊讶为什么没有人关闭此项或否决此项,说这是基于意见的内容,并且没有用!(我喜欢这个问题。不管怎样,测试模拟数据有什么意义?)构建半生不熟的对象是否算作“真正的类不支持”?构建只有一个字段的对象,因为这是测试所需要的。您的答案仍然不是很清楚:)我问了这么多,因为我想看看其他人对我的具体案例的看法。我对你的回答的解释是对这个具体案例使用模拟。我说的对吗?resetPassword的实现类似于emailService.sendEmail(user.getEmail(),“您的密码已被重置!”);问题是,如果我想要一个真正的用户,我不能设置它的其他字段,因为没有设置程序,我需要使用自动生成的生成器,它会检查所有字段是否为空。其他字段不是原语,而是ob对象,如地址和城市,它们有其他对象等等。我只需要一个用户与电子邮件,而不是其他对象。但是我不能构建一个,因为生成器会验证它们。