Unit Testing Mu, Part 3
The current build of Mu now contains tests for MuBitmapImpl.
mu, build 12
In the back of my head up until now, I had some lingering feeling that unit testing a bitmap class was going to be hard, or at the very least annoying. I don't know why I felt that way, but in hindsight I think I was trying to talk myself out of doing something that I'd never done before.
Learn By Doing
I don't know if anybody else has this problem, but for some reason I have this habit of wanting to be able to reason something out fully in my head before attempting it. I want to have not only a grasp of the situation, but a firm one.
At my job, I've trained myself not to do this. It's easy at work, really. I don't have a close personal interest or concern for the software I create. I have a professional interest in writing good maintainable code to the best of my ability, and to provide a solution our clients will be pleased with, yes. But business applications are not something I write in my spare time for fun.
At my job, as with many jobs, you need a good "knee jerk" reaction. You can't spend too much time overanalyzing or preparing. You need to make a decision, and then run with it. Wrong choice? That's life. Make another one and start running again.
When I'm left to my own devices, I have that little voice inside saying "Okay, you have all the time in the world. You can make it awesome! You can make it perfect from the very start!" If I can't come up with that perfect "creamy" solution in my head, I end up doing nothing for a long time.
I've only recently begun to understand this about myself, and it's an exciting thing to realize. The reason it's exciting is that I am now in a constant mode of production.
You learn by doing. Doing, for crying out loud! So do something, anything. Just do it frequently and thoughtfully. Do it the best you can with what you know at that moment. The creaminess will be a natural byproduct your efforts over time.
If it sucks at first, if it's simplistic, if it's disgustingly inadequate, then good! Every incredible invention man has ever conceived had a gestation period of inept suckitude.
Why Test MuBitmapImpl?
Perhaps one of the reasons I put off testing this class is that its interface has no unique methods of its own. It's just a collection of other interfaces.
interface MuBitmap extends MuMap<MuColor>, MuPaintable {}
I'd just be testing the MuMap interface again, wouldn't I?
Well, for starters, I don't even have any unit tests for MuMap yet — there is no TestMuMapImpl. This is primarily because there is no MuMapImpl class. But even if there was, there are good reasons for me to test MuBitmapImpl.
By way of comparison, there is a MuSetImpl class. This is basically a generic implementation of MuSet. When I perform my tests in TestMuSetImpl, I test only with an Integer incarnation: MuSet<Integer>. This is an acceptable way to test MuSetImpl because it only involves itself with the mechanics of maintaining a list of objects, regardless of type.
MuBitmapImpl, on the other hand, is a specific incarnation of the MuMap interface, that is: MuMap<MuColor>. Even if I had a MuMapImpl class, similar to MuSetImpl in providing a generic manipulation of a "grid of stuff," MuBitmapImpl is its own unique implementation of MuMap.
MuBitmapImpl acts as a lense through which you can access the data of a BufferedImage. It has a custom iterator, operations it does not support, and accessors that are geared specifically towards transporting MuColor objects to and from the underlying BufferedImage. All of this demands dedicated unit tests.
Testing MuBitmapImpl
After working on it for about an hour, my overall test count for Mu was up to 74 tests. That means I ended up with 14 tests for MuBitmapImpl.
The first tests I thought of were pretty basic.
MuBitmapImpl checks that the image passed into the constructor cannot be null. In fact, I throw an exception if that does occur. From my reading on unit testing, I've gahtered that when your class throws specific exceptions you put in yourself, you should create tests for each scenario that would produce them.
So, my first test created an image, created a MuBitmapImpl with it, and then checked to see that getImage() was not null.
"But how could that ever fail? You already check for null in the constructor and throw an exception there!"
Well, remember that unit tests are intended to validate your expectations. It throws an exception now, sure. But it may unwittingly be changed in the future. Perhaps the constructor grows more complex and a try/catch block is introduced which effectively keeps the NullPointerException from ever making its way out.
Perhaps the check is deleted accidentally, or even on purpose. If it's accidental, the test flags it down and reminds me "Oh yeah, that check needs to be in there!" If it's on purpose, and I've decided I really do want to be able to create a MuBitmapImpl with a null image, then the failing test will help me to realize that I need to alter the test to match my new expectation: constructing a MuBitmapImpl object with null should not throw an exception.
For the next test, it seemed logical that the image I passed in should be the exact one handed to me by getImage(), so I added in a test for that.
Then I put in a couple tests to validate the dimensions of my image. I tested to make sure that the width and height of my MuBitmapImpl as reported by wide() and tall() were the same as the dimensions of the BufferedImage I was creating it with.
I also threw in a check to make sure that count() (by way of MuMap via MuSet) was equal to the sum of these dimensions, since any inaccuracy there would mean that loops iterating over the bitmap's pixels with get(int) or iterator() could produce incorrect results (or exceptions!) if they were relying on count() to test the end of loop.
Next I whipped up some tests for the indexed and locational flavors of get(). I've read that it can get sticky including external resources in your tests, such as loading an image from disk, so instead of loading a test image from disk and using get() to validate the colors matched up, I made a helper method to generate dummy content in the BufferedImage, which I then checked with get().
Then came tests for the set() methods, which I used to generate dummy image data in the bitmap from the other end. That is, MuBitmapImpl's interface. I could've probably used the set() tests to also act as the tests for get(), since these tests needed to call the getters for verification. But since I'd already created those other tests, I figured why not keep them! Maybe some day they'll prove useful.
Sandbagging My Way To Enlightenment
Looping through the entire image's pixels probably violates the unit testing principle that your tests should all be very small and execute very fast, but it actually proved useful for me to arrange my tests in this fashion.
What ended up happening is I had refactored my helper code a bit, and also created the setUp() and tearDown() methods that would be called before and after each test, since I was duplicating the code for creating BufferedImage and MuBitmapImpl objects in pretty much every test.
In the inner loop of my helper to generate dummy content in the image, I had:
samples[0] = offset; samples[1] = helper_limitComponent(offset + 1); samples[2] = helper_limitComponent(offset + 2); samples[3] = helper_limitComponent(offset + 3);
The helper_limitComponent() method bounds the component value to 0 if it is less than zero and 255 if it is greater than 255. I didn't put one around the first because my test image was 16x16, and thus had 256 pixels. The offset increased for each pixel, so it would never reach above 255 (0..255 indices equal 256 elements.)
Of course, after my tests for get() were finished, I decided to make the width and height of my test image smaller, just to see how much it would speed the tests up. I made it 8x8 and rant the tests. Everything was fine and it didn't really speed things up too much.
But then I though "Why not make the images HUGE!" And so, because it amused me, I made the image 1024x1024. It cranked along for 4 seconds and then gave me a big fat red bar.
"What you say???" I thought to myself.
After following the stack trace, I realized my error. Of course the offset will hit 256 now, the image is huge and the offset will go even beyond one million, let alone 256! So, I ended up including the bounding check on the first element as well.
It was a pretty minor catch, to be sure, and not necessarily one that could've had a confusing and distressing impact on down the road. But for me it's just another example of how unit testing helps you write more thorough and resilient code, even in the tests themselves. All of these little things inevitably add up. I hope to continue to have similarly minor disturbances!
Scraps
Next to last were tests for my custom PixelIterator. I made sure it couldn't iterate paste the end of the image data, and that it iterated over the correct number of pixels.
And finally I tested to make sure that the add() and remove() methods both threw an UnsupportedOperationException, since those methods make no sense in the context of a bitmap.
Phew!
And with that, I had covered all the angles! Or at least all of the methods in one more ways. And that's good enough fer me. Hope it was informative for you also!