A Case for Automated Testing
Discipline and Testing
Disciplined programmers write tests while developing code. Some even refactor their tests when certain situations arise. Imagine this one. You have just written a method that takes two integer arguments, adds them and subtracts seven before returning the result.
Your code might look like this:
int addThenSubtract7(int x, int y) { return x + y - 7; }
When you test this code — either before, during or after you write the method above — you will likely write tests that exhibit some property about the method under test. For example, when one argument is the negation of the other, you will always get back -7.
assertEquals(-7, addThenSubtract7(0, 0)); assertEquals(-7, addThenSubtract7(1, -1)); assertEquals(-7, addThenSubtract7(2, -2)); assertEquals(-7, addThenSubtract7(3, -3)); assertEquals(-7, addThenSubtract7(4, -4)); assertEquals(-7, addThenSubtract7(5, -5));
You might refactor in this scenario:
assertTrue(negationBecomesNegative7(0)); assertTrue(negationBecomesNegative7(1)); assertTrue(negationBecomesNegative7(2)); ... public boolean negationBecomesNegative7(int a) { return addThenSubtract(a, -a) == -7; }
Indeed, you might use a loop for your input values:
for(int i = 0; i < 100; i++) { assertTrue(negationBecomesNegative7(i)); } ...
Specifying Properties
There are other properties about the method addThenSubtract7. For example, if you switch the arguments, the result is unaffected. In fact, this property comes up a fair bit so it even has a name; commutativity. You could come up with a similar scenario as that described above through a process of repeating assertions with different values, refactoring a bit and generally trying to test as rigorously as possible with minimal effort and permitting future maintainability of both your code and your tests.
Eventually, you will have written properties about your method such that there is no reason to write any more properties, since they would be implied by your existing properties. For example:
- You could write a test for
addThenSubtract7(0, anyValue)results inanyValue - 7. - Then you could write a test for commutativity;
addThenSubtract7(someX, someY)results in the same value asaddThenSubtract7(someY, someX).
Now, is there any point writing a test for addThenSubtract7(anyValue, 0) results in anyValue - 7? Isn’t this implied by the test for commutativity? Right, this test would be redundant.
Ultimately, you could aim for an unambiguous specification of your method under test. However is there a nicer way to write all these tests?
Let’s Automate it!
Going back to the first property negationBecomesNegative7, don’t you really just want to say this and then let the rest be taken care of by clever automation?
for all int values named a. then addThenSubtract7(a, -a) == -7
This is the job of Automated Specification-based Testing such as that which is implemented by Reductio for Java. Reductio allows you to write the above expression using standard Java syntax:
Property p = property(arbInteger, new F<integer, Property>() { public Property f(final Integer a) { return prop(addThenSubtract7(a, -a) == -7); } });
What is going on here? We are passing in our generator of arbitrary integer values (arbInteger), then an anonymous inner class that asserts our property for our free variable (the one called ‘a’).
OK, it can’t be this easy can it? No, sorry, it can’t. There is an enormous of work to do next. Ready, here it is:
p.check();
With all that heavy lifting taken off you by the test automation, you should now have plenty of time and energy to get on with delivering your software to your client or maybe reading news blogs if that is what you prefer
Shrinking
When you run the test automation, you receive a result of success after running 100 (by default — adjustable of course) tests or you receive a failed result with a counter-example. That is, you receive a value for your free variable (a) for which the property was not true.
Does it stop here? I mean, this is great and everything, but surely there’s not more help that can be provided by clever test automation software? There are a couple more clever things that can be done.
First is the shrinking of a counter-example. Imagine there was a bug in the addThenSubtract7 method and you received a counter-example of 1789234. What’s so special about this number? Well, that depends on the bug, however, maybe this is just the first counter-example that was found. Imagine, however, that the bug also exhibited itself (i.e. the property fails) with a counter-example of 2. Isn’t this a more useful counter-example? Wouldn’t you prefer that this one was reported and not that big, insignificant number?
So that’s another great feature of test automation; it can report back to you useful counter-examples in the event of a bug. When you perform manual testing as above, you might remove a couple of your assert methods to help narrow down the bug. This is the hard way! This very act can be automated.
Imagine you wrote this property (a + b == a - b):
final Property p = property(arbInteger, arbInteger, new F2<integer, Integer, Property>() { public Property f(final Integer a, final Integer b) { return prop(a + b == a - b); } });
It turns out that this property is true for some values such as a == 0 and b == 0, but false for others:
a = 1, b = 15a = 147, b = 92582a = 5923, b = 12
But which of these counter-examples is going to be most useful to you, the tester? Any of the above? How about this counter-example: a = 0, b = 1? Isn’t this more useful to you than any of those above?
Good test automation software will report a reasonably small counter-example in the event of the falsification of a property.
Automated Mocking
OK, is there more? Yes sorry to keep your attention. Test automation software will also generate instances of an interface for you. That’s right, you can quantify across an interface in your property expressions. This is a bit like mocking, except not so manual; you just pass in the arbitrary generator (Reductio provides them of course) just as we did above and you can wipe your hands clean of the remaining manual effort otherwise.
You might want to write a property where you say something like, “for all instances of my interface or class, then such and such…”. This might be an instance of the HttpServletRequest interface or perhaps one of your own classes. The good part is, no more manual mocking.
Want to Learn More?
You can read the Reductio Manual or perhaps look at some code examples. If you’re a fan of the coming Java 7 BGGA closure syntax, you might prefer these code examples. Perhaps you’re into higher-level programming and prefer Scala code examples. Whichever you choose, please feel free to jump on the Reductio mailing list or chat channel and ask your question, no matter how silly it seems to you.
June 6th, 2008 at 10:00 pm
I didn’t quite grasp the section about mocking. Do you mean to say that I can hand Reductio MyCleverInterface.class and it will find/construct implementations of it? You said “you just pass in the arbitrary generator (Reductio provides them of course)”, which leads me to think you mean that.
Also, I think you could pick an example that is easier to identify with for programmers, though I personally found a + b - b == a quite good fun with students (especially where a is a float).
June 7th, 2008 at 12:37 am
“int addThenSubtractSeven(int x, int y) {
return x + y - 7;
}
…
For example, when x and y are the same value, you will always get back -7.”
Are you sure?
Automated tests are interesting, but they are just code. Sometimes bad code.
June 7th, 2008 at 1:57 am
Cute, but largely academic. Could you illustrate this at a bigger scale, where you are trying to tests objects (not primitive types) and in integration scenarios too (database access, remote calls, GUI, etc…).
To make things concrete: how could I verify that a certain row has been inserted in the database after I update an object?
June 7th, 2008 at 2:13 am
Your first example doesn’t really work, you shouldn’t be getting -7 back, you’d be getting.. the product of the two ints minus 7.., so 1 and 1 would return -5.
Plus, really, your unit tests should be testing for different return values, it’s pretty pointless if you do 100 tests for the same return value.
I agree though, testing is important and automating testing is important. There should also be separation of unit tests like these and actual blackbox testing that is also automated. Yes, I think developers should do their own blackbox testing during a build cycle(hopefully continuous)
June 7th, 2008 at 3:03 am
You probably want:
addThenSubtract(a, -a)
June 9th, 2008 at 6:57 am
Hi Ricky,
Sorry for my delayed response, your comment escaped my attention.
Yes, Reductio will generate implementations of an interface (or a class), however, there is often a small amount of work to do on your behalf. You might even consider this work to be “boiler-plate” since it is so systematic and requires no thinking. Ultimately, you will require an Arbitrary<T> where T is your interface or class.
Classes can be shown to be convertible to/from zero or more transformations and since Reductio can construct arbitrary transformations (see
reductio.Arbitrary.arbF), then it is simply a matter of converting them to the appropriate class, type using the overloadedGen.bindmethod, which allows up to 8 transformations, however, if more are required, then they can be written using theGen.applymethod. That is to say the overload ofGen.bindthat accepts N transformations can be written quite easily using theGen.bindfor N-1 transformations plus apply. I understand that this might sound complicated, but it’s really easy to do; perhaps a future tutorialPS: Thanks to the others who corrected my mistake.