Unit testing needs to be sorted out before implementing any classes, even if writing actual tests isn't going to start yet. IDEs or unit testing frameworks such as JUnit can be used (and it is a good idea to look into them for ideas, at least). But what is the simplest approach that works properly?
For small classes, testing can be done in a main
method in the
class itself. Java's assertions could be used, but are complicated by the need
for the -ea
flag. So it seems simpler to avoid assertions and
define classes like this:
class X { static void claim(boolean b) { if (!b) throw new Error("Bug"); } public static void main(String[] args) { claim(2+2 == 4); System.out.println("Class X OK"); } }
The claim
method is roughly equivalent to Java's
assert
keyword. This is all that is necessary for the current
project.
This simple approach to testing can be developed further in various ways.
For example, rather than call claim(s.equals("x"))
, it might be
preferable to use claimEq(s, "x")
. The claimEq
method
can be written:
static void claimEq(Object x, Object y) { if (x == y) return; if (x != null && x.equals(y)) return; throw new Error("Bug"); }
Because of the boxing/unboxing feature of Java, this works for primitive
types, e.g. integers and characters, as well as objects which have a suitable
equals
method. (However, as it stands, it doesn't work for
arrays).
If you do want to use Java's assertions, you can test whether they have been switched on like this:
... public static void main(String[] args) { boolean testing = false; assert(testing = true); if (! testing) System.err.println("You forgot the -ea flag"); ... }
Note the argument to the assert call is an assignment testing =
true
not a comparison testing == true
.
The main
method approach to unit testing can also be scaled up
to large projects, by defining classes something like this:
class X implements Testable { public String test(String in) { ... } public static void main(String[] args) { Test.run(args, new X()); } }
The Testable
interface describes a test
method
which takes a test expressed as a string and returns a string result which can
be checked against what is expected. The test
method may be
effectively static, creating any objects it needs, but is not declared
static
, so that it can be described by the interface.
Shared test code is put into a general Test
class, which doesn't
depend directly on any of the project classes, only on the Testable
interface, so that a class can be tested when other classes that it doesn't
depend on are broken. The run
method has the signature:
static run(String[] args, Testable sample) { ... }
The object passed as the second argument is just a sample object used by the
Test
class to access the test
method. The battery of
tests for a class is put into a text file, which is read and interpreted by the
Test
class.