4

If I have to test a service that uses a mutable entity I would build the smallest object that I need (a real one) and pass it to my service. Example:

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

resetPasswordService.resetPassword(joe);

verif(emailServiceMock).sendEmail("joe@example.com", "Your password has been reset!");

Obviously User has lots of fields but I do not set them since resetPasswordService does not need them. This is very refactor-friendly since if I rename a User field that is not the email this test will not be changed.

The problem appears when I try to do the same with an Immutables object. I will stick with the same example and turn User from an entity into an immutable.

@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: Cannot build User, some of required attributes are not set [postalAddress, signupDate, city, ....]

This fails in the builder when it tries to build the object. So what should I do?

  • Use a mock for User and have it return mocks even if every time a mock returns a mock a fairy dies
  • Create a testing DSL and have some sort of factory to build the entire User tree structure with all the fields I don't need? Seems heavy and not so refactor-friendly. This makes the requirements of the test not so transparent.
  • Make all the fields in User @Nullable and have the builder not validate the object? This would expose me to the risk of having incomplete objects in production, right?
  • some other option I missed?

I know Users should be entities and not immutable value objects. I used User in this example since it is easy to understand.

GhostCat
  • 137,827
  • 25
  • 176
  • 248
ddreian
  • 1,766
  • 5
  • 21
  • 29
  • Just populate it with some data. You can always create a test builder which have some valid data in there. I am strongly against mocking data transfer objects, because if you do so, the only place where you test your getters/setters would be in high level acceptance tests and it might be difficult to debug later (see more: https://jaroslawpawlak.wordpress.com/2014/09/07/what-to-mock-and-why-not-to-mock-data-objects/). – Jaroslaw Pawlak Aug 30 '17 at 09:32
  • I'm surprised why nobody closed this or downvoted saying this is a opinion based stuff and not useful for SO! (I like this question. Any way what sense does it make to test mock data?) – Anoop Thiruonam Jun 13 '18 at 20:48

2 Answers2

15

Simple answer: you only use mocks if you have to.

Meaning: when you need to either control the behavior of an object in ways that the "real" class doesn't support. Or when you have to verify calls on the mock.

So: when you can write a test case that does what you want it to do without using mocking - then go for that.

Mock frameworks are tools. You don't use them because you can, but because they solve a problem for you that you otherwise can't address (easily).

Beyond that: as explained, the default should be to avoid mocks. On the other hand, programming is always about balancing efforts and "return on investment". That is why I used the word easily above. When it turns out that using a mock results in writing down 2, 3 easy-to-comprehend lines of code ... but using "the real" class is much more complicated (or relies on certain implicit assumption about how that class works) - then using a mock can be the better choice.

In that sense, the answer is: don't take answers and rules as golden standard. In the end, this is always about human judgement.

GhostCat
  • 137,827
  • 25
  • 176
  • 248
  • Does building half-baked objects count as "the real class doesn't support"? Building objects that have only 1 field since that is what the test requires. – ddreian Aug 30 '17 at 10:37
  • Your answer is still not very clear :) I asked on SO since I wanted to see what others think of my specific case. My interpretation for your answer is to use mocks for this specific case. Am I right? – ddreian Aug 30 '17 at 10:49
3

Your test is currently relying on implementation details of the password reset feature.

This is the behaviour you want to test:

  • Given a user
  • When that user requests a password reset
  • Then an email is sent

Suppose you decide later on to change the password reset feature so that the email includes their name:

Dear Joe,

You have requested a password reset...

Your test will now fail with a NullPointerException because you based your testing strategy on the assumption that the User instance will never need a name. A perfectly innocuous change has caused our test to fail when it should still pass.

The solution: use a real object. If you find your creating lots of users in different tests, refactor the user creation to its own function:

private User getUser()
{
    User joe = new User();
    joe.setEmail("joe@example.com");
    joe.setName("Joe");
    joe.setAge(20);
    joe.setHeight(180);
    return joe;
}
Michael
  • 41,989
  • 11
  • 82
  • 128
  • The implementation of resetPassword is something like this emailService.sendEmail(user.getEmail(), "Your password has been reset!"); The problem is that if I want a real user I cannot set it's other fields since there are no setters, I need to use the auto generated builder, which checks all the fields for null. The other fields are not primitives but ob objects like Address and City which have other objects and so on. I only need an User with an email and not other objects. But I cannot build one since the builder validates them. – ddreian Aug 30 '17 at 10:40