Tips for (unit testing) JavaBeans
If you’re writing Java code chances are you’re writing at least a few classes that adhere to the JavaBean conventions, i.e., classes that have private properties with public getter and setter methods, contain a no-arguments constructor, are serializable, and comply with the Equals and HashCode contract. And on top of that you’ll probably also throw in a useful toString() implementation.
If, e.g., we take a very simple class called MyBean that contains two fields named id and name, we’ll end up with the following code:
MyBean – a JavaBean example
package it.jdev.example; import java.io.Serializable; public class MyBean implements Serializable { private static final long serialVersionUID = 6170536066049208199L; private long id; private String name; public MyBean() { super(); } public long getId() { return id; } public void setId(final long id) { this.id = id; } public String getName() { return name; } public void setName(final String name) { this.name = name; } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + (int) (id ^ (id >>> 32)); result = prime * result + ((name == null) ? 0 : name.hashCode()); return result; } @Override public boolean equals(final Object obj) { if (this == obj) { return true; } if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } final MyBean other = (MyBean) obj; if (id != other.id) { return false; } if (name == null) { if (other.name != null) { return false; } } else if (!name.equals(other.name)) { return false; } return true; } @Override public String toString() { return "MyBean [id=" + id + ", name=" + name + "]"; } }
So for a class with just two fields, we’ve ended up with 70 lines of code. That is a lot of boilerplate code. And furthermore, every time you add or change any properties you will have to adjust or regenerate alot of that boilerplate code.
Project Lombok to the rescue
Luckily, there is a nice open source tool that aims to reduce just the type of boilerplate code we’re dealing with in our MyBean class. It is called Project Lombok. Just install Lombok as a plugin in your favourite IDE, and include the Lombok jar file on your build classpath or add it as a maven dependency, and you should be good to go.
Project Lombok contains alot of different annotations but for our example we’re only going to need one: @Data. When we apply the annotation to our code we’re down to a mere 15 lines of code from our original 70 lines, with Project Lombok generating all the methods for us during compilation. And moreover, we never again have to worry about our hashCode(), equals(), and toString() methods running out-of-sync.
MyBean – our JavaBean example with Project Lombok
package it.jdev.example; import java.io.Serializable; import lombok.Data; @Data public class MyBean implements Serializable { private static final long serialVersionUID = 6170536066049208199L; private long id; private String name; }
Help, my code coverage is down
The fact that we now have Project Lombok generating the boilerplate code for us, doesn’t necessarily mean that we can skip unit testing the generated methods. Especially if you value code coverage, and you have minimal coverage level checks in place in your CI set-up, you’ll want to add some extra tests. Luckily, there are some easy ways to get your code coverage up.
Testing Serializability
If your serializable objects contain any custom fields, these probably should also be serializable. However, this is something that is easily overlooked. Using the SerializationUtils class from the Apache Commons Lang library, you can write a very simple test that checks whether an object correctly serializes, and deserializes back again.
Testing for Serializibility of our MyBean
package it.jdev.example; import static org.junit.Assert.*; import org.apache.commons.lang3.SerializationUtils; import org.junit.Before; import org.junit.Test; public class MyBeanTest { private MyBean myBean; @Before public void setUp() throws Exception { myBean = new MyBean(); myBean.setId(123L); myBean.setName("Bean, James Bean"); } @Test public void beanIsSerializable() { final byte[] serializedMyBean = SerializationUtils.serialize(myBean); final MyBean deserializedMyBean = (MyBean) SerializationUtils.deserialize(serializedMyBean); assertEquals(myBean, deserializedMyBean); } }
Testing getter and setters methods
Testing the getter and setter method pairs of a JavaBean can become very tedious very quickly. Fortunately there is a nice test library called meanBean that can do the work for us. So after adding the following method to our unit test, we’re finished testing the getters and setters:
Testing the getters and setters of our MyBean example
@Test public void getterAndSetterCorrectness() throws Exception { new BeanTester().testBean(MyBean.class); }
Testing equals() and hashCode()
Testing all the intricacies of the equals and hashCode contract yourself is a very wearisome task. Again, there are some nice tools that can take it off your hands. The aforementioned library meanBean offers functionality to do so. However, I found a tool like EqualsVerifier to be a tad more stringent in its tests, and it also provides detailled explaination about any errors. So we’re going to add the next test case to our suite:
Testing the Equals and HashCode contract of our MyBean example
@Test public void equalsAndHashCodeContract() throws Exception { EqualsVerifier.forClass(MyBean.class).suppress(Warning.STRICT_INHERITANCE, Warning.NONFINAL_FIELDS).verify(); }
Note, that we are suppressing some warnings here. For more information as to why, see the EqualsVerifier information about errormessages: http://www.jqno.nl/equalsverifier/errormessages/.
A common base class for our JavaBean test cases
Even with tools like meanBean and EqualsVerifier doing the heavy lifting, you don’t want to repeat the same test code over and over again. So you’ll probably will want to put the tests in an abstract base class. A possible implementation of that base class could look something like the following:
An abstract base class for testing JavaBeans
package it.jdev.example; import static org.junit.Assert.assertEquals; import java.io.Serializable; import java.time.LocalDateTime; import nl.jqno.equalsverifier.EqualsVerifier; import nl.jqno.equalsverifier.Warning; import org.apache.commons.lang3.SerializationUtils; import org.junit.Test; import org.meanbean.lang.Factory; import org.meanbean.test.BeanTester; public abstract class AbstractJavaBeanTest { protected String[] propertiesToBeIgnored; @Test public void beanIsSerializable() throws Exception { final T myBean = getBeanInstance(); final byte[] serializedMyBean = SerializationUtils.serialize((Serializable) myBean); @SuppressWarnings("unchecked") final T deserializedMyBean = (T) SerializationUtils.deserialize(serializedMyBean); assertEquals(myBean, deserializedMyBean); } @Test public void equalsAndHashCodeContract() { EqualsVerifier.forClass(getBeanInstance().getClass()).suppress(Warning.STRICT_INHERITANCE, Warning.NONFINAL_FIELDS).verify(); } @Test public void getterAndSetterCorrectness() throws Exception { final BeanTester beanTester = new BeanTester(); beanTester.getFactoryCollection().addFactory(LocalDateTime.class, new LocalDateTimeFactory()); beanTester.testBean(getBeanInstance().getClass()); } protected abstract T getBeanInstance(); /** * Concrete Factory that creates a LocalDateTime. */ class LocalDateTimeFactory implements Factory { @Override public LocalDateTime create() { return LocalDateTime.now(); } } }
Note that – just for the fun of it – I’ve added a LocalDateTimeFactory so that meanBean can test the getters and setters of any LocalDateTime attributes you might have used in your JavaBean class.
Applying the abstract base class to the unit test of our MyBean example, the resulting unit test would be something like:
The final unit test for our MyBean
package it.jdev.example; import static org.junit.Assert.*; import org.junit.Test; public class MyBeanTest extends AbstractJavaBeanTest<MyBean> { @Override protected MyBean getBeanInstance() { return new MyBean(); } }
Reference: | Tips for (unit testing) JavaBeans from our JCG partner Wim van Haaren at the JDev blog. |
this import is not working with our jdk