If you can send in different types of data to your methods, and some of those types have a chance of working incorrectly, then you need to be testing those possibilities. Plain and simple.
However, if that is really the case, I think the actual problem is that your code is not set up cleanly. I smell problems.
What I'm going to do is first talk about how to refactor your code so that it can easily be tested. Then, I'll explain how you might approach testing a system like the one you've described, if you have to (my hope is that you don't).
Code Refatoring (the best solution)
First, lets talk about why your methods allow so many different types to be sent in when they don't work consistently with them. This is an obvious code smell that should probably be addressed. My guess is that you need to refactor your code so that this isn't a problem in the first place. An approach I would take might look like this:
- I'm going to make sure that all objects sent to this method have a common interface
- I'm going to have the method only use this common interface to do what it needs
- The objects themselves will each have their own implementation of the interface that will always work for that type of object.
If you do this, you can now easily test the method and see that it is correctly using the common interface. You now know that this code is working no matter what object you put in. The interface is defined, so the correct action is taken.
You will also test each of the objects that implement the interface, and make sure that they have implemented it correctly for the type of object it is. Once you've tested that, you are done, you know everything is working without a million different tests testing each object with each type.
To illustrate, I've put together some pseudo-code to demonstrate. Lets say your code looks like this:
function output_to_file (shape x) {
if (x is a triangle) {
// output to file
} else if (x is a square) {
// output to file
} else if (x is a pentagon) {
// output to a file
}
}
Then your tests are going to require you to send every different type of shape to the output_to_file method. If you have a whole bunch of methods like this, and a whole bunch of shapes, you will need a whole bunch of tests. Every time you add a shape, you need to modify your output_to_file code and add tests. No fun at all. Not good code.
Instead, you need to refactor your code to look more like this:
function output_to_file (shape x) {
text = x.get_text_representation()
save text to a file
}
Instead of handling the details of each shape, output_to_file just uses a common interface: the method get_text_representation.
Now to test output_to_file all you have to do is check that it is calling get_text_representation and putting the results in a file. If it is doing that, it is working. The output_to_file method now follows the "open/closed" principle. It is open to new types of objects being sent in (as long as they support the interface), but closed to the need for additional changes.
This means of course that our shapes need the get_text_representation function. But that is simple:
class triangle {
function get_text_representation {
// return whatever
}
}
class square {
function get_text_representation {
// return whatever
}
}
//and so on
Now we test the get_text_representation method on each of those objects to make sure that they are working like they should. That method might be used in a variety of places, but we don't really care, as long as it is working right we know that other methods relying on it will work right.
By setting up our objects in a different way, we've eliminated the need to test a whole bunch of different objects/method combinations. We test that the method is using the correct interface, and we test that the objects implement the interface correctly. Nothing more is needed. Yay!
Testing More Cases (the less than best solution)
Now perhaps you've been reading this so far, and you're thinking, "well that is nice and all, but I can't do that, he doesn't understand my situation."
Ok, first, try really hard to refactor your code. I'll be worth it. Did you try? Ok then, I'll give you benefit of the doubt and assume you are in some type of complicated situation that can't be refactored the way it should be (but I still don't believe you).
Here's the deal: you have to test each possible different type of object with each different method. You just have to. If some of those object/method combinations can cause error, you must test. Sorry.
Now, if it seems tedious or repetitive to be testing the same methods with different types of input again and again, you can automate the process somewhat. A lot of people forget that you can code your tests to do repetitive things for you. Let me give an example of how you might set up your test:
test_objects = {new triangle, new square, new pentagon .... }
function test_output_to_file {
foreach (test_objects as x) {
output_to_file(x)
assert everything is good
}
}
This is really the best you can do if you can't refactor your code correctly. Is it tedious, yes. Is it clean, no. Refactor your code if you can, this isn't a good alternative.
Conclusion
In conclusion, the problem you are describing -- being able to send many different types of data to many different functions, some of which are not supported or buggy -- is probably an indication that your code isn't clean. Testing isn't the problem, and refactoring your code is the solution.
If for some reason refactoring isn't possible (I find that hard to believe!) then you are going to have to bite the bullet and test every combination. Make some structure that allows you to do that a little more easily.
I highly recommend anyone interested in creating clean, testable code to read the book Clean Code by Robert C Martin.
I tend to side with your friend because all too often, unit tests are testing the wrong things.
Unit tests are not inherently bad. But they often test the implementation details rather than the input/output flow. You end up with completely pointless tests when this happens. My own rule is that a good unit test tells you that you just broke something; a bad unit test merely tells you that you just changed something.
An example off the top of my head is one test that got tucked into WordPress a few years back. The functionality being tested revolved around filters that called one another, and the tests were verifying that callbacks would then get called in the correct order. But instead of (a) running the chain to verify that callbacks get called in the expected order, the tests focused on (b) reading some internal state that arguably shouldn't have been exposed to begin with. Change the internals and (b) turns red; whereas (a) only turns red if changes to the internals break the expected result while doing so. (b) was clearly a pointless test in my view.
If you have a class that exposes a few methods to the outside world, the correct thing to test in my view are the latter methods only. If you test the internal logic as well, you may end up exposing the internal logic to the outside world, using convoluted testing methods, or with a litany of unit tests that invariably break whenever you want to change anything.
With all that said, I'd be surprised if your friend is as critical about unit tests per se as you seem to suggest. Rather I'd gather he's pragmatic. That is, he observed that the unit tests that get written are mostly pointless in practice. Quoting: "unit tests tend to be commented out rather than reworked". To me there's an implicit message in there - if they tend to need reworking it is because they tend to suck. Assuming so, the second proposition follows: developers would waste less time writing code that is harder to get wrong - i.e. integration tests.
As such it's not about one being better or worse. It's just that one is a lot easier to get wrong, and indeed very often wrong in practice.
Best Answer
The only way to practice is to just do it. I'm still learning how to write effective unit tests myself. Unfortunately, you need hours behind the wheel to get true feeling for what good unit tests looks like. The only shortcut I could think of is working 16 hours a day instead of 8 and you'll learn twice as fast, but only until fatigue sets in.
Few things I'd suggest:
Make sure you are working on a task that is not time critical. Time pressures and the feeling that you must be done right away, is what typically causes people to take short cuts and either a) not produce any tests, b) produce very little or c) produce tests which are very hard to maintain because you never took the time to refactor them.
Approach the task of writing unit tests with the same requirements for code quality (if not more strict) as you would production code. Some people think that unit tests are not being released, so it's not as important to follow good practices and then they end up with tests which are very fragile and extremely hard to maintain.
Allocate enough time to not only write the test but also any helper classes that would make yours (and others on your team) life easier. From my own experience, it seems that people tend to take shortcuts when writing tests because certain tasks are too long to code properly and because it takes even longer to factor code out into a library, they end up copy/pasting code between unit test projects. I started a unit test helper library for our group with the intention that it would speed up development of future tests (some common functionality in the library: simple code for recording/verifying mock object actions, code for working with files to help unit tests which need to feed external data, standard hooks to override win32 calls...).
Get into a habit of practicing TDD where you write tests before the code. At first it seems like unit tests are a lot of overhead, but since you must exercise the production code you've just written anyway, TDD more than makes up for it because it becomes incredibly easy to verify functionality of the production code so even when not considering all the long-term benefits, it will immediately start paying back the time it took you to write the tests.
This outlines how to learn to write good unit tests. If you want to simply "know" what a good unit test is, there's a number of really good books out there. (I'd start with The Art of Unit Testing)