Start with a Unit Test

Not everyone is a fan of test-driven development.

Writing unit tests for your code can be a laborious task, and it has no immediate benefit to your customers.  So why waste any time writing the tests, right?

This past week, a colleague of mine summed up the problem pretty well:

I’ve finished writing the code for that feature.  Now I just need to take some time to sit down and write some unit tests for it.

Unit Test Results in Visual Studio

He’s not following test-driven development.  Instead, his tests come in as an afterthought.  Actually, most of us code this way.

We write a chunk of code to perform a task.  Then a customer asks for a new feature, so we write some more code.  Then a new hire comes in to the company demanding we take some time to go back and document our existing code base with unit tests.

It’s not fun. It’s not glamorous.  It feels like a waste of time.

But I still feel it’s the way things should be done.

The Advantage

If you write your unit tests first, it actually saves you time.

Now, instead of a long specifications document written in English and littered with biases and assumptions about functionality, you have a concise definition of what the code in your application must do.  It doesn’t need to “flash,” “pop,” or “zing.”  It just needs to pass the test.

If you just start writing code, I guarantee the client or your boss will come back halfway through the project and change the requirements on you.  Even if they promise not to, they will.

Perceived needs change.  Requirements change.  If they do, write another test to include the new conditions – as a developer, your only concern should be writing code that passes the test.

Case Study

I wrote some code for a custom content management system.  Since I didn’t want to be solely responsible for user management, I added some dynamic user creation capabilities.  I wrote my tests, I wrote code that passed the tests, and I thought everything was peachy.

This is the basic function that creates a new user account on the server.  It takes in an object that contains the username, their password, and their email address.  It returns a JSON object containing all of the users in the database.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
public JsonResult CreateUser(PostedUserModel post)
{
  if (post == null) throw new ClientException("Invalid data.");
           
  MembershipCreateStatus status;

  var user = Membership.CreateUser(
    post.UserName,
    post.PlainTextPassword,
    post.Email,
    post.PasswordQuestion,
    post.PasswordAnswer,
    true,
    out status);

  // If the account wasn't created, we need to throw an
  // exception to alert the user.
  if (user == null)
  {
    // Default error message
    string errorMessage = "Error: Unable to create user.";

    switch (status)
    {
      case MembershipCreateStatus.DuplicateEmail:
        errorMessage = "Email address is in use.";
        break;
      case MembershipCreateStatus.DuplicateUserName:
        errorMessage = "Username is in use.";
        break;
      case MembershipCreateStatus.InvalidEmail:
        errorMessage = "Email address is invalid.";
        break;
      case MembershipCreateStatus.InvalidPassword:
        errorMessage = "Password is invalid.";
        break;
    }

    throw new ClientException(errorMessage);
  }

  Roles.AddUserToRole(user.UserName, post.UserRole);
           
  return this.Json(this.Users);
}

Then the QA team kicked the project back.

It turned out, you could create a user with all sorts of invalid characters in their name.  My function allowed for test\user, test<script>user, and quite a few other names that should never be used.

If I were the only one using this function, this wouldn’t be an issue.  But as I open the new administrative features to a team of 40+ it becomes a major problem.

To fix this, I first wrote a unit test to check that my method returned an error if any of these invalid characters were used:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
[TestMethod]
public void CreateUser_Requires_Valid_Username()
{
  PostedUserModel post = new PostedUserModel();

  List<string> userNamesToTest = new List<string>()
  {
    @"test|",  // Test | character
    @"test/",  // Test / character
    @"test\",  // Test \ character
    @"test?",  // Test ? character
    @"test<",  // Test < character
    @"test>",  // Test > character
    @"test'",  // Test ' character
    @"test"""  // Test " character
  };

  foreach (string userName in userNamesToTest)
  {
    try
    {
      post.UserName = userName;
      var result = ac.CreateUser(post);
    }
    catch (ClientException clientException)
    {
      Assert.AreEqual(
        "Username contains invalid characters.",
        clientException.Message);
    }
  }
}

What do you know?  My function now fails the test–which is exactly what we want it to do.

Armed with this new end-user requirement, I was able to go back in to my code and refactor it.  Now, it checks for invalid characters and returns an error if the username is invalid:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
public JsonResult CreateUser(PostedUserModel post)
{
  if (post == null) throw new ClientException("Invalid data.");
           
  MembershipCreateStatus status;
  MembershipUser user;

  List<char> invalidCharacters = new List<char>() {
    '|', '/', '\', '?', '<', '>', '\'', '"'
  };

  if (invalidCharacters.Any(s => post.UserName.Contains(s)))
  {
    user = null;
    status = MembershipCreateStatus.InvalidUserName;
  }
  else
  {
    user = Membership.CreateUser(
      post.UserName,
      post.PlainTextPassword,
      post.Email,
      post.PasswordQuestion,
      post.PasswordAnswer,
      true,
      out status);
  }

  // If the account wasn't created, we need to throw an
  // exception to alert the user.
  if (user == null)
  {
    // Default error message
    string errorMessage = "Error: Unable to create user.";

    switch (status)
    {
      case MembershipCreateStatus.DuplicateEmail:
        errorMessage = "Email address is in use.";
        break;
      case MembershipCreateStatus.DuplicateUserName:
        errorMessage = "Username is in use.";
        break;
      case MembershipCreateStatus.InvalidEmail:
        errorMessage = "Email address is invalid.";
        break;
      case MembershipCreateStatus.InvalidPassword:
        errorMessage = "Password is invalid.";
        break;
      case MembershipCreateStatus.InvalidUserName:
        errorMessage = "Username contains invalid characters.";
        break;
    }

    throw new ClientException(errorMessage);
  }

  Roles.AddUserToRole(user.UserName, post.UserRole);
           
  return this.Json(this.Users);
}

What We Gain

Unit testing makes code more robust.  Not only does it function, but other developers can look at even the most complex code and know exactly what it is and is not supposed to do.

If requirements change down the road, just write a new test and refactor your code to pass.  Having a large suite of unit tests also insulates you from inadvertent breaking changes down the road–run all of your unit tests after every change.  If any fail, you broke something.

If you run into a bug while using the application, write a new test to reproduce the situation.  Refactor your code to pass, and you’ve fixed the bug.  Not only is it fixed now, but when you release the next version in the future, you can automatically re-test to make sure the bug didn’t reappear.

What other benefits can be gained by starting development with a unit test?

About Eric

Eric Mann is a writer, web developer, and outdoorsman living in the Pacific Northwest. If he's not working with new technologies in web development, you can probably find him out running, climbing, or hiking with his dog.

Comments

  1. This almost sounds like an argument for “Design by Unit Test” rather than just writing unit tests first. That does have some merit.

  2. Good article & point about the ‘driven’ part:
    _Tests are like snow tires for your code._

    The old waterfall option was to write an extensive project plan. That still works, but it’s no fun when you’re a developer and your most-effective tool is more code.

Leave a Reply