Unit tests serve several related purposes:
- Verify that requirements are being met;
- Develop more precise specifications for the code;
- Test functional units in isolation;
- Make regression testing easier (i.e., make sure you don't break working code).
Verify that requirements are being met
For example, I'm working on some code to display transaction history for a bank account. I have a requirement that if I get a history record where fee, escrow, or interest amounts are greater than 0, I break those out into separate entries with their own transaction codes and descriptions. One method in one class is responsible for this, so I write code that calls that method directly (or, if that particular method isn't publicly visible, I call the method that calls the method under test) with known inputs for each case, and then check the results for each of those inputs to verify that the right sub-transactions have been created, with the right data.
Develop more precise specifications for the code
As you're developing your unit tests, you will inevitably find that the requirements do not cover all possible cases. For example, I have another requirement that says if the transaction code is X, do one thing, and if it's Y, do something else. But there's nothing telling me what to do if the transaction code isn't either X or Y. It's a hole in the specification that needs to be filled, in this case by the architect. It's a good way of checking your work.
In a way, the unit test becomes the specification for the code.
Test functional units in isolation
This is a biggie. Unit testing allows you to test your code as you're writing it; you don't have to wait until all the pieces are in place before testing can begin. If you're testing a piece of code that relies on another class or method, you can write up a dummy implementation of that class or method that returns known values or behaves in a known manner, so that you can test specific scenarios.
It also encourages you to factor your code more aggressively, reducing dependecies between components, which makes the code easier to maintain and extend.
Make regression testing easier
The nightmare scenario is releasing a patch that breaks working code. By running unit tests as part of every build, you guard against accidental breakage because the associated test will fail. You can then analyze the failure and see if the break is real (i.e., someone coded in a bug) or if the requirement/specification changed, and adjust the unit test accordingly.