There always seems to be a lot of discussion regarding the whens and hows of unit testing. These can be challening topics when you’re getting started with unit testing, and I wanted to document some of the rules, guidelines, and lessons-learned that I apply when writing my own tests.
Keep it simple.
If it seems like testing a piece of functionality is really hard, there’s probably an easier way. One of the keys to writing good unit tests is having testable code. (Duh, right?) Well, one of the best skills that you can have is the ability to take a block of untestable code and transform it into a well-tested masterpiece. Doing this isn’t as hard as it may seem. The secret is identifying the natural seams in the code and extracting them into testable and/or mockable objects and functions. Divide and conquer!
It’s not all or nothing.
Sometimes when you’re working with legacy code that doesn’t have unit tests, it’s easy to follow suit and not write tests yourself. Just because tests don’t exist for existing code doesn’t mean that you can’t write tests for your changes, though. If you write a test to confirm a change that you make, you’re future-proofing your change. If somebody comes along later and messes up your change, your test will fail even if it’s the only test in the whole world.
It’s been said before, but it’s true: one test is better than no tests. It’s also unlikely that you’ll ever go back and unit test everything in a project hat wasn’t tested previously, so just start already!
Test what makes sense.
Code coverage is great, and 100% is a great goal to strive for, but I don’t think it’s realistic in most situations. If you’re writing a test where you’re replicating the entire contents of the function you’re testing to create expectations and asserts of the test, it may be a bad test for two reasons. First, you don’t feel good about the test you’ve written. Second, you’ve made your code more rigid because changing the actual function will require you to make the same change again in the unit test. Blech!
Instead, strive to write tests that you believe are meaningful and useful. A good example of this would be translating a person object. It may not be useful to test that ObjectA.LastName equals ObjectB.LastName and ObjectA.FirstName equals ObjectB.FirstName, but it may be worthwhile to write a test that verifies that ObjectA.SsnAsInteger populates correctly into ObjectB.SsnFormattedString. It’s hard to come up with a good, brief example of this, but what I’m trying to get at is that you shouldn’t spend hours and hours working on tests that you yourself see no value in. Grab a peer and look at it together. Maybe they can help you see the value or come up with a better way to test. Or maybe you’ll both agree that the code in question simply doesn’t need to be tested.
Don’t write random value generators and object populators.
I’ve gone through this phase, and I’ve seen others go through it. I want to save you, though. Don’t do this. I know that it seems like a good idea. This should work for any string of any length, so I’ll just write a function to populate my object with a random string of random length! The problem is that when this test fails randomly 1 out of 50 test runs, you may not be able to re-create the situation. I believe it’s better to write a specific test for your specific situation. If you need to test multiple scenarios, write multiples tests.
One of my least favorite unit testing experiences comes from working with a project whose unit tests were written by a code generator that randomly populated complex source business objects, manipulated them, and then used comparison-via-reflection to verify correctness. The problem is that these tests were nearly impossible to maintain and troubleshoot when they failed.