Topics

 

Introduction

To top of page

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:

Diagram described in accompanying text.

Fig1: Testing a Component you've implemented

Those other components may cause problems for your testing:

  1. They may not be implemented yet.
  2. 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.
  3. 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.
  4. They may make testing so slow that tests aren't run often enough. For example, initializing the database might take five minutes per test.
  5. 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.

Diagram described in accompanying text.

Fig2: Testing a Component you've implemented by stubbing out a component it depends on

Stubs have two disadvantages.

  1. They can be expensive to build. (That's especially the case for emulators.) Being software themselves, they also need to be maintained.
  2. 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.

 

Stubs and software design practices

To top of page

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.)

 

Further information

To top of page

For further information related to Stubs, see the following:



Rational Unified Process  

2003.06.13