Introduction
I find myself using default values much more in unit tests than in production code. That is, I do it more often, and use more parameters with defaults per method. This isn’t because default values are bad, but they are often particularly useful when setting data up for unit tests. It makes the tests shorter and so easier to read and understand. I’ve done this for tests written in C# and T-SQL, but I imagine it could be useful in any language that supports default values for parameters.
Driver
One method in production code will almost always need several unit test methods to test it adequately. These tests will probably need data to be set up first, and the data for the different tests for a given method might be related like this:
All the tests have a core bit of data in common, and then add to this in some way that is specific to the test.
For instance, all tests for a given method need a customer to be created with an order, but for one test the order will need to be pending, and for another the customer will need to have different addresses for billing and delivery etc. If you use parameters with defaults, the set-up parts of the tests will look like this (using C# and MSTest, but hopefully the general principle is clear beneath the specifics):
[TestMethod] public void testOne() { PrepareData(orderStatus: OrderStatus.Pending); ... } [TestMethod] public void testTwo() { PrepareData(singleAddress: false); ... }
The test methods should have better names, and the ellipsis would be filled in with the rest of the tests’ definition. The definition of PrepareData will vary by test class – creating customers and orders in one test class, creating products and discounts in another test class etc.
The nice thing about this approach is that it makes the point of each test more obvious. How does its data need to be different from normal? That will give a strong clue as to why the test is important.
Implementing
In the example given above to do with customers and orders, PrepareData might be implemented something like this. Note that PrepareData isn’t itself a test, but is called from tests (hence it has no [TestMethod] attribute).
private void PrepareData(OrderStatus orderStatus = OrderStatus.Complete, bool singleAddress = true) { Address billing, shipping; billing = new Address("a", "b", "c"); shipping = singleAddress ? billing : new Address ("d", "e", "f"); _customer = CreateCustomer(billing, shipping); _customer.AddOrder(orderStatus); }
orderStatus defaults to complete, and singleAddress defaults to true. In the case of orderStatus, overriding the default merely means that when the order is created, it has the override rather than the default value as its status. However, in the case of singleAddress, different code is executed depending on whether it has the default value or an override. (The values passed to the Address constructors are dummy, just to show that the two addresses are different.)
This is a fairly simple example with a lot of the normal detail missing, but I hope it gives you an idea. You might have a combination of parameters that have no defaults and other parameters that do have defaults. You might have more than two parameters with defaults, and so on.
Why not objects?
You could probably do this via objects set up differently for the different test methods, e.g. passing different values to constructors, setting properties differently etc. This is valid, but in my experience this hasn’t seemed the most natural way to do it. Each test tends to be shallow as it’s testing a single method rather than involved business logic in production code. So there’s not much passing around of each bit of data (which is something that would suit objects). Also, you might want logic to be different rather than just values, e.g. create a list of 3 things rather than creating no list at all. However, your mileage may vary, and objects might be the better way to go for your tests.
Conclusion
Test code often doesn’t receive the same care and attention as is paid to production code. While it’s possible to gold plate any kind of code, I’ve found that test code often repays a bit of TLC. Tests become shorter, easier to understand and to change.
Also, you need to ask yourself: what will find mistakes in my tests? The tests are there to find mistakes in your production code, which is great, but what finds mistakes in your tests? While looking at test coverage helps and mutation might be an option, there’s still a lot more reliance on eye-balling them than with production code, and so making tests shorter and more understandable will make that process easier and more productive.