Friday, January 25, 2008

Example Driven Development and Unit testing

In a project I once worked, we were required by company standards to write formal test specifications for manual testing. It was a lot of overhead to write these Word documents and get them through the bureaucracy. Finally, we proposed to write the test specifications as comments inside functional unit tests. That way, we could maintain the test documents easily. We saved a lot of work, and the documents got higher quality because it was easier to update them. And it really helped us to write good unit tests that covered the functionality.

In this post, I will take this approach one step further to show how unit tests and manual tests can be unified.

In an earlier post Example driven development, I argued that a few simple examples can be used for requirements, manual testing and unit testing. I don't say that a few examples are sufficient as a requirements specification, but they may be in relatively simple projects. And it is far better than nothing. Examples help you to think clearly, and to communicate accurately with others. Don't we all use examples when we try to explain something? That's the best way to explain something anyway.

The problem is that it is hard to keep the examples up to date, and then they lose the value they had for communication with the client and manual testing.

But if we implement the examples as unit tests, we completely avoid this problem! As long as the unit tests pass, the examples will be in sync with the code. And if the client changes the requirements, we modify the unit tests, and then implement the changes until the unit tests pass.

Unit tests are hard to read for non-programmers, but if we put a lot of effort into it, we can make them readable. They don't need to be writable, as programmers will write them.

Here is an example of a test of a servlet that generates licenses:
   public void testGenerateLicense() throws Exception
   {
      // Call the servlet.
      InputParams params = new InputParams();
      params.productId = "123456";
      params.quantity = 1;
      params.firstName = "Lars";
      params.lastName = "Høidahl";
      params.email = "lars@mycompany.com";
      params.company = "Object Generation";
      params.country = "Sweden";
      File licenseFile = servlet.generateLicense(params);
      
      // Check the returned file.
      assertTrue("Attachment is a license file",
            licenseFile.getName().endsWith(".lic"));
      
      // Check that 1 user was created in the database.
      List users = userDatabase.getAllUsers();
      assertEquals("Users", 1, users.size());
      User user = users.get(users.size()-1);
      assertEquals("User name", "Lars", user.getUsername());
      assertEquals("Email", "lars@mycompany.com", user.getEmail());
      assertEquals("Country", "Sweden", user.getCountry());
   }

   public void testGenerateMultipleLicenses() throws Exception
   {
      // Call the servlet.
      InputParams params = new InputParams();
      params.productId = "123456";
      params.quantity = 3;
      params.firstName = "Lars";
      params.lastName = "Høidahl";
      params.email = "lars@mycompany.com";
      params.company = "Object Generation";
      params.country = "Sweden";
      File licenseFile = servlet.generateLicense(params);
      
      // Check the returned file.
      assertTrue("Attachment is a zip file",
            licenseFile.getName().endsWith(".zip"));
      ZipFile zipFile = new ZipFile(licenseFile);
      assertEquals("Number of entries", 3, zipFile.size());
      
      // Check that 3 users were created in the database.
      List users = userDatabase.getAllUsers();
      assertEquals("Users", 3, users.size());
      User user = users.get(users.size()-1);
      assertEquals("User name", "Lars", user.getUsername());
      assertEquals("Email", "lars@mycompany.com", user.getEmail());
      assertEquals("Country", "Sweden", user.getCountry());
   }

No comments: