Last week, we began looking at automated unit testing. I gave you a tour through my transformation from manual tester to a guy that thinks everyone should practice TDD.
This week, I want to have a look at unit testing best practices.
I’m not just going to give you a list of things you might read in a book – although you can probably find them in several books. I’m going to lay out for you a part of my personal list of must-do practices.
Unit Test Should Be Committed With the Code They Test
It’s common practice on too many teams that the main code is written by a more seasoned developer and the tests are written by someone less seasoned.
If you’re practicing this on your team, stop it right now!
The excuse for doing this is that it helps the less seasoned developer learn the system without doing possible harm to it. That’s a poor excuse, and it’s actually a bad example to set for those less seasoned developers.
However you write your tests (before, at the same time, or after the main code), make sure you only commit the main code once the tests are written, account for all requirements, and pass.
Why? I’m glad you asked!
Tests Make A Developer Go Faster
It’s counter-intuitive to anyone that hasn’t bought-into testing, but writing tests does make you go faster!
Think about how much hemming and hawing you do when you write code without tests. You put some pretty heavy thought into what you’re doing, because you want to make sure you get it right.
You build a lot too. And, you deploy a lot.
You wait on your server to start-up a lot.
Imagine you put all that time you spend hemming and hawing and waiting into writing a test. Not only could you test each class in isolation, but the test would act as a check to ensure you implemented all the business rules properly without the build, deploy, test cycle.
I’m not saying you shouldn’t think carefully about what you do; I am saying it’s easier to take too long doing it when you have nothing but your eyes and brain to judge if you are done.
Tests Make a Team Go Faster
When tests are written with the code they test, defects are dramatically reduced. Without those defects, developers are able to complete more features instead of fixing things they should have gotten right the first time.
You’ll still have defects, but – over time – you’ll have fewer and fewer.
Test Through an Interface
Imagine you’re building a typical Java service class that manages business rules for a Person entity. You likely have an interface (PersonService) and an implementation (DefaultPersonService).
Your test class likely depends on both the interface and the implementation. This is so you can ensure the implementation adheres to the interface by only writing tests against the interface methods. I say likely because if you are using Dependency Injection you can avoid having a direct dependency on the actual implementation class.
Your design probably looks similar similar to this:
Test Public Methods Only
I mentioned above that you should only test through interface methods. I said it was so you could ensure that the implementation adheres to the interface, but there’s another reason for this too:
If you have to resort to testing protected methods, the class you are testing probably needs to go through mitosis. Then, you can write a test for the new class.
This is one of the ways that tests and testing can influence your design, if you let them.
Also, when you start testing protected methods, you start letting the tests know too much about your implementation and they become brittle. This works against refactoring – and isn’t helpful to your goal of making sure your application is solid.
If you make refactoring difficult, it won’t happen. It’s just human nature. So, don’t test through protected methods!
And, don’t even think about testing private methods. That’s like counting to five when throwing the Holy Hand-Grenade of Antioch – it’s right out!
Each Logical Path Requires One or More Test Methods
If you have no flow-control statements (conditionals, such as if – or loops) in your main code, you have one path to test and one test method to write.
Add an if statement, and now you have to multiply by two. Loops split in this same way, and can be further complicated by inner loops or other conditional statements.
Each path needs one test method.
It’s About Coverage, But Only Partially
This is the notion of code coverage, and a code coverage tool can help you ensure that each logical path is covered by a test.
Code coverage doesn’t tell the whole story, but having a coverage that’s in the mid-to-high 90s is a great place to start. Those practicing TDD will expect even higher numbers, if not 100%.
Legacy applications (applications that have no unit tests) should simply set the expectation that coverage increases with each commit.
I’ll talk more about why reaching 100% coverage may not be enough at a later time.
One Path Should Have One Outcome
Make sure that each test method only calls and tests one method on the class being tested.
You may have multiple asserts – checks to ensure something happened a specific way when the tested method was run. That’s perfectly fine, so long as each check is required to ensure one single outcome.
For example, if I call the create method on PersonService, a validator should run to make sure the passed-in Person instance is valid. If there are validation errors, those errors should be communicated in some way.
There could be multiple validation errors, and each should be checked for.
Even if everything goes fine in this example (there are no validation errors), you still need to test that delegation to the validator occurred and that control was then delegated to whatever storage mechanism you’re using (DAO, Repository, Gateway – Do we have enough names for these yet?!)
So even in this simple example, we have two test methods: one for things going perfectly well, and another for when there are validation errors.
Beyond 100% Coverage
I’m not going to say a lot about boundary testing right now. I’ve already said I would punt on that.
I do want to point out the fact that you can have 100% coverage and still have legitimate defects filed against your tested code. Don’t think that 100% coverage means you’re bulletproof.
Again, we’ll circle back to this in the future.
Wrapping It Up
We’ve covered a lot of unit testing best practices, but there’s even more to know. Too much to cover in one sitting.
People write entire books about this stuff!
It’s kind of hard to read a lot of this at one go though, so I’m stopping here. Perhaps I’ll do a screencast for next week and toss in a little TDD. We’ll see how it goes.
One of the core concepts in test testing to isolate, exercise, and measure. We isolate by testing one code path at a time, exercise that one code path, and then measure the outcome.
The more specific your tests are, the more valuable they will be. The value comes in when code is changed in a way that causes tests to break.
Learning to test well is not easy, but it’s a skill worth spending your time to learn.
I hope this week’s article has helped you learn a little more than you knew – or has helped you understand the thing that has been keeping you from starting to learn unit testing. If you did find it useful, I hope you’ll help me out by sharing this article with your own network.
Until next week: keep your tests passing and your code clean!