Testing Object’s Immutability: Is This a Good Idea?

In our last post we talked about how to use different features of a language to guarantee immutability.

Now, let’s move on how to test that feature.

TRY IT YOURSELF: You can find this post source code here.

Object’s Immutability Series

Why Should I Test Immutability?

Well, if your business rules depends on having immutable objects, it means, that if you break your immutable environment, your business rules will break.

The crazy thing here is that you might not realize an immutability failure, they are usually hard to catch, because, they appear in concurrent environments, and those are difficult to trace and debug.

NOTE: Remember the advantages and use cases of immutability, you can check: Object Immutability: The Basics and Breaking Immutability: Avoiding A Developer’s Mistake

Let’s Create Immutability Tests

We are going to use our previous Person class:

class Person{
 private final String name;
 private final List<String> addresses;
 private final LocalDate birthDate;
 
 public Person(String name, List<String> addresses, LocalDate birthDate){
  this.name = name;
  this.addresses = Collections.unmodifiableList(addresses);
  this.birthDate = birthDate;
 }

 public Person setName(String name){
   return new Person(name, this.addresses, this.birthDate);
 }
 
 public Person setAddress(List<String> addresses){
   return new Person(this.name, addresses, this.birthDate);
 }
 
 public Person setBirthDate(LocalDate birthDate){
   return new Person(this.name, this.address, birthDate);
 }
 
 public String getName(){
   return this.name;
 }
 
 public List<String> getAddresses(){
   return this.addresses;
 }
 
 public LocalDate getBirthDate(){
   return this.birthDate;
 }
}

Now, let’s create some unit tests for that class.

public class PersonTest {
    private Person person;
    private Person personBefore;

    @Before
    public void before() {
        this.person = new Person("myname",
                Arrays.asList("address1", "address2"),
                LocalDate.of(1988, 5, 4));

        this.personBefore = new Person("myname",
                Arrays.asList("address1", "address2"),
                LocalDate.of(1988, 5, 4));
    }

    @Test
    public void setName_newPerson_changedName() {
        assertThat(this.person.setName("otherName"))
                .isEqualTo(new Person("otherName",
                        Arrays.asList("address1", "address2"),
                        LocalDate.of(1988, 5, 4)));

        assertThat(this.person).isEqualTo(this.personBefore);
    }

    @Test
    public void setAddress_newPerson_changedAddress() {
        assertThat(this.person.setAddress(Arrays.asList("address5", "address2")))
                .isEqualTo(new Person("myname",
                        Arrays.asList("address5", "address2"),
                        LocalDate.of(1988, 5, 4)));

        assertThat(this.person).isEqualTo(this.personBefore);
    }

    @Test
    public void setBirthDate_newPerson_changedAddress() {
        assertThat(this.person.setBirthDate(LocalDate.of(1919, 5, 4)))
                .isEqualTo(new Person("myname",
                        Arrays.asList("address1", "address2"),
                        LocalDate.of(1919, 5, 4)));

        assertThat(this.person).isEqualTo(this.personBefore);
    }

    @Test
    public void getName_ok() {
        assertThat(this.person.getName()).isEqualTo("myname");

        assertThat(this.person).isEqualTo(this.personBefore);
    }

    @Test
    public void getAddresses_ok() {
        assertThat(this.person.getAddresses()).isEqualTo(Arrays.asList("address1", "address2"));

        assertThat(this.person).isEqualTo(this.personBefore);
    }

    @Test
    public void getBirthDate_immutability_ok() {
        assertThat(this.person.getBirthDate()).isEqualTo(LocalDate.of(1988, 5, 4));
        
        assertThat(this.person).isEqualTo(this.personBefore);
    }
}

First, in line 11-13 we created a Person copy, the previous state for our Person object.

NOTE: Immutability must be tested against each class behavior, and should be included in the same tests we usually do to guarantee the normal behavior of our class.

Now, for each method on the Person class, we created a unit test, and at the end, we validated that the current Person object (the test subject, the object we used to execute the behavior) is equal to the Person copy we did. If they are equal, immutability is valid.

NOTE: You should override equals and toString when you do those kind of tests, otherwise, you will get weird behavior.

If we run the unit tests, we get:

All unit tests passed

TRY IT YOURSELF: You can find this source code here.

Adding a Developer’s Mistake… Again

Well, a new developer in the project needs to change a little bit how Person class updates his name, so, he did the following:

class Person{
 private String name;
 private final List<String> addresses;
 private final LocalDate birthDate;
 
 public Person(String name, List<String> addresses, LocalDate birthDate){
  this.name = name;
  this.addresses = Collections.unmodifiableList(addresses);
  this.birthDate = birthDate;
 }

 public Person setName(String name){
   this.name = name;

   return new Person(name, this.addresses, this.birthDate);
 }
 
 public Person setAddress(List<String> addresses){
   return new Person(this.name, addresses, this.birthDate);
 }
 
 public Person setBirthDate(LocalDate birthDate){
   return new Person(this.name, this.address, birthDate);
 }
 
 public String getName(){
   return this.name;
 }
 
 public List<String> getAddresses(){
   return this.addresses;
 }
 
 public LocalDate getBirthDate(){
   return this.birthDate;
 }
}

Now, he realized that removing the final word from name, he will be able to update the name of this object, and return a new one on the setName method. That is awesome, we have now two behaviors in one….

Well, he just broke our immutable class and the Single Responsibility Principle.

Unit Testing to the Rescue

Well, as we created unit testing for our immutable Person class, let’s see how they react to this new change:

One unit test breaks

Now, we have a way to see if any change broke our immutability class.

TRY IT YOURSELF: You can find this source code here.

Final Thought

Unit testing is pretty important to guarantee the behavior of our application and avoiding developer’s mistake.

Testing immutability is not complex, but, requires attention.

In the following posts, we are going to talk about builder pattern and how it helps us to build immutable objects in a more clean way.

If you liked this post and are interested in hearing more about my journey as a Software Engineer, you can follow me on Twitter and travel together.

Advertisement

5 comments

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s