Learning Resources
Unit Testing Applications
Testing helps you find errors in your code.
Of course, you only find errors if you have written a test that exercises the particular functionality that's broken, but once you start writing unit tests, you may be surprised by how many errors you find. As you write test cases, you'll think, "Gee, I wonder whether I handle this case correctly," or, "Hm. I don't think that I handle this error correctly." When you're using a testing framework, you can simply add more test cases. The ease of adding new tests and running them encourages more thorough testing. Try it for a week, and you'll see what I mean. Many developers who have gotten in the habit of unit testing find programming without writing unit tests to be difficult and somewhat frightening. Programming is to driving like unit tests are to seat belts.
Testing helps you write better code.
In addition to finding errors in your code, unit testing can help you during the initial development of your class or module's public interface. Unit tests force you to think about how another developer will want to use your code while you're writing that code. This shift in focus can help you present a smaller, cleaner interface to your classes and modules. This benefit is most often associated with test-driven development.
Writing test cases will save you time later.
Imagine that you don't write test cases in code. You finish some module, and you start testing it at the interactive prompt. You realize that there's a tricky case your code doesn't handle correctly. A quick test reveals that you're right. You fix the module and move on. Later, you decide to rework the implementation of your module. Are you sure you didn't forget that tricky case? Can the new code handle it? If you had written test cases, you could just rerun the same set of tests (as long as the interface didn't change). As an added bonus, you can run your tests often so that you may find a bug after you've only written a few dozen lines of code. If a test suddenly fails, you know that it was introduced in the little bit of code you just changed. Not only is it easier to detect when you've introduced an error, but it is also easier to find the code that causes the error, reducing the time spent debugging.
Unit tests provide immediate feedback on your code.
When you're writing code deep in a library or in a server side module for a user interface, a unit test gives you feedback as you work. You don't have to wait until after code in a separate part of the application is written before you can test and know whether your code works. Those little "throw-away" programs you may be writing to test your code become reusable unit tests attached to a consistent testing framework.
Test cases document intent.
Test cases provide minimal documentation about what you think your code should do. Tests don't necessarily explain why your code should behave the way shown, but they can explain how it should behave. In fact, some developers write their tests before they write their code. The tests define how the unit should behave. In this case, tests always fail at first. The programmer writes code until all of the tests are passing again. This style of programming called test-first programming or test-driven programming. Test-driven development also ensures that you have unit tests for all of your code since all of the code was developed to satisfy a test.
Automated testing is an extremely useful bug-killing tool for the modern Web developer. You can use a collection of tests – a test suite – to solve, or avoid, a number of problems:
When you’re writing new code, you can use tests to validate your code works as expected.
When you’re refactoring or modifying old code, you can use tests to ensure your changes haven’t affected your application’s behavior unexpectedly.
The idea of unit testing is to enable completely modular code development. You probably already know that modular code is the way to go, as it encourages reusable and clearly focused designs.
If you attempt to test an entire system as a whole, then many different parts have a possibility of failing. In any reasonably sized system, it would be nearly impossible to account for every possible problem. Also, when problems are found, it may be difficult (or nearly impossible) to locate the responsible code.
However, if different portions of your code are completely separated from other portions (i.e. they don't rely on each other), then you can use unit testing to test a subset of your larger system. It is much easier to detect bugs in a small amount of code, as opposed to a large amount.
In the example you presented, there are two super-portions of your code: the part that retrieves data from a database; and the part that processes that data. If you attempted to test the two together, your test could become riddled with failures because of the attempted database access. There are numerous networking and other I/O issues that could come up when reading the database. Especially in extreme cases, this can make it very difficult to test the processing portion of your code. Similarly, there could be bugs in the processing code which, for some reason, are not clearly located within that part of the code.
The solution is to use unit testing. Rather than test the retrieval and processing together, you separate them. You can feed any data you want to the processing portion to see if it functions correctly. If something does go wrong, you know that the problem is a processing issue, and not related to reading the database. Similarly, you can make simple tests to ensure that you are attempting to access that database in a correct manner. Any problems that arise this time are sure to be related to the database reading.
So unit testing is really invaluable in testing code for robustness. Also, it encourages a design pattern which is greatly encourage. Specifically, developers wising to use unit tests must design in such a way that their code can be separated into modular units. For example, a function that relies heavily on global mutable state is inherently less unit-testable than a function which only relies on it arguments to produce a result.