As a follow up to my Central Ohio Day of .Net presentation in Wilmington (download here) on Test Driven Development, I am going to dedicate several posts to expanding on the sample code (and slides) since there is much more than can be covered in an hour.
If you are already writing unit tests, kudos. (I am still amazed at how much code is being slung without any test coverage at all.) In this post, I want to cover some of the basics of Unit Testing.
What is a Unit Test
Several people approached me after my talk and asked if I could explain what makes a good unit test. Before we define "good", let's define "what" is an unit test:
- Code that is used to validate another unit of application code
- Is independent of any other Unit Test
- Leaves the System Under Test in the exact same state it was in prior to the test execution
Validation of Application Code
The target for a unit test is the smallest testable piece of an application. For example, a method of a class. Large pieces are integration tests, and while necessary, are not covered here.
Take a simple example that I use a lot when I do presentations: An addition only calculator.
Before we develop any tests or code, this simple statement represents one of the largest (imho) problems that we face as software developers. How did you interpret that requirement? Does the function take Integers only? Decimals? Positive and Negative? Are Nulls allowed? (The only solution to this age old problem in a more Agile/XP/Scrum - pick your flavor - approach where the dev team has constant access to the product owner. We will come back to this in a later post).
We'll go with the definition of the requirement that the addition function takes two integers. The code for this is straightforward:
public static int Add(int x, int y)
return x + y;
To test this, you would develop a series of Asserts similar to this:
Although the Add function doesn't appear to have any side effects (and it doesn't), each Unit Test must be independent of every other Unit Test. This problem typically surfaces when a datastore (like a database) is involved.
For example, if you are developing an account management system, and you are testing the change password functionality, the unit test must create any required resources (database connections, database records, etc) and not rely on another test to create those resources. While Test Frameworks allow execution of a battery of tests in an automated fashion, there is no guarantee of order. Also, the test would fail if ran in isolation if there was a dependency on the Should_Create_User() test to be executed prior to the Should_Change_Password() test.
Each test must clean up after itself, even in the event of failure. Following along with the Account Management example, the Should_Create_User() test MUST be transacted in some manner to roll back any database changes after completion. One of the biggest sources of problems I see in this area is when exceptions occur in either the Unit Test code or the System Under Test (SUT), and artifacts get left behind. This then causes other issues due to a constantly changing database and "dirty" data.
Types of Unit Tests
There are two general types of Unit Tests:
- State Based Testing
- Interaction Testing
State Based Testing
This is the "typical" unit test (like the tests for the Add method above). It Asserts some quantifiable state change (or non-change) after interaction with the System Under Test. Creating a user in the data base, changing their password, returning a value from a function are examples of state based testing.
This is a unit test that verifies the behavior of a System Under Test. To write these tests, you incorporate a "Mocking" framework into your test code. (More on mocking in a later post)
What is a Good Unit Test
There are two questions that I ask myself when I am developing unit tests:
- Do I have adequate Use Case Coverage (including "Happy" and "Unhappy" paths)?
- Do I have adequate Code Coverage?
Covering All Use Cases
While the Assert for the Add method above seems like it's adequate, we haven't nearly covered all of the use cases. We merely handled one set of positive integers. At a minimum, I would want to code these Asserts:
If the business changes the requirement to only allow positive integers, Tests 4,5,6,7,8 should all fail to match the new requirement.
(In my next post, I'll cover the RowTest feature - plus many others - of MbUnit that makes this much more efficient.)
Code coverage refers to the lines of code for the System Under Test that get executed during test runs. The goal (and this varies depending on who you ask) that I strive for is 85% coverage. There are some code paths that just can't be covered through Unit Tests (although Integration tests should pick these up).
Bringing it All Together
Finishing the example of the Add method, the business changed the requirement to not allow negative numbers, and the decision was made to throw an Exception if a negative Integer was passed in as a parameter. So, here is our new method:
public static int Add(int x, int y)
if (x < 0 y < 0)
throw new ArgumentOutOfRangeException();
return x + y;
Our use case coverage is good, and we get failing tests since we are not handling the exception, and our code coverage covers all execution paths in the Add method.
Note that these examples do NOT illustrate Test Driven Development, but are used here to illustrate unit testing itself.