One of the challenges in getting started arises when constructors set state independent of getters and setters. A typical example of this occurs when constructors use default values, common when you have multiple constructors and simpler ones call more complex ones (Listing 6-4).
public class FixedThreadPool {
private final int poolSize;
public FixedThreadPool(int poolSize) {
this.poolSize = poolSize;
}
public FixedThreadPool() {
this(10);
}
public int getPoolSize() {
return poolSize;
}
}
Testing the first constructor is trivial. We pass a pool size and use the getter to verify it. We cannot use the getter and setter combination test because the poolSize
attribute cannot be changed once it is set. But how do we test that the default constructor does its job correctly? An initial attempt at a test might look like Listing 6-5.
@Test
public void testFixedThreadPool() {
FixedThreadPool sut = new FixedThreadPool();
int actualPoolSize = sut.getPoolSize();
assertEquals(10, actualPoolSize);
}
This certainly gets the job done to start us off. What happens if the default pool size changes? As we have seen in previous examples, we get a maintainability benefit from coupling our test inputs to the expected values. The same is true with the “hidden” inputs that manifest with default values. Fortunately, we can easily refactor our code to make our defaults both visible and self-documenting. Review the refactored fragment of the original production code in Listing 6-6.
public class FixedThreadPool {
public static final int DEFAULT_POOL_SIZE = 10;
...
public FixedThreadPool() {
this(DEFAULT_POOL_SIZE);
}
public int getPoolSize() {
return poolSize;
}
}
The declaration and usage of the DEFAULT_POOL_SIZE
constant gives us a publicly accessible, intent-revealing name for the value used by the default constructor. Our test now becomes much more legible and less fragile when written as in Listing 6-7.
@Test
public void testFixedThreadPool() {
FixedThreadPool sut = new FixedThreadPool();
int actualPoolSize = sut.getPoolSize();
assertEquals(FixedThreadPool.DEFAULT_POOL_SIZE,
actualPoolSize);
}
The same technique can be used to expose string constants or even object constants and not just in constructors. Any fixed value or “magic number” that makes the software work is a candidate. You will often see such constants defined in well-defined libraries simply based on their convenience and readability, but they serve to enhance the testability of the software as well. This is a technique that is hard to abuse.3
3. However, like any good thing, it can be abused. The most egregious abuse I have seen of shared constants is the preallocated exception. In Java in particular, the stack trace for an exception is captured when the object is allocated. When it is preallocated as a static constant, the stack trace shows the static initialization context, not the context in which the exception is actually thrown. This can be very confusing when diagnosing a test failure.
Sometimes the form of constant values may differ. When there are multiple values that represent a fixed set, an enumeration is often used. Listing 6-8 shows a common idiom in C++ using nested anonymous enumerations.
class 3DPoint {
enum {
X_INDEX = 0,
Y_INDEX,
Z_INDEX
};
...
};
By default, the first member of the enum
is assigned the value 0
automatically, but it is a common idiom to specify the initial value. Successive values increment by one from their predecessor.
3.15.34.39