Look at the following image...
...it shows an object being tested.
You can't see inside the object. All you can do is send it messages. This is an important point to make because we should be "testing the interface, and NOT the implementation" - doing so will allow us to change the implementation without causing our tests to break.
Messages can go 'into' an object and can be sent 'out' from an object (as you can see from the image above, there are messages going in as well as messages going out). That's fine, that's how objects communicate.
Now there are two types of messages: 'query' and 'command'...
Queries are messages that "return something" and "change nothing".
In programming terms they are "getters" and not "setters".
Commands are messages that "return nothing" and "change something".
In programming terms they are "setters" and not "getters".
- Test incoming query messages by making assertions about what they send back
- Test incoming command messages by making assertions about direct public side effects
- Messages that are sent from within the object itself (e.g. private methods).
- Outgoing query messages (as they have no public side effects)
- Outgoing command messages (use mocks and set expectations on behaviour to ensure rest of your code pass without error)
- Incoming messages that have no dependants (just remove those tests)
Note: there is no point in testing outgoing messages because they should be tested as incoming messages on another object
Command messages should be mocked, while query messages should be stubbed
Contract tests exist to ensure a specific 'role' (or 'interface' by another - stricter - name) actually presents an API that we expect.
These types of tests can be useful to ensure third party APIs do (or don't) cause our code to break when we update the version of the software.
Note: if the libraries we use follow Semantic Versioning then this should only happen when we do a major version upgrade. But it's still good to have contract/role/interface tests in place to catch any problems.
The following is a modified example (written in Ruby) borrowed from the book "Practical Object-Oriented Design in Ruby":
# Following test asserts that SomeObject (@some_object)
# implements the method `some_x_interface_method`
module SomeObjectInterfaceTest
def test_object_implements_the_x_interface
assert_respond_to(@some_object, :some_x_interface_method)
end
end
# Following test proves that Foobar implements the SomeObject role correctly
# i.e. Foobar implements the SomeObject interface
class FoobarTest < MiniTest::Unit::TestCase
include SomeObjectInterfaceTest
def setup
@foobar = @some_object = Foobar.new
end
# ...other tests...
end
Right - I think I understand - thank you for constructing this stub.
What I'm concluding is that in order to force me to write a valid implementation (in my TDD loop), I need to pass a stub where the output depends on the input.
Perhaps there's some rule of thumb that if the output of a class (eg.
WeatherAPI) depends on its input, then a stub for that class also needs to depend on its input?Consider the following, which implements the interface just fine, but doesn't have the property we want?
(for clarity this is more or less what
instance_double(WeatherApi, weather: :rain)creates: even though Ruby doesn't have interfaces,instance_doublechecks that the method exists on the real class. It doesn't check types though, since Ruby doesn't have types either and by convention uses ducktyping and polymorphism)I see the point that if we pass this in our spec:
...then we don't need any explicit assertion that the API was called.
...but won't a passing spec have implicitly asserted that the API was called with "London" since there's no other way it could possibly have come up with the correct result? 🙃.
To put it another way, it feels like passing this stub is more or less equivalent to:
const stubAPI = new HardcodedWeatherStub()(always return "rain")stubAPIreceivedweatherwithLondon(this is easy with testing frameworks in Ruby)...but maybe there's some key difference that I'm missing?
I 100% agree on not testing the SDK I didn't write, and relying on the interface of the API rather than what it actually does: this was never in doubt.
I still feel like making an assertion on the outgoing query message is only relying on the interface to the
WeatherAPI, not on its implementation? But there are of course other reasons to not make such assertions.