Unit Testing is
one of the pillars of Agile Software Development. First introduced by Kent Beck, unit testing has found its way into
the hearts and systems of many organizations. Unit tests help engineers reduce the number of bugs, hours spent on debugging, and
contribute to healthier, more stable software.
In this post we look at a dozen unit testing tips that software engineers can apply, regardless of their programming language or environment.
A newbie might ask Why should I write tests? Indeed, aren't tests boring
stuff that software engineers want to outsource to those QA guys?
That's a mentality
that no longer has a place in modern software engineering. The goal of software teams
is to produce software of the highest quality. Consumers and business users were
rightly intolerant of buggy software of the 80s and 90s. But with the
abundance of libraries, web services and integrated development environments
that support refactoring and unit testing, there's now no excuse for software with bugs.
The idea behind unit testing is to create a set of tests for each software component. Unit tests facilitate continuous software testing; unlike manual tests, it's cheap to perform them repeatedly.
As your system expands, so does the body of unit tests. Each test is an insurance that the system works. Having a bug in the code means carrying a risk. Utilising a set of unit tests, engineers can dramatically reduce number of bugs and the risk with untested code.
When you start unit testing, always ask What Tests Should I
Write?
The initial impulse is to write a bunch of functional tests; i.e., tests that probe different functions of the system. This is not correct. The right thing is to create a test case (a set of tests) for each major component.
The focus of the test is one component at a time. Within each component, look for an interface - a set of publicly exposed behaviour that component offers. You then should write at least one test per public method.
As with any code, there will be common things
all your tests need to do. Start with finding a unit testing for your language. For example, in Java, engineers use
JUnit - a simple yet powerful framework for writing tests in Java.
The framework comes with TestCase class, the base class for all tests. Add
convenient methods and utilities applicable to your environment. This way, all your tests cases can share this
common infrastructure.
Testing is time-consuming, so ensure
your tests are effective. Good tests probe the core behaviour of each component, but do it with the least code possible.
For example, there is very little reason in writing tests for Java Bean setter and getter
methods, for these will be tested anyway.
Instead, write a test that focuses on the behaviour of the system. You don't need to be comprehensive; create the tests that come to mind now, then be ready to come back to add more.
Software engineers are always concerned with efficiency, so when they hear
that each test needs to be set up separately they worry about performance. Yet setting up each test correctly
and from scratch is important. The last thing you want is for the test to fail because it used some old
piece of data from another test. Ensure each test is set up properly and don't
worry about efficiency.
In cases when you have a common environment for all tests - which doesn't change as tests run - you can add a static set up block to your base test class.
Setting up tests is not that
simple; and at first glance sometimes seems impossible.
For example, if using Amazon Web Services in your code, how can you simulate it in the test
without impacting the real system?
There are a couple of ways. You can create fake data and use that in tests. In the system that has users, a special set of accounts can be utilised exclusively for testing.
Running tests against a production system is risky: what if something goes wrong and you delete actual user data? An alternative is fake data, called stubs or mock objects.
A mock object implements a particular interface, but returns predetermined results. For example, you can create a mock object for Amazon S3 which always reads files from your local disk. Mock objects are helpful when testing complex systems with lots of components. In Java, several frameworks help create mock objects, most notably JMock.
Testing only pays if you really invest in it. Not only
do you
need to write tests, you also need to ensure they're up to date. When adding a new method to a component, you need to add one or more corresponding
tests. Just like you should clean out unused code, also remove tests that are no longer
applicable.
Unit tests are particularly helpful when doing large refactorings. Refactoring focuses on continuous sculpting of the code to help it stay correct. After you move code around and fix the tests, rerunning all the related tests ensures you didn't break anything while changing the system.
Unit tests are effective weapons in the fight against bugs.
When you uncover a problem in your code, write a test that exposes this problem before
fixing the code. This way, if the problem reappears, it will be caught with the test.
It is important to do this since you can't always write comprehensive tests right away. When you add a test for a bug, you're filling in the gap in your original tests in a disciplined way.
In addition to guarding correctness of the code,
unit tests can help ensure the performance of your code doesn't degrade over time.
In many systems slowness creeps in as the system grows.
To write performance tests, you need to implement start and stop functions in your base test class. When appropriate you can use a time-particular method or code and assert that the elapsed time is within the limits of the desired performance.
Concurrent code is notoriously tricky and
typically a source of many bugs. This is why
it's important to unit test concurrent code. The way to do this is by using a system of sleeps and locks.
You can write in sleep calls in your tests if you need to wait for a particular system state.
While this is not a 100% correct solution, in many cases it's sufficient. To simulate concurrency in a more
sophisticated scenario, you need to pass locks around to the objects you're testing.
In doing so, you will be able to simulate concurrent system, but sequentially.
The whole point of tests is to run
them a lot. Particularly in larger teams where dozens
of developers are working on a common code base, continuous unit testing is important. You can
set up tests to run every few hours or you can run them on each check-in of the code or just once a day (typically overnight).
Decide which method is the most appropriate for your project and make the tests run automatically and
continuously.
Probably the most important tip is to have fun. When I first encountered unit testing, I
was sceptical and thought it was just extra work. But I gave it a chance, because smart people who
I trusted told me that it's very useful.
Unit testing puts your brain into a state which is very different from coding state. It is challenging to think about what is a simple and correct set of tests for this given component.
Once you start writing tests, you'd wonder how you ever got by without them. To make tests even more fun, you can incorporate pair programming. Whether you get together with fellow engineers to write tests or write tests for each other's code, fun is guaranteed. At the end of the day, you will be comfortable knowing your system really works because your tests pass.
And now please join the conversation! Share unit testing lessons from your projects with all of us.
Comments
Subscribe to comments for this post OR Subscribe to comments for all Read/WriteWeb posts
Another good book to add to a developers arsenal is Pragmatic Unit Testing in Java with JUnit, by Andy Hunt and Dave Thomas. I have had this book for a while now and I find it very useful.
Posted by: Falafulu Fisi | August 14, 2008 4:30 AM
I've found that unit testing is very useful as a tracking metric for mid size and larger projects. In the projects I've managed I like to include the information about the unit tests created to certify that each requirement is indeed complete and correctly implemented. Then, as development continues, I can report that each feature is X% complete as Y out Z test are currently returning expected results. This kind of objective measurement help me keep my clients informed about the real progress of the project, rather than depend of subjective appreciation of each developer.
Regards,
Posted by: Esteban Felipe | August 14, 2008 6:09 AM
@2 Good angle, Esteban.
Stability is another way to look at risk. Since tests serve as insurance of sort, failures are the measure of non-readiness or risk if the software went live.
Alex
Posted by: Alex Iskold | August 14, 2008 7:05 AM
I posted on this subject a while back. In a nutshell, most teams I've worked with don't do extensive unit testing and end up "coding scared" in that they stop making changes or improvements because something might get broken, and in the absence of a comprehensive unit test, the only way to find out is to do a bunch of manual testing. It was a big psychological shift for me to switch from thinking unit testing is something done for other people to verify my code to saying that unit testing is something for myself to free me up from worrying about what I'm breaking. Especially nowadays, given the tight integration of JUnit with IDEs like Eclipse, the ability run tests automatically with Maven builds, and the auto-generation of test fixtures in frameworks like Ruby on Rails, there's really no excuse for keeping a large test suite up to date.
Posted by: Elias Holman | August 14, 2008 7:43 AM
Nice article. Two points:
1) @Elias is totally correct that too many organizations end up "coding scared" because they don't aren't confident about refactoring without unintentionally breaking things. Surround your code (ALL your code) with unit tests to give you back that control.
2) You missed one essential benefit to unit testing: improved design. By writing tests first, you attack the problem from the perspective of the consumers of your software (people and/or other software), and you are essentially forced to create a more elegant, decoupled design. And, when you have to refactor later in response to changing requirements, your design is less brittle and can be refactored more easily.
Posted by: Anthony Stevens | August 14, 2008 8:37 AM
You should refactor tests even when you don't refactor the code.
Why write one test case per major component? Why not write at least one test case per functionality of a component?
Ideally we should also write test before any functionality is written. It really helps in reducing the bug count.
I think the most important take-home point is not to say "have fun" but testing should be fun by itself. If you cannot get into that mindset of observing the red bar going green (JUnit style) again and again then you are not really into unit testing.
Posted by: Angsuman Chakraborty
|
August 14, 2008 9:43 AM
Great post!
Whenever we quote a software or web application project, we ALWAYS include a certain percentage of the total cost of the project toward testing. However, it has been noted very consistently over the last 5 years that a lot of people (read prospects) cannot grasp the importance of or even the need for testing, especially when it comes to custom development.
So, thanks, because this post can illustrate to a technically savvy prospect what testing is all about!
That said, if is quite difficult to write a test plan before development starts. Reason? Specs do have a (bad) habit of changing. If a test plan is written up front, it becomes, well, "obsolete" once the specs change...and change they do...
Comments?
Posted by: Rajeev Ratra | August 14, 2008 5:15 PM
@Rajeev
"...it has been noted very consistently over the last 5 years that a lot of people (read prospects) cannot grasp the importance of or even the need for testing"
When a client hires you to create a software, they EXPECT your software to work! Why should they pay you extra for YOU to do testing to ensure that YOUR code works?
Posted by: reader | August 16, 2008 6:44 PM
Quality of the software is in the quality of Testing.
Posted by: shine | August 17, 2008 10:08 PM
I've not done any unit-testing, but I've read a lot about it and hope to on my next project. That said, this article doesn't mention the whole "red-green-refactor" mantra mentioned in so many other places, where the feature coding cycle is write stubs for the feature, write a test for the feature (will fail, i.e. appear "red" in a test tool), write the code for the feature in the stub so that the test passes, then refactor the feature code.
Another thing that strikes me in this article is the advice to refactor the test code. To me, if the test code is ensuring quality, altering test code that is known to work is highly risky. Certainly it must be done, but I would imagine that it should be approached with extreme care (pair programming, or senior developers only...). I would also refactor test source code separately of refactoring product source code to eliminate the chance that bugs are introduced.
Again I have no experience so I wonder what others think about these issues.
Posted by: Mike McG | August 20, 2008 4:23 PM