Say we have a service object, with a dependency injected. The service performs an operation with data obtained from the dependency. For simplicity in this example, let’s say the service would wrap the data into a response hash, with keys success
indicating successful operation and data
containing the result of the operation.
When writing tests for such an object, it’s common to mock the dependency using Rspec and stubbing the response from the dependency. The test then creates a new service object with this dependency injected and assert the result of the operation against the mock data.
This test would pass with the current implementation of Service
. But what would happen if the operation performs unintended mutation on the data received from the dependency?
So now instead of returning data {a: 1, b: 2}
, the service would return {a: 1, b: 2, hello: ‘world’}
. The test should fail, right? Surprisingly, at least to me and our team at first, the didn’t catch this error. Running the same test would still result in a passing test.
The reason for the passing test is this line in the test:
expect(result[:data]).to eql(data)
By asserting the test result against the variable, we are comparing the result with the object referenced by the variable data
. Remember that data
was the stub response returned by the dependency. This response, in turn, was mutated by the service through merge!
. Throughout this process, it is always the same data
object that is being operated on. When the time comes for the test to assert result[:data]
, it compares it to the object data
, which by now have been added with a new key value pair hello: 'world'
. result[:data]
and data
now have exactly the same content, and the test passes.
So how do we write a better test? Simply assert against the actual values.
expect(result[:data]).to eql(a: 1, b: 2)
This will ensure the test fails when that happens.