Kenneth Truyers

Unit testing: How to start writing effective tests

Introduction

Kenneth Truyers

Kenneth Truyers


unit testing

Unit testing: How to start writing effective tests

Posted by Kenneth Truyers on .
Featured

unit testing

Unit testing: How to start writing effective tests

Posted by Kenneth Truyers on .

Unit testing is widely accepted as a good development practice. Strangely enough it’s also** widely ignored** in a lot of companies.

It seems that everyone agrees that it is a must and should be implemented in every development shop. In reality, however, you’ll notice that there are actually not a lot of companies that implement unit testing. In the ones where they have unit testing often it’s not implemented effectively.

When you talk about unit tests these are often overheard statements:

  • “It costs too much time, we’d better fix these bugs first”
  • “Our system is too big for unit tests”
  • “We just ought to be more careful not to introduce bugs, that will help us a lot”
  • “Our system is not suited for unit tests”
  • “We have a QA-team that will detect errors”

Usually these statements go accompanied by a lot of reasoning as why not to implement unit tests. Let’s tackle them one by one:

1. Unit tests cost time

That’s true, but the time spent writing the unit test is time you have to spend anyway. If you don’t write the test, you will have to execute the software manually (or were you planning on writing your code and releasing it straight into production?). The benefit of automated tests is that you can run them over and over again. So yes, it costs time to write a test, but you will win that time back, along with many other benefits.

2. Our system is too big for unit tests

It actually might be true that your system is too big to get it completely covered. But that’s not really an issue. If you start writing tests now, steadily your codebase will be covered more and more by tests and it will become a higher quality system.

3. We ought to be more careful not to introduce bugs

In an ideal world we wouldn’t make mistakes and we’d always ship bug-free software. The truth is, building software is hard, it’s very hard and like or not, you are going to make mistakes. You can try to tell everyone to do a perfect job all the time, but if you’re realistic you’ll realize that you’ll never achieve this. People make mistakes and they will keep making mistakes, especially in dealing with complex problems such as building software. What you need to achieve is a certain tolerance for errors. This doesn’t mean that you shouldn’t care for errors, you should. The worst thing about a bug is not the bug, it’s the fact that the customer detects it, and not you, the developer.** If you can detect it, you can fix it** before it does any damage.

4. Our system is not suited for unit tests

I’ve heard this statement pop up in almost every type of system and business. The fact is, if you are writing code, it’s suitable for unit tests. Often developers don’t exactly know how unit tests work and they can’t find a good way to put their code under tests. Therefor, they consider the particular system they’re working in to be not suitable for unit testing. It normally boils down to the fact that the code has not been written with testing in mind. Code that wasn’t written to be prepared for tests, usually contains a lot of dependencies. If you have a lot of dependencies you can’t separate the units. If you can’t separate units, you can’t write unit tests. Throughout the following posts I will explain various techniques for breaking dependencies step-by-step without breaking existing code. Once the dependencies are gone (or at least reduced), usually the system can be tested quite easily.

5. Our QA-team will detect errors, report them and then we’ll fix them

If you have a dedicated QA-team, consider yourself lucky, because a lot of developers don’t have that luxury. Even so, as developers we should aim to provide the QA-team with the best and most bug-free software possible. Why? Because it’s cheaper to fix bugs immediately.

If you look at errors, the cheapest errors are the ones you find immediately after you’ve inserted them. The most expensive errors are the ones that are detected some time after you (or someone else) created them. Compare these types of errors:

  • You have just finished a method and you’re ready to run your program. When compiling, the compiler tells you this: The type or namespace name ‘strin’ could not be found
  • You are testing your program and you try to create a report when suddenly it crashes and the debugger pops up a dialog which says: “Object reference not set to an instance of an object”
  • Your code has been approved and installed into production. After 6 months you go for a (well-deserved) two week holiday and your co-worker receives the following bug report:
    ”Sometimes invoices don’t get generated for certain clients”

In which situation would you rather be?

  • In the first case you’d probably say: oops, a little typo, let me quickly fix that.
  • In the second case you’d rethink your latest changes and maybe have an idea that that last change you made to the database has affected the report generation.
  • In the third case you’ll probably get a phone call on the beach from your co-worker asking you how this can be possible and where to look. You won’t have a clue and he will have a hard time finding out what went wrong.

With decent tests in place, you can catch all three bugs in the same place: when you run your test suite!
Unit tests are like a super advanced compiler on steroids: Not only do they check your syntax, they also tell you if you have made any logical errors or wrong assumptions.

Why are unit tests valuable?

I don’t want to spend a lot of time on explaining all the benefits of unit testing here, because there’s already a lot of literature on that available. I’ll sum up the main benefits:

  • A set of unit tests acts as a regression test suite: you avoid the *“Fix 2 bugs, introduce a new one”-*phenomenon (or worse, the other way around).
  • Production code is documented through test code. Want to know what the software does? Read the tests!
  • It forces developers to think more about their code.
  • It constraints errors to be integrated in your SCM. The tests don’t pass? No check-in!
  • It improves the design of the code: you have to make your code testable, so you’ll make sure to separate concerns better.
  • It enables refactoring. Since you know that your tests will tell you if you made a mistake, you have a free hand to go and improve things. (No more “if it works, don’t touch it!”)
  • It gives you peace of mind.

What is unit test and what is not a unit test?

*“A unit test is a method to test individual units of code”. *Cool, but that doesn’t really help if you don’t know what a unit is. Is it a method, a field, an assembly (or a package in Java) or a class?
When you write a unit test you should aim to test for the smallest possible unit that contains logic. This makes sure your tests are small and useful. In most cases it comes down to testing a method but it could also be a small class.

You know it’s not a unit test when you’re testing 500 lines of code in one go. You should aim more for the region of around 20 lines of code.

How does a unit test work?

A test basically calls a piece of your code with known inputs and then checks the output to check whether the code behaves as expected. A simple example will make this clear:

publicclass Calculator { public Decimal GetOrderTotal(Product Product, int Quantity) { return Product.Price * Quantity; } }

This is a simple class with one method that calculates the total of an order. We pass in a product, calculate the total price based on the quantity and return that value. You could easily imagine this becoming more complicated with various products, price conditions, discounts and many more.

Now here is the unit test for this method:

[TestClass] publicclass CalculatorTest { [TestMethod] void TestGetOrderTotal() { Calculator calc = new Calculator(); Product p = new Product() { Price = 150 }; int quantity = 5; decimal total = calc.GetOrderTotal(p, quantity); Assert.AreEqual(750, total); } }

We create a new instance of our Calulcator, create a product and a quantity (our known values) and ask the calculator to get the order total. We then compare the result to a known value.
As we add more logic to the calculator, we will probably have to adapt the unit test, but that’s OK, that way our tests grow with our code and we always have a check for what we are doing. Another benefit is that it actually documents our code.

Conclusion

In this post I have given a short introduction on what unit testing is and why it is valuable. We have talked about the reasons to not implement unit tests and how they are actually not valid. In the next posts I will explain my arguments further and give you a guide to effective testing with the following topics:

  • How to test legacy code?
  • How to break dependencies?
  • When does a unit test become an integration test?
  • How much testing should we do?

Stay tuned!

Kenneth Truyers

Kenneth Truyers

View Comments...