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 it is not being used in a test, 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:
|