Thursday, April 19, 2012

Unit Testing Guidelines

As quality-conscious software engineers, we all (hopefully) do some level of unit testing.  This typically ranges from simply running your code to see if it works, up to full-blown test-driven development in which the component coding cannot commence until all positive and negative unit tests are thoroughly completed.  If you fall towards the bottom of the range, your QA team probably hates you, and if you are closer to the top of the range, well, uh, honestly, your QA probably still hates you.  Either way, if you create any level of unit tests, there are some basic guidelines that should be followed.  These are the guidelines I like to follow.  If you don't have a set of "rules of thumb" for your team, here's a good place to start......

General Rules:

  • At least one test method should be written for every public service method and every new or overridden repository method
  • Where applicable, positive and negative tests should be written
  • Every Assert should include a relevant, meaningful error message for when failure occurs
  • The Arrange, Act, Assert pattern should always be followed (see below)
There's no need to re-hash what others have written better than I can, so, please read this article on MSDN: Guidelines forTest-Driven Development.  If you don't want to delve into that heavy reading, one important (must-read) highlight from that article is that a good unit test has the following characteristics:
  • Runs fast, runs fast, runs fast. If the tests are slow, they will not be run often.
  • Separates or simulates environmental dependencies such as databases, file systems, networks, queues, and so on. Tests that exercise these will not run fast, and a failure does not give meaningful feedback about what the problem actually is.
  • Is very limited in scope. If the test fails, it's obvious where to look for the problem. Use few Assert calls so that the offending code is obvious. It's important to only test one thing in a single test.
  • Runs and passes in isolation. If the tests require special environmental setup or fail unexpectedly, then they are not good unit tests. Change them for simplicity and reliability. Tests should run and pass on any machine. The "works on my box" excuse doesn't work.
  • Often uses stubs and mock objects. If the code being tested typically calls out to a database or file system, these dependencies must be simulated, or mocked. These dependencies will ordinarily be abstracted away by using interfaces. We are accomplishing this by using the Moq mocking framework.
  • Clearly reveals its intention. Another developer can look at the test and understand what is expected of the production code.

Arrange, Act, Assert:

Follow the "3-As" pattern for test methods: Arrange, Act, Assert. Specifically, use separate code paragraphs (groups of lines of code separated by a blank line) for each of the As.
  • Arrange is variable declaration and initialization.
  • Act is invoking the code under test.
  • Assert is using the Assert.* methods to verify that expectations were met. Following this pattern consistently makes it easy to revisit test code.
public void should_be_able_to_add_two_numbers_together()
    // Arrange
    int firstNumber = 1;
    int secondNumber = 2;
    Calculator calculator = new Calculator();

    // Act
    var result = calculator.Add(firstNumber, secondNumber);


This makes it much easier to see:
  • What is being set up and initialized in the arrange section
  • What method is being executed in the act section
  • What determines the outcome of the test in the assert section
This pattern becomes even more important when using a mocking framework, which we will be doing, using Moq, which I'll describe in my next post.

No comments:

Post a Comment