Testing the Ocean class in Battleship
Fall 2007, David Matuszek

My TAs and I have been getting the following question:

How can I test the Ocean class if all the fields are private, and I can't write getters for them?

I'll answer the "how," but first I'm going to talk about the "why."

Why make instance variables private?

The instance variables describe the "state" of the object. It is the responsibility of the class to keep its objects in a valid state. From outside the class it should not be possible, by accident or design, to either (a) get an invalid object, or (b) make an object invalid.

Aside #1: Values that don't really represent part of the state of an object should be represented by local variables. If they are needed in more than one method, they should be passed around as parameters.

Aside #2: If an instance variable is part of the state of an object, but any value (of the correct type) is valid, then the variable can be made public. With what we have covered so far, the best example I can give is an array--the array remains valid regardless of what values (of the correct type) we put in it.

Okay, but why not provide getter methods?

First, consider the following "real life" scenario: You are working on a large project with 50 other people. A month ago, you gave them all the information they need in order to use your class. Now they are using it, in dozens or maybe hundreds of places. Now you just thought of a much better, safer, simpler, less error-prone way to do things.

Can you do it?

It depends. What do they know about your class? If they only know about the methods you provided, you can probably completely revise the internals of the class, and no one will be affected (except their code might work better). On the other hand, if they know you are using, say, a two-dimensional array of characters, and they know what the character '@' means, then chances are you have to convince the CEO that the change is worth making.

The interface that you provide to your class (not to be confused with Java's interface class) consists of all the things that other people (and other classes) are allowed to know about your class. This is a contract. Once made, it cannot be changed, except by agreement among all the parties using it. As with any kind of contract, the rule is: Provide what is needed, but don't include a bunch of stuff that isn't needed. Like, for example, how you implement some internal variables.

Here's another, possibly even more important reason. A well designed class has a simple, easy to use interface. The more complex it is, the more difficult it is to use. Classes with five methods are easier to use than classes with fifty methods. Any variables that aren't private are, whether you like it or not, part of the interface; they can be used (and abused) by other classes.

But I just want to test the Ocean class!

Which Ocean class? This one?

public class Ocean {
    char[][] sea = new char[10][10];
    ...   
}

Or this one?

public class Ocean {
    int[][] battleshipOcean = new int[10][10];
    ...   
}

Or this one?

public class Ocean {
    boolean[][] ship = new boolean[9][9];
    boolean[][] hits = new boolean[9][9];
    ...   
}

Or this one?

public class Ocean {
    boolean[][] shots = new boolean[9][9][2];
    ...   
}

Or this one?

public class Ocean {
    Ship[] ships = new Ships[20];
    ...   
}

Don't forget: Your OceanTest class is a user of the Ocean class. If you decide to change the representation of your "ocean," wouldn't it be nice if your tests still worked? For that matter, if your instructor decides to try your JUnit tests with his own Ocean class, wouldn't it be nice if they worked?

Grumble. Okay, so how do I test the Ocean class?

With the methods provided.

You can test by putting things in the ocean (with the place__At methods) and testing whether they are there (with the isOccupied method). You can't fully test isOccupied without using some place__At methods, and you can't test place__At and okToPlace__At methods at all without using isOccupied, so this isn't ideal, but few things are.

You could either do spot checking--placing a ship, then checking that it is there--or complete checking--placing a ship and checking that that's the only change in the entire ocean. Either is okay, so long as you think the tests are good enough to catch any likely errors. Don't forget to check illegal calls as well--battleships sticking out over the edge of the ocean, that kind of thing.

You can test isOccupied by creating an empty ocean, checking that some spots are empty, placing a ship there, and checking that they are no longer empty.

You can test the okToPlace__At methods by placing a ship or two, then checking various places that are or aren't legal to place additional ships.

You can test the place__At methods by using them, then checking whether the appropriate places are occupied. You might also want to check that surrounding places are not occupied. Remember, the assignment says "You can assume that the indicated location is legal" for these methods, so assume it--you only need to test with legal calls.

The hardest one to test is placeAllShipsRandomly. A really thorough test would check that all ships have been placed, and none are adjacent to one another. However, if your placeAllShipsRandomly method uses the okToPlace__At methods, it is probably safe to assume that no ships are adjacent. So it's probably sufficient to test that exactly 20 squares are occupied.

That's a lot of work!

Yes, quite a bit. But with a little thought, you can minimize the amount of work required.

Methods are one of the best ways to avoid unnecessary work--just follow the DRY principle. Well-designed methods can save you an immense amount of code (poorly-designed methods, maybe not so much).

Remember, unit test methods are just Java. You can use ordinary Java code, for example, loops. And if you find yourself writing the same loops more than once, write a method to do it. Any method whose name doesn't begin with test will not be called automatically by JUnit, so you can write as many of these as you like.

You are not required to comment your JUnit test methods. You should give them good descriptive names, and you really ought to comment anything that isn't obvious, but JUnit test methods usually aren't commented.