You can write test double for all the external entities of an application, such as an email server, a credit card processor, or an enterprise database. But you still have to check that you’ve made the right connection to all of these. How do you do that?
The answer is in the interface and a couple of patterns – strategy and adapter. Every external entity interface’s is adapted to a development’s group interface for that service. See my Interface-Oriented Design book for more details. Since external entities are often used by more than one application, it makes for less redundancy and more consistency if the development group creates the adapters.
Credit Card Processor Example
For a credit card processor, the external interface might be an XML file, such as:
<Transaction> <Account>378282246310005</Account> <Amount>30.00</Amount> <Type>Debit</Type> <Notation>Beach Boy Concert</Notation> </Transaction>
You make up an interface that adapts the XML interface to the language of your application. For example:
class Transaction { Account id; Dollar amount; Type type; String notation; } enum TransactionResponses { ACCEPTED, REJECTED, UNAVAILABLE}; interface CreditCardProcessor { TransactionResponse processTransaction( Transaction transaction); }
The test double implements this interface and returns the desired values. The implementation for the real entity implements this interface and calls the external entity.
Someone needs to check that the external entity actually performs its expected operation (e.g. charge the account the money and put it into your account). And you also need to check that your implementation calls the entity correctly.
However, you do not need to run these tests against the real entity repeatedly. First, it will max out your credit card. Second, it can be slow. You typically do this test a few times as desired. You’ll particularly do it if you change the interface, since the external entity may have changed its paradigm.
You can substitute the real implementation for the test double using the strategy pattern. In that pattern, you have two or more classes that implement the same interface. You use a factory to determine which interface to use. This factory can be implemented several ways:
- A factory class
- Loading a separate library that contains either the test double or the real
- Using a dependency injection container (Spring, etc.)
Email Server Example
Other forms of external entities can be called repeatedly without causing too many issues. However, you still probably want a test double for ease of automation. Once again, the same approach applies – an interface with an adapter and a way to inject different implementations into the system. Here’s an example interface for an email server that uses domain specific classes that separate out responsibilities for validation.
class EmailAddress {……}; class Message {….}; class Password{…}; enum EmailResponse {….}; interface EmailService { EmailResponse sendEmail( EmailAddress fromAddress, EmailAddress toAddress, Password password, Message message); Message getOneEmailforAddress( EmailAddress address, Password password); }
When you test the real server with this interface, you might need to add a slight delay between sending an email and attempting to retrieve it.
It’s possible that you might not need a test double for an email server, except for speed. If you do need a test double, you could create a method that acts as the test double (mock) with this interface.
Database Example
Once again, the database test double and the real entity should have the same interface. Then the same adapter/strategy/factory pattern can be used. As an example, let’s take a simple customer data object.
class Customer { CustomerID id; String name; Dollar balance; } enum DatabaseResult {SUCCESS, DATABASE_SERVER_ERROR, …} class DatabaseService { Customer getCustomerForID(CustomerID id); DatabaseResult storeCustomer(Customer customer); }
Now here’s one test for the real database. You might have to ensure that the test has the proper credentials to check all the operations.
Customer current = getCustomerForID(customerID); // make a copy Customer test = current.clone(); // change values in each field test.name= "Something else"; test.balance = 100000; // store changed record storeCustomer(test); // get it back Customer result = getCustomerForID(customerID); // Check to see if stored properly assertEqual(test.name, result.name); assertEqual(test.balance, result.balance); storeCustomer(current); // put back original
Note that this is a non-destructive test for the database, although the underlying system may have additional log entries You could also run tests using two different ids to ensure that everything is not getting stored into one record. And as many more tests as you wish.
Tests like this could be run often run, since they do not change the data. However, they might only be run on a periodic basis.
A Few Parting Words
Even if you are using service virtualization so that it is easy to switch between real and test double implementations, I recommend creating programmatic interfaces as shown. They will encapsulate the details of the service calls from the application programs and make those programs easier to maintain.
Summary
Create common interfaces for external entities. Test those interfaces with the real entities as often as necessary. Use the same interface for the test doubles.