I'm encountering a recurring challenge when using TDD to develop code that must perform actions in a specific order. Usually I'm able to write tests such that it is only possible to create a correct solution. However, when I need to test code that is required to perform actions in a specified sequence this becomes more complex that I would want.
Let's take the example of a program that has to send AT commands and process the responses. Somewhere in the code you'll eventually find a function that takes a command as an argument, and returns the received response.
Here is a test case that I would write for such a function:
def test_exchange_sends_a_command_and_returns_the_received_response():
stdout = io.StringIO()
stdin = io.StringIO("CONNECT\r\n")
response = exchange(stdout, stdin, "ATD")
assert stdout.getvalue() == "ATD\r\n"
assert response == "CONNECT"
A correct implementation would be:
def exchange(stdout, stdin, command):
stdout.write(command + "\r\n")
return stdin.readline().strip()
However, the following incorrect implementation would also pass the test:
def exchange(stdout, stdin, command):
response = stdin.readline().strip()
stdout.write(command + "\r\n")
return response
One potential solution is to implement a stateful test double. For example:
def test_exchange_command_sends_command_and_receives_response():
stdin = io.StringIO()
class StdoutStub(io.StringIO):
def write(self, buf):
super().write(buf)
if self.getvalue() == "ATD\r\n":
stdin.write("CONNECT\r\n")
stdin.seek(0)
stdout = StdoutStub()
response = modemcontrol.exchange(stdout, stdin, "ATD")
assert stdout.getvalue() == "ATD\r\n"
assert response == "CONNECT"
There are probably better ways to implement this test double, but in essence the same logic would be necessary.
Another solution which turned out to work is using Rust async code. A full example is way too complex, but it would boil down to something like this:
let exchange_task = tokio::spawn(
async move { exchange(stdout_stub, stdin_stub, "ATD").await }
);
// Check if command was send.
assert_eq!(stdout_stub.peek(), "ATD\r\n");
// Make the response available to the function
stdin_stub.put("CONNECT\r\n")
// Check response
let response = exchange_task.await.unwrap();
assert_eq!(response, "CONNECT");
I certainly wouldn't want to start using async in a project only to make the tests more simple. And I'm not sure if I would consider the code above 'simple' anyway.
Yet another option would probably be to use an event driven design. But again, that would result much more code.
Does anybody has a common pattern to use to test these kinds of functions?