Concepts: Stubs
Topics
A component is tested by sending inputs to its interface, waiting for the component
to process them, then checking the results. In the course of its processing,
a component very likely uses other components by sending inputs to them and
using their results:
Fig1: Testing a Component you've implemented
Those other components may cause problems for your testing:
- They may not be implemented yet.
- They may have defects that prevent your tests from
working or make you spend a lot of time discovering that
a test failure was not caused by your component.
- They may make it hard to run tests when you need to. If a
component is a commercial database, your company might
not have enough floating licenses for everyone. Or one of
the components may be hardware that's available only at
scheduled times in a separate lab.
- They may make testing so slow that tests aren't run often
enough. For example, initializing the database might take
five minutes per test.
- It may be difficult to provoke the components to produce
certain results. For example, you may want each of your
methods that writes to disk to handle "disk full"
errors. How do you make sure the disk fills at just the
moment that method is called?
To avoid these problems, you may choose to use
stub components (also called mock objects).
Stub components behave like the real components, at least for the values that
your component sends them while responding to its tests. They may go beyond
that: they may be general-purpose emulators that seek to faithfully
mimic most or all the component's behaviors. For example, it's often a good
strategy to build software emulators for hardware. They behave just like the
hardware, only slower. They're useful because they support better debugging,
more copies of them are available, and they can be used before the hardware
is finished.
Fig2: Testing a Component you've implemented by stubbing
out a component it depends on
Stubs have two disadvantages.
-
They can be expensive to build.
(That's especially the case for emulators.) Being software themselves, they
also need to be maintained.
-
They may mask errors. For example, suppose your component
uses trigonometric functions, but no library is available yet. Your three test
cases ask for the sine of three angles: 10 degrees, 45 degrees, and 90 degrees.
You use your calculator to find the correct values, then construct a stub for
sine that returns, respectively, 0.173648178, 0.707106781, and 1.0. All is fine
until you integrate your component with the real trigonometric library, whose
sine function takes arguments in radians and so returns -0.544021111,
0.850903525, and 0.893996664. That's a defect in your code that's discovered
later, and with more effort, than you'd like.
Unless the stubs were constructed because the real component wasn't available
yet, you should expect to retain them past deployment. The tests they support
will likely be important during product maintenance. Stubs, therefore, need
to be written to higher standards than throwaway code. While they don't need
to meet the standards of product code - for example, most do not need a test suite of their
own - later developers will have to maintain them as components
of the product change. If that maintenance is too hard, the stubs will be discarded,
and the investment in them will be lost.
Especially when they're to be retained, stubs alter component
design. For example, suppose your component will use a database
to store key/value pairs persistently. Consider two design
scenarios:
Scenario 1: The database is used for testing as well
as for normal use. The existence of the database needn't
be hidden from the component. You might initialize it with the
name of the database:
public Component(String databaseURL) {
try {
databaseConnection =
DriverManager.getConnection(databaseURL);
...
} catch (SQLException e) {...}
}
And, while you wouldn't want each location that read or wrote
a value to construct a SQL statement, you'd certainly have some
methods that contain SQL. For example, component code that needs
a value might call this component method:
public String get(String key) {
try {
Statement stmt =
databaseConnection.createStatement();
ResultSet rs = stmt.executeQuery(
"SELECT value FROM Table1 WHERE key=" + key);
...
} catch (SQLException e) {...}
}
Scenario 2: For testing, the database is replaced by a
stub. The component code should look the same whether
it's running against the real database or the stub. So it needs
to be coded to use methods of an abstract interface:
interface KeyValuePairs {
String get(String key);
void put(String key, String value);
}
Tests would implement KeyValuePairs
with something simple like a hash table:
class FakeDatabase implements KeyValuePairs {
Hashtable table = new Hashtable();
public String get(String key) {
return (String) table.get(key);
}
public void put(String key, String value) {
table.put(key, value);
}
}
When not being tested, the component would use an adapter
object that converted calls to the KeyValuePairs
interface into SQL statements:
class DatabaseAdapter implements KeyValuePairs {
private Connection databaseConnection;
public DatabaseAdapter(String databaseURL) {
try {
databaseConnection =
DriverManager.getConnection(databaseURL);
...
} catch (SQLException e) {...}
}
public String get(String key) {
try {
Statement stmt =
databaseConnection.createStatement();
ResultSet rs = stmt.executeQuery(
"SELECT value FROM Table1 WHERE key=" + key);
...
} catch (SQLException e) {...}
}
public void put(String key, String value) {
...
}
}
Your component might have a single constructor for both tests and
other clients. That constructor would take an
object that implements KeyValuePairs.
Or it might provide that interface only for tests, requiring that
ordinary clients of the component pass in the name of a database:
class Component {
public Component(String databaseURL) {
this.valueStash = new DatabaseAdapter(databaseURL);
}
// For testing.
protected Component(KeyValuePairs valueStash) {
this.valueStash = valueStash;
}
}
So, from the point of view of client programmers, the two
design scenarios yield the same API, but one is more readily
testable. (Note that some tests might use the real database and
some might use the stub database.)
For further information related to Stubs, see the following:
|