Summary
Passing null into methods is considered bad practice, but sometimes it can be very powerful.
Advertisement
When I was in school, I had a great calculus teacher. He knew how to spin off memorable phrases, phrases that made learning fun. Some of them have stuck with me to this day. For instance, one day he was going over a problem and he pointed out that if we were not careful we'd end up dividing by zero. "Dividing by zero", he said, "isn't that like violating the virginity of algebra?"
I used to think of that phrase whenever I saw a line of code like this:
new ADBDocument(null, defaultSize, palettes[n]);
Passing null isn't quite the same as dividing by zero, but it often conjures up the same feeling, that feeling that something is wrong, that we have to re-think something. If you are lucky, you already know that passing null is a bad idea. If you're not, you probably have an incredible amount of checking code in your code base. "Is this reference null? Yes? Okay, I have to throw an exception. No? Okay, I can actually use it."
If that's been your experience, you should stop passing null into methods. Me? I do it relatively often, and I don't really care about writing checking code when I do it either.
Shocked? Well, like everything else in life, context is important. I don't pass null in production code, I pass it in tests.
Let's look at an example. We need to test the paginate() method of a class named ADBDocument. ADBDocument has a single constructor which accepts three parameters:
public ADBDocument(ToolFactory factory, int initialSize, TypePalette palette) {
...
}
How many of these parameters are really needed to test paginate()? You don't know? Well, neither do I really. But, I know that I can write a test like this:
void testConstruct() {
new ADBDocument(null, 0, null);
}
and find out very quickly. If the parameters are really being used, I'll get a null pointer exception. If they aren't, I may very well get a nice live object that I can use when I start writing my paginate() tests:
This trick works well in most languages. The key to using it well is noticing that triggering exceptions when we are writing tests is no vice. If we trigger an exception, the test harness will catch it and we will learn something new about the code we are working with. If you work in a language whose runtime does not throw exceptions on a null dereference, this technique isn't very useful. Fortunately, runtime support is adequate in most languages.
Passing null is not a panacea. Most of the time, we do need fake or mock objects when we are trying to get older classes under test. But, there are cases where it makes it easy to get some quick tests in place, cases where fakes or mocks would be superfluous.
I'm inclined to think that ADBDocument needs another constructor or factory method that takes a single parameter. It's not that I mind that the test passes the nulls so much that I mind that the production code has a constructor that doesn't puke on nulls. If the constructor params aren't necessary for a valid object then why have them there?
> > <pre> void testPaginateSingle() { > > ADBDocument doc = new ADBDocument(null, 0, > null); > > doc.setText("\n"); > > doc.paginate(); > > assertEquals(1, doc.pageCount()); > > }</pre> > > I'm inclined to think that ADBDocument needs another > constructor or factory method that takes a single > parameter. It's not that I mind that the test passes the > nulls so much that I mind that the production code has a > constructor that doesn't puke on nulls. If the > constructor params aren't necessary for a valid object > then why have them there?
When I see this it is often it is because some other methods of the class are going to use them later.
By the way, that instance of ADBDocument can still blow up. We can call setText(String), paginate(), and pageCount() on it because none of them use what we are null-ing, but there are other methods that do use what we are null-ing and if we call them.. boom.
So, what we have in the test above is not a completely valid object, in that sense. But, we can write some tests to pin down its behavior, and then start to tease it apart. Often, externalizing those nullable constructor arguments makes sense, but it is nice that we can get there incrementally.
In your closing paragraphs you talk about NULLs vs mock objects.
But to me, especially after reading a few pages of the Sally & Ramone story, it appears that NULLs are mock objects. Sure, they have an extremely limited interface, but in your example case a NULL fulfills the requirements, i.e. that there is something there.
And if you consider NULLs to be first class mock objects, there is really no harm in using them for testing, right?
Unless the object really can be correctly constructed with the null parameters, the constructor should fail the test. Otherwise it seems the test is giving you a false positive by saying everything is fine with the constructed object. I forget about JUnit, but in NUnit you can have an ExpectedException attribute. It seems to me that the constructor should throw an exception if it needs these parameters to be non-null and if it doesn't it should fail the unit test.
> In your closing paragraphs you talk about NULLs vs mock > objects. > > But to me, especially after reading a few pages of the > Sally & Ramone story, it appears that NULLs are > mock objects. Sure, they have an extremely limited > interface, but in your example case a NULL fulfills the > requirements, i.e. that there is something there. > > And if you consider NULLs to be first class mock objects, > there is really no harm in using them for testing, right? > > /J
Excellent observation, Johan. I hadn't looked at it that way before.
> Unless the object really can be correctly constructed with > the null parameters, the constructor should fail the test. > Otherwise it seems the test is giving you a false > se positive by saying everything is fine with the > constructed object. I forget about JUnit, but in NUnit > you can have an ExpectedException attribute. It > seems to me that the constructor should throw an exception > if it needs these parameters to be non-null and if it > doesn't it should fail the unit test.
Matt, I'm operating under a slightly different philosophy of testing. These tests aren't there to show correctness, but rather to preserve behavior while making changes.
There are times when writing tests against a less than perfectly constructed object is useful. Particularly, when you are about to refactor (because the class has tangled dependencies) or make a change and the methods you need to touch do not use any fields set by the parameters.
There is a communication aspect to tests that seems like it could be clouded. For instance, if someone sees a null passed in a test, they could be led to believe that passing null is okay in production code. Unless, it is clear in the team just how horrible that is as a practice. If a team is already passing null all over the place in their app, they are in a world of trouble. If they aren't, it becomes a team convention, and there isn't really a need for null checks in team-written code. In general, once that rule is in place (don't pass null in production code) people don't see its use in tests as an example of something you should do in production code.
> But to me, especially after reading a few pages of the > Sally & Ramone story, it appears that NULLs are > mock objects. Sure, they have an extremely limited > interface, but in your example case a NULL fulfills the > requirements, i.e. that there is something there.
Um, er, I'm confused by your statement that there's something there with a NULL. The semantic point of a NULL value is the very fact that something does NOT exist.
In terms of Michael's point of consciously using NULLs in testing, looking at that particular usage as the ultimate degenerate mock object is interesting but methinks that the important question is why it's so hard to get objects constructed to a clean, useful state. Since nobody has mentioned them yet, one big help is NullObjects.
> > But to me, especially after reading a few pages of the > > Sally & Ramone story, it appears that NULLs are > > mock objects. Sure, they have an extremely limited > > interface, but in your example case a NULL fulfills the > > requirements, i.e. that there is something > there. > > Um, er, I'm confused by your statement that there's > something there with a NULL. The semantic point of > a NULL value is the very fact that something does > NOT exist.
Look at it this way. Imagine writing something like a null object where every method throws null pointer exception. It pretty much behaves the same.
> In terms of Michael's point of consciously using NULLs in > testing, looking at that particular usage as the ultimate > degenerate mock object is interesting but methinks that > the important question is why it's so hard to get objects > constructed to a clean, useful state. Since nobody has > mentioned them yet, one big help is NullObjects.
True, sometimes it is easy. Other times it is particularly hard. Sometimes interfaces don't extract cleanly, particularly in C++ when a client uses virtuals and non-virtuals and there is no tool support. Sometimes it is just because you don't really have to travel down that road yet to do what you need to do. Eventually, you may need to, but for the forseeable future, a null will suffice.
I get this a lot in legacy code. You want to make a change but you have to limit the scope of what you are going to do to get the code under test. If you don't, you get into a large cascading change.
> > Um, er, I'm confused by your statement that there's > > something there with a NULL. The semantic point > > of > > a NULL value is the very fact that something does > > NOT exist. > > Look at it this way. Imagine writing something like a null > object where every method throws null pointer exception. > It pretty much behaves the same.
Indeed. However, its meaning is NOT the same. I.e., the intent being communicated is, in essence, a lie.
> > In terms of Michael's point of consciously using NULLs > > in > > testing, looking at that particular usage as the > > ultimate > > degenerate mock object is interesting but methinks that > > the important question is why it's so hard to get > > objects > > constructed to a clean, useful state. Since nobody has > > mentioned them yet, one big help is NullObjects. > > True, sometimes it is easy. Other times it is > particularly hard. Sometimes interfaces don't extract > cleanly, particularly in C++ when a client uses virtuals > and non-virtuals and there is no tool support. Sometimes > it is just because you don't really have to travel down > that road yet to do what you need to do. Eventually, you > may need to, but for the forseeable future, a null will > suffice. > > I get this a lot in legacy code. You want to make a > change but you have to limit the scope of what you are > going to do to get the code under test. If you don't, you > get into a large cascading change.
I totally hear what you're saying. From a pragmatic standpoint, I too have BTDT. As I'm getting older, I'm finding that taking a bit of time to create a NullObject version is well contained in terms of effort and risk and the payoff is something that communicates honestly. Rather than looking at NULL as the base form of a Mock Object, I find that it's better to think of NullObjects as the base for mocks.
The part I like most about your ariticle is the idea of using a test to learn about the code under test. If null works, great, you move on. If not, you've learned something, which is another sort of progress.
Personally I've gotten in the habit of validating the input to all of my constructors and most of my setters. I do this because I find a nice stacktrace and message saying something like "foo must not be null" to be a useful thing to encounter when something unexpected happens instead of random behavior, and then all my other code can be written w/out a lot of checking for bad parameters.
So I suppose if that null trick did work in the code I might have learned something else: I probably need to come back and refactor this constructor.