We start by answering the questions that weren't asked: why do we test and why should we test?
Testing increases developers' confidence to introduce new changes without breaking existing functionality. Anecdotally, it also reduces overall project completion time because less time is spent in the testing phase because bugs are found and dealt with sooner. It would be a tall order to define all the reasons we should test in one paragraph of one tiny blog.
There are usually ancillary reasons why we do test. Someone else forces us to, or we choose to, or we need some experience for the CV. People's motivations are endlessly creative. Heck, I can imagine many a political reason for coding unit tests.
We can test pretty much anything, but it doesn't mean we should. As we become more familiar with various testing tools and APIs we increase our range of what can be tested, but we should focus on the application of our knowledge to what we should be testing.
What should we test? Best question yet. From the point of view of the person who's paying you to develop software: we should test things that make our project more attractive than a (perhaps imaginary) competitor's project that doesn't employ unit testing. Do we deliver it faster? Do we deliver it with less bugs? Is everyone on the team happy? Did we get to do a little bit of CV++ along the way?
What do we test? In an ideal world, we test the objects we write. Seeing as I come from an object-oriented background, I'd say we test the state and behaviour of objects. We test state by inspecting the values of properties. We test behaviour by inspecting the return values of methods, and sometimes by ensuring that we're interacting correctly with other objects.
- State: make assertions against expected and actual property values
- Behaviour: make assertions against expected and actual method return values
- Interactive behaviour: make assertions against the objects with which our objects interact - what we're testing is that we're actually interacting with them in the expected manner
Mock objects are the flavour of the week when it comes to testing interactive behaviour - we set up expectations on a mock object, interact with it, and then validate our expectations. They are there so that we can test our object under test is correctly interacting with it's surroundings.
Stub objects don't fit anywhere in this list of tests because we don't assert against them... we only use stub objects in order to allow our tests to compile and execute. We choose to use stub objects because we can control them; control their dependencies. They are an important part of any isolation framework. We don't want our logging method to keep writing disk files or sending e-mails while our tests run, but we don't want it to throw NullReferenceExceptions either. We definitely don't want to code our application to continuously check if the logger is null (this would be tedious and error prone), nor would we want to introduce special branching logic to not perform logging under test conditions. We could code up our own NullLogger : ILogger that does nothing but implements all ILogger's events/properties/methods to do nothing, but this would be fragile and often a waste of time when functionality is added to or removed from the ILogger interface a month down the line. Good frameworks like Rhino.Mocks allow dynamic stubs to be built on the fly without you ever having to care about creating and maintaining your own NullLogger implementations. Saving us time, saving us money.
Just because we can test state and behaviour (even interactive behaviour) doesn't mean we should. There needs to be justification. Does it make the application development experience quicker and better? Does it reduce overall costs? Does it allow you to sleep better at night (no, not because you are so exhausted from fighting fires and fixing production bugs!)? Are the tests valid? Will they help someone newly joining the team to understand what's going on? You can be as creative as you want in your reasoning, but without proper justification, you may just be wasting time writing and running tests.