An external API may require a ‘nonce’ parameter to be present. This parameter should be different for every request, or they will be rejected. In those cases, generating a single random value for each request is often good enough.
# /lib/api_client.rb
def send_request(param)
nonce = SecureRandom.hex(6)
# snip...
end
But in tests, when using the Ruby VCR gem to record requests (to replay them later), the nonce will be different every time the request is replayed, and our recorded request params won’t match. We need a way to make the nonce deterministic.
Let’s stub the nonce generation:
# /test/api_client_test.rb
VCR.use_cassette do
SecureRandom.stub(:hex, '77a3c9b4') do
api_client.send_request('foo')
end
end
Good.
Now we add another request:
# /test/api_client_test.rb
VCR.use_cassette do
SecureRandom.stub(:hex, '77a3c9b4') do
api_client.send_request('foo')
api_client.send_request('bar')
end
end
Error: Server responded with code 500: ‘Nonce already used’
The API starts returning errors, because the second request uses the same nonce as the first one. We need to make the nonce deterministic, but still change it for each request.
Luckily, Minitest’s stub method can invoke a callable: instead of returning a fixed value for every invocation, it will call our lambda each time. We can then increment the hexadecimal nonce.
# /test/api_client_test.rb
VCR.use_cassette do
nonce = '77a3c9b4'
SecureRandom.stub(:hex, -> { nonce = (nonce.hex + 1).to_s(16) }) do
api_client.send_request('foo')
api_client.send_request('bar')
end
end
Better.
Now let’s delete the recorded request, and record a new one by actually calling the API again.
Error: Server responded with code 500: ‘Nonce already used’
Ha. Well, that makes sense: our fixed nonce has already been used before, last time the request was performed. W need a nonce that is generated randomly the first time, but then kept around for deterministic reproduction.
Fortunately, the latest release of the vcr gem (v6.4.1) adds support for record-defined metadata.
# /test/api_client_test.rb
VCR.use_cassette do |cassette|
cassette.metadata[:nonce] = cassette.recording? ? SecureRandom.hex(6) : cassette.metadata[:nonce]
nonce = cassette.metadata[:nonce]
SecureRandom.stub(:hex, -> { nonce = (nonce.hex + 1).to_s(16) }) do
api_client.send_request('foo')
api_client.send_request('bar')
end
end
Et voilà: when performing requests against the real API, the initial nonce will be generated randomly, and stored into the recording metadata. Next time, when the recording will be used in place of the real request, the initial nonce will be re-used. And it keeps incrementing gradually for each request.