Should I use real objects or mocks in unit tests with Immutables?

4.4k Views Asked by At

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("[email protected]");

resetPasswordService.resetPassword(joe);

verif(emailServiceMock).sendEmail("[email protected]", "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("[email protected]").build();

resetPasswordService.resetPassword(joe);

verif(emailServiceMock).sendEmail("[email protected]", "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.

2

There are 2 best solutions below

2
On BEST ANSWER

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.

1
On

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("[email protected]");
    joe.setName("Joe");
    joe.setAge(20);
    joe.setHeight(180);
    return joe;
}