Wednesday, March 4, 2009

Unit Testing Java and Groovy with JUnit

As new frameworks pop up it pleases me to see how much emphasis is placed on writing automated tests. In Rails and Grails you almost need to go out of your way to opt out of writing tests (as you generate components for your application, test stubs are automatically created for you as part of the process). Unfortunately, far too many professional developers still do not write even the simplest automated tests as part of their software development tasks. Another common problem I see is developers who are new to testing in this manner who insist on creating test methods with a dozen or more assertions in a single individual unit test. I could probably write several posts on this topic and still only scratch the surface of what’s available in this space but I’ll try to focus this one on some simple guidance for someone who’s new to automated testing.

In an effort to keep things as simple as possible I’m purposely staying away from more complex topics (Mock Objects for example). If this post proves popular, I’d be happy to devote more time writing about this topic in greater detail (software quality is something I’m very passionate about). I’m going to use the same example from my last post on interoperability for this post since the domain model is pretty simple to understand and it demonstrates what I consider to be a very simple approach to writing unit tests.

For this example, our domain consists of three classes, a Customer class, an Address class, and a PhoneNumber class. In order to give us something relevant to test, I added a helper method to the Customer class and also to the Address class (the PhoneNumber class has only simple getters and setters). Let me take the opportunity now to say that I’m not a big fan of having unit tests for simple getter and setter methods even though many popular IDEs will generate these for you as part of generating a testing template. My definition of a simple getter or setter in this context is one you used the IDE to generate and that you have NOT further modified (no custom logic). If you’re new to advanced IDEs and professional Java development you should familiarize yourself with the proper way to generate these getter/setter methods from the IDE menus and NEVER create these methods manually. Many frameworks and tools in the Java space work under the assumption that your code conforms to a standard java beans format. If you mistype of even worse use the improper case for a single letter in one of your method names it can cause problems for frameworks that make this assumption (hibernate for example).

The relationship between these three classes is as follows: A customer can have zero or one address. An address can have zero or one PhoneNumber. In the customer class, I’ve added a single helper method (returnFullName) which simply returns the customer’s full name in the format ‘firstName lastName’. In the address class, I created a simple helper method which returns a single line string with the full address flattened out.

I like to think I’m pragmatic when it comes to testing software. There are many schools of thought around testing and one of them: test driven development is extremely popular right now (also a bit controversial in some circles). Given what I typically see when interacting with the average IT shop I’d be happy if the team is writing tests at all.

Before we get into the examples, let’s discuss the general guidance I’d like to offer. This is by no means a comprehensive list – not even close. I’m hoping to provide just a few quick tips which IMO will help to improve the quality of the tests you write. In no particular order they are:

1. Use factory classes to generate your test data.

2. Use constants (public static final) for values in your factory classes.

3. Usually (I try not to say ‘Always’ when it comes to software) an individual unit test should focus on testing a single concern (if your test has more than a few assert tests, it’s probably worth a closer look to see if you might need to break things up a bit).

4. The name of a test method should be descriptive enough to tell others what you’re trying to test.

Let’s take a look at the domain classes we’re hoping to test:

Customer.java

   1: package org.demo;
   2:  
   3: /**
   4:  * A sample customer domain class implemented as a java class
   5:  * @author twalters
   6:  */
   7: public class Customer {
   8:     private String firstName;
   9:     private String lastName;
  10:     private Address address;
  11:  
  12:     public Address getAddress() {
  13:         return address;
  14:     }
  15:  
  16:     public void setAddress(Address address) {
  17:         this.address = address;
  18:     }
  19:  
  20:     public String getFirstName() {
  21:         return firstName;
  22:     }
  23:  
  24:     public void setFirstName(String firstName) {
  25:         this.firstName = firstName;
  26:     }
  27:  
  28:     public String getLastName() {
  29:         return lastName;
  30:     }
  31:  
  32:     public void setLastName(String lastName) {
  33:         this.lastName = lastName;
  34:     }
  35:  
  36:     /**
  37:      * This method returns a fully formatted name for this customer.
  38:      * @return - a string of the format 'firstName lastName'
  39:      */
  40:     public String returnFullName(){
  41:         if ("".equals(this.getFirstName())  this.getFirstName() == null)
  42:             return this.getLastName();
  43:  
  44:         if ("".equals(this.getLastName())  this.getLastName() == null)
  45:             return this.getFirstName();
  46:         
  47:         return this.getFirstName() + " " + this.getLastName();
  48:     }
  49:  
  50: }

Address.groovy

   1: package org.demo
   2:  
   3: /**
   4:  * This is a simple domain model class for a snail mail address.
   5:  * @author twalters
   6:  */
   7: class Address {
   8:     String addressLine1
   9:     String addressLine2
  10:     String city
  11:     String State
  12:     String postalCode
  13:     PhoneNumber phone
  14:  
  15:     /**
  16:     * Returns the address formated as a single line string.  If line1, city, state, or postal code are null,
  17:     * the entire string will return null
  18:     **/
  19:     def String formattedAddress() {
  20:         if (addressLine1 == null  addressLine1 == "" 
  21:             city == null  city == ""  state == null  state == "" 
  22:             postalCode == null  postalCode == "")
  23:             return null;
  24:  
  25:         if (addressLine2 != null && addressLine2 != "")
  26:             return addressLine1 + " " + addressLine2 + " " + city + ", " + state + " " + postalCode
  27:         else
  28:             return addressLine1 + " " + city + ", " + state + " " + postalCode
  29:     }
  30: }
  31:  

PhoneNumber.java only contains simple getters and setters and thus we’ll exclude it from this example at least as far as unit tests are concerned.

The first class I’d like to unit test is the Address domain class. Logically, I would typically lean toward testing the Customer first since it’s at the top of the food chain but that’s exactly why I’ve chosen to not test that one first (since it has an address field, I’d prefer to make sure my address class is working properly before worrying too much about the customer). When I pulled this together I actually practiced TDD (simply stated I wrote a test which failed, then I added logic to my domain to make the test pass) but for this example, the thought process is more important than the tactical approach I took.

In the address class, there are several simple fields (since it’s a groovy class the simple getter/setters are generated as needed). There is one special helper method in this class which must be tested. The purpose of this method is to flatten out an address into a single string field. In order to test this method, we must understand the requirements a bit better. For example, what should this method return when one or more of these fields are not filled in with data. If you think about a real world address, it’s pretty standard for the second address line to be empty but the other fields are typically required. To keep things nice and simple we’ll allow the consumers of this method decide what to do when one of these fields is blank. The method will simply return null if any of the fields are null or empty (except for address line 2 which can be null or empty). Now that we’ve clarified the expected behavior of this method, let’s move on to the next step.

At this point, I could crack open a unit test class and try to write a test to see how far I get before getting stuck. Since, I know that I’m testing a simple domain object, past experience tells me that I’m going to need a way to easily (and consistently) generate some test data in order to properly test the method. Let’s start with a simple test factory class for the address.

I typically name this type of factory class using a convention similar to this: ‘domainNameFactory’ (alternatively you could use a convention of: ‘domainNameTestFactory’). In our example this first factory is named AddressFactory. In general the idea behind a factory class of this type is to provide a static method which contains an instance of the class and returns it to the caller (populated with seed data). We’ll add a public static method to this new class named ‘getTestAddress’. I typically use a name similar to this to make it perfectly clear that this method does more than return an empty object instance. The remainder of this class is pretty self explanatory but I do want to point out one other thing here. I like to define all of the sample data at the top of the class as public static final fields. If this factory grows over time to provide multiple methods which create customers in different ways it’s often useful to have these variables defined in a single place at the top of the class (makes it easier to change a value later). It’s also handy as I often use these public static final fields in the unit test class as part of my assert tests. Here’s the AddressFactory class:

   1: public class AddressFactory {
   2:     public static final String ADDRESS_LINE1 = "First";
   3:     public static final String ADDRESS_LINE2 = "Second";
   4:     public static final String CITY = "City";
   5:     public static final String STATE = "State";
   6:     public static final String POSTAL_CODE = "Postal Code";
   7:  
   8:     public static Address getTestAddress(){
   9:         Address address = new Address();
  10:         address.setAddressLine1(ADDRESS_LINE1);
  11:         address.setAddressLine2(ADDRESS_LINE2);
  12:         address.setCity(CITY);
  13:         address.setState(STATE);
  14:         address.setPostalCode(POSTAL_CODE);
  15:  
  16:         return address;
  17:     }
  18: }

One cautionary note about using public static final fields to define your data. If your field is a complex object type (customer has an address field for example) just remember that we’re dealing with Java here and declaring a complex object as public static final prevents someone from reassigning the reference to a new one but it does NOT prevent you from changing one of the fields within the complex object.

Now that we have our AddressFactory class, let’s generate some unit tests. In thinking through our requirements, it’s clear that this method should in the case of the happy path return the full address with spaces and/or commas between the fields. It’s also clear that we need to test what happens when each of the fields provided isn’t filled in. It’s also important to remember that not being filled in could mean the field is null or it could mean that the field contains an empty string. Even if the language will default a field to one or the other if it’s never set via code you should never assume this value is always there since our application code could potentially null out a field or set it’s value to an empty string. In addition to having the proper tests for each of these scenarios, these tests should:

1. utilize the factory to get sample data

2. have names which makes it clear to someone else what the method is trying to test

Here are the methods I created for this class:

   1: public class AddressTest {
   2:  
   3:     private Address address;
   4:  
   5:     public AddressTest() {
   6:     }
   7:  
   8:     @BeforeClass
   9:     public static void setUpClass() throws Exception {
  10:     }
  11:  
  12:     @AfterClass
  13:     public static void tearDownClass() throws Exception {
  14:     }
  15:  
  16:     @Before
  17:     public void setUp() {
  18:         address = AddressFactory.getTestAddress();
  19:     }
  20:  
  21:     @After
  22:     public void tearDown() {
  23:     }
  24:  
  25:     @Test
  26:     public void testReturnFormattedAddressFullPopulated() {
  27:         String expResult = AddressFactory.ADDRESS_LINE1 + " " +
  28:                 AddressFactory.ADDRESS_LINE2 + " " + AddressFactory.CITY + ", " +
  29:                 AddressFactory.STATE + " " + AddressFactory.POSTAL_CODE;
  30:         String result = address.formattedAddress();
  31:         assertEquals(expResult, result);
  32:     }
  33:  
  34:     @Test
  35:     public void testReturnFormattedAddressWithNullLine2() {
  36:         address.setAddressLine2(null);
  37:         String expResult = AddressFactory.ADDRESS_LINE1 + " " +
  38:                 AddressFactory.CITY + ", " +
  39:                 AddressFactory.STATE + " " + AddressFactory.POSTAL_CODE;
  40:         String result = address.formattedAddress();
  41:         assertEquals(expResult, result);
  42:     }
  43:  
  44:     @Test
  45:     public void testReturnFormattedAddressWithEmptyLine2() {
  46:         address.setAddressLine2("");
  47:         String expResult = AddressFactory.ADDRESS_LINE1 + " " +
  48:                 AddressFactory.CITY + ", " +
  49:                 AddressFactory.STATE + " " + AddressFactory.POSTAL_CODE;
  50:         String result = address.formattedAddress();
  51:         assertEquals(expResult, result);
  52:     }
  53:  
  54:     @Test
  55:     public void testReturnFormattedAddressWithNullLine1() {
  56:         address.setAddressLine1(null);
  57:         String expResult = null;
  58:         String result = address.formattedAddress();
  59:         assertEquals(expResult, result);
  60:     }
  61:  
  62:     @Test
  63:     public void testReturnFormattedAddressWithEmptyLine1() {
  64:         address.setAddressLine1("");
  65:         String expResult = null;
  66:         String result = address.formattedAddress();
  67:         assertEquals(expResult, result);
  68:     }
  69:  
  70:     @Test
  71:     public void testReturnFormattedAddressWithNullCity() {
  72:         address.setCity(null);
  73:         String expResult = null;
  74:         String result = address.formattedAddress();
  75:         assertEquals(expResult, result);
  76:     }
  77:  
  78:     @Test
  79:     public void testReturnFormattedAddressWithEmptyCity() {
  80:         address.setCity("");
  81:         String expResult = null;
  82:         String result = address.formattedAddress();
  83:         assertEquals(expResult, result);
  84:     }
  85:  
  86:     @Test
  87:     public void testReturnFormattedAddressWithNullState() {
  88:         address.setState(null);
  89:         String expResult = null;
  90:         String result = address.formattedAddress();
  91:         assertEquals(expResult, result);
  92:     }
  93:  
  94:     @Test
  95:     public void testReturnFormattedAddressWithEmptyState() {
  96:         address.setState("");
  97:         String expResult = null;
  98:         String result = address.formattedAddress();
  99:         assertEquals(expResult, result);
 100:     }
 101:  
 102:     @Test
 103:     public void testReturnFormattedAddressWithNullPostalCode() {
 104:         address.setPostalCode(null);
 105:         String expResult = null;
 106:         String result = address.formattedAddress();
 107:         assertEquals(expResult, result);
 108:     }
 109:  
 110:     @Test
 111:     public void testReturnFormattedAddressWithEmptyPostalCode() {
 112:         address.setPostalCode("");
 113:         String expResult = null;
 114:         String result = address.formattedAddress();
 115:         assertEquals(expResult, result);
 116:     }
 117:  
 118: }

Each test in the above example is 4-6 lines long and tests a single condition. Some of you might not agree with me to split each of these scenarios up into a separate test but I just think it makes your test scripts easier to read and to verify for completeness (a non technical user could read the method names and determine if your test coverage seems sufficient). Also notice that I’m using the setUp method to regenerate a valid happy path customer before firing each individual unit test. By setting things up this way I no longer care what the other tests are doing to my in memory object because it is reset to a known state before each test is executed.

Now let’s move over to the Customer domain class. There are some requirements which would need to be clarified for this helper method also (for example, what does the user expect to happen if the first name is empty but the last name isn’t?) Using a pattern similar to our address testing we’ll create a CustomerFactory class to create a valid happy path customer for us. Here it is:

   1: public class CustomerFactory {
   2:     public static final String FIRST_NAME = "First";
   3:     public static final String LAST_NAME = "Last";
   4:  
   5:     public static Customer getTestCustomer(){
   6:         Customer customer = new Customer();
   7:         customer.setFirstName("First");
   8:         customer.setLastName("Last");
   9:         customer.setAddress(AddressFactory.getTestAddress());
  10:         return customer;
  11:     }
  12: }

Notice in this one that we’re leveraging the AddressFactory to create a valid address to associate with our customer. If you didn’t see the benefit of the Factory before it should a bit more apparent now (trust me it’s a huge time saver for larger test bases). Given the same general advice about watching out for null as well as empty strings, here is the test class for our customer domain:

   1: public class CustomerTest {
   2:  
   3:     private Customer customer;
   4:  
   5:     public CustomerTest() {
   6:     }
   7:  
   8:     @BeforeClass
   9:     public static void setUpClass() throws Exception {
  10:     }
  11:  
  12:     @AfterClass
  13:     public static void tearDownClass() throws Exception {
  14:     }
  15:  
  16:     @Before
  17:     public void setUp() {
  18:         customer = CustomerFactory.getTestCustomer();
  19:     }
  20:  
  21:     @After
  22:     public void tearDown() {
  23:     }
  24:  
  25:     /**
  26:      * Test of returnFullName method, of class Customer.
  27:      */
  28:     @Test
  29:     public void testReturnFullName() {
  30:         String expResult = CustomerFactory.FIRST_NAME + " " + CustomerFactory.LAST_NAME;
  31:         String result = customer.returnFullName();
  32:         assertEquals(expResult, result);
  33:     }
  34:  
  35:     @Test
  36:     public void testReturnFullNameNullLastName() {
  37:         customer.setLastName(null);
  38:         String result = customer.returnFullName();
  39:         String expResult = CustomerFactory.FIRST_NAME;
  40:         assertEquals(expResult, result);
  41:     }
  42:  
  43:     @Test
  44:     public void testReturnFullNameNullFirstName() {
  45:         customer.setFirstName(null);
  46:         String result = customer.returnFullName();
  47:         String expResult = CustomerFactory.LAST_NAME;
  48:         assertEquals(expResult, result);
  49:     }
  50:  
  51:         @Test
  52:     public void testReturnFullNameEmptyLastName() {
  53:         customer.setLastName("");
  54:         String result = customer.returnFullName();
  55:         String expResult = CustomerFactory.FIRST_NAME;
  56:         assertEquals(expResult, result);
  57:     }
  58:  
  59:     @Test
  60:     public void testReturnFullNameEmptyFirstName() {
  61:         customer.setFirstName("");
  62:         String result = customer.returnFullName();
  63:         String expResult = CustomerFactory.LAST_NAME;
  64:         assertEquals(expResult, result);
  65:     }
  66:  
  67: }

No comments:

Post a Comment