Suppose you’re building a service that depends on several other services to work. You write a bunch of code and carefully include error handling code and have a plan for what happens if each service your new service calls fails. Naturally, you want to test your code. These services are invoked over a network. Perhaps they’re web services but they may be some other network protocol. Suppose further your code is nicely factored so there’s a “client” class that presents the network service as a library API to the rest of the service.
There are a few approaches to ensure you have tests that exercise as much of your own service’s code as possible.
All of these approaches share the idea that you want to have a mock service that you can instruct how to reply so each test can exercise an aspect of the “target” services code.
One approach is to write “mock” services at the library level – swap out the code that calls out to the network with tame code you tell what to do as part of the test set-up. This would involve preparing to return predetermined results for a given request (a proper response or an error of some kind).
Another approach is to “mock” out at the “service transport” level – keep the client implementation that thinks it’s making, say, HTTP requests but swap out the HTTP client with a library that accepts HTTP requests at the API level and replies with appropriate HTTP responses.
A third approach is to “mock” the network service at the network level. This last approach involves writing a complete enough implementation of the target service to behave like the target service that can be instructed to reply with a proper response, a malformed response of some kind, or an error after some specified time (as fast as possible, normally, except when exercising timeout handling code) .. or even to do things like close the network connection before responding, or simply never respond at all. This exercises the entire stack of client code and permits testing of the service code and the network client code. It can be more work than the first two approaches, but provides the most confidence that as much of the service is exercised as possible. It also makes the test code more resilient to internal refactoring of the service since more of it is adhering to the externally visible API boundary (which presumably is more costly to change anyway) rather than introducing another surface acting as an API boundary. This mock service implementation can also be used with other client implementations helping to reduce the amount of code involved.