Assert anything about received arguments

Chances are you have never read RSpec’s documentation from A to Z. I know I haven’t. I picked up on some fancy matchers here and there and never felt compelled to discover more. I have only ever matched the arguments received by a spy or a double with literals, like in the example below.

Spying 101

1
2
3
4
5
6
7
it 'knows my every move'
  nsa = spy('NSA')
  nsa.download_movies('Finding Dory 1080p')
  
  expect(nsa).to have_received(:download_movies)
    .with('Finding Dory 1080p')
end

This has recently proven not to be enough.

My problem

I needed to assert that a logger would receive a call to log_error with an argument that was an error with a very specific message. The error was created inside the method under test and I didn’t care what its class would be. I just wanted to make sure it had the right message.

This beautiful table holds the answer to my problem. Before I get to the last entry, with(<matcher>), which is the “anything” part of this post and my solution, let’s review some of the other interesting options first.

Fancy argument matching

Match with a regular expression

1
2
3
4
nsa.download_movies('Finding Dory 1080p')

expect(nsa).to have_received(:download_movies)
  .with(/Finding Dory/)

Care only about the nth argument

1
2
3
4
nsa.download_movies('Toy Story 720p', 'Finding Dory 1080p')

expect(nsa).to have_received(:download_movies)
  .with(anything, 'Finding Dory 1080p')

Care only about the last argument

1
2
3
4
5
6
7
8
9
nsa.download_movies(
  'Toy Story 720p',
  'Monsters, Inc. 720p',
  'Finding Nemo 1080p',
  'Finding Dory 1080p'
)

expect(nsa).to have_received(:download_movies)
  .with(any_args, 'Finding Dory 1080p')

Expect no arguments at all

1
2
3
4
nsa.download_movies

expect(nsa).to have_received(:download_movies)
  .with(no_args)

Expect a certain interface

1
2
3
4
nsa.download_movies('Finding Dory 1080p')

expect(nsa).to have_received(:download_movies)
  .with(duck_type(:upcase))

Expect an instance of a certain class

1
2
3
4
nsa.download_movies('Finding Dory 1080p')

expect(nsa).to have_received(:download_movies)
  .with(instance_of(String))

Check the ancestors list

1
2
3
4
nsa.download_movies('Finding Dory 1080p')

expect(nsa).to have_received(:download_movies)
  .with(kind_of(Comparable))

Satisfy - the solution to my problem

Since with can be called with any matchers (those things that go after expect(sth).to), I can use the flexible matcher satisfy. It takes a block and calls it with the argument received by the spy. Inside that block, you can do whatever you please with the argument, like calling its methods. The matcher will pass if the block returns a truthy value.

1
2
3
4
5
6
logger = spy()

# some code that uses the spying logger

expect(logger).to have_received(:log_error)
  .with(satisfy { |error| error.message == 'Ooops' })