Keyword arguments and instance doubles

541 views
Skip to first unread message

Steven Webb

unread,
Sep 19, 2017, 2:13:28 AM9/19/17
to rspec
I'm having trouble testing a method signature change from taking a single argument (a hash) to using keyword arguments. I've created a contrived example of HTML parsing to simplify things (I'm not actually writing a html parser):

class HTML
  def parse(body) # body is a hash
    ...
  end
end

I want to update it so that it can take an optional headers argument. It becomes:

class HTML
  def parse(body: , headers: {}) # body and headers are both hashes
  end
end

In a related unit test of a different class I'm using something like this:

RSpec.describe "calling the parser" do
  let(:html) { instance_double("HTML", parse: nil) }
  let(:body) { double("body") }
  let(:headers) { double("headers") }

  before { html.parse(body: body, headers: headers) }

  it "allows passing optional headers" do
    expect(html).to have_received(:parse).with(body: body, headers: headers)
  end
end

The problem I've got is that this test passes before updating the HTML class. After updating the HTML class it correctly detects the keywords as arguments and passes. Before it incorrectly determines the keywords are the "body" hash and passes. Both are valid ruby, but the method signature has changed (at least to me, possibly not to the VM). I tried:

it "allows an optional headers argument" do
  expect(html).to respond_to(:parse).with_keywords(:body, :headers)
end

but that fails (presumably the instance double is using method_missing).

  1) calling the parser allows an optional headers argument
     Failure/Error: expect(html).to respond_to(:parse).with_keywords(:body, :headers)
       expected #<InstanceDouble(HTML) (anonymous)> to respond to :parse with keywords :body and :headers
     # ./spec/keyword_args_spec.rb:40:in `block (2 levels) in <top (required)>'

Can anyone explain how I should be testing this correctly?

Thanks

Steve.

Myron Marston

unread,
Sep 19, 2017, 2:20:10 AM9/19/17
to rs...@googlegroups.com

What goal do you have in mind for this test? From the examples you gave, it looks like you are only testing how RSpec’s verifying doubles work. For example, this expectation:

expect(html).to respond_to(:parse).with_keywords(:body, :headers)

…isn’t exercising your code at all, because you’ve declared html as a test double, so it’s just testing how doubles work. If you’re trying to test the HTML class, you should not use a double in its place. Test doubles are intended for when you want to control the environment in which you test something, by replacing some collaborators with fake versions. They’re not intended to ever replace the thing you are testing—once you do that, you’re no longer testing the thing.

Myron


--
You received this message because you are subscribed to the Google Groups "rspec" group.
To unsubscribe from this group and stop receiving emails from it, send an email to rspec+unsubscribe@googlegroups.com.
To post to this group, send email to rs...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/rspec/8dbe9153-45c0-44f7-a0be-e6fa6ceffd7e%40googlegroups.com.
For more options, visit https://groups.google.com/d/optout.

Steven Webb

unread,
Sep 19, 2017, 2:33:22 AM9/19/17
to rspec
Thank you.

I guess I was hoping that when the instance double verified that the message matches the method signature it would determine that I was trying to use keyword arguments instead of a hash.

Steve.
To unsubscribe from this group and stop receiving emails from it, send an email to rspec+un...@googlegroups.com.

Steven Webb

unread,
Sep 19, 2017, 2:53:55 AM9/19/17
to rspec
To expand further.

If I'd continued using position arguments and changed from:

def parse(body)

to

def parse(body, headers)

the instance double could calls to this were using the correct number of arguments.

Alternatively if I was already using keyword arguments and added another one:

def parse(body:)

to

def parse(body:, headers: {})

it would also get picked up.

It's only because passing keywords arguments to a method can be interpreted as either a single hash or multiple arguments that this isn't picked up.

Steve.

Myron Marston

unread,
Sep 20, 2017, 2:20:22 AM9/20/17
to rs...@googlegroups.com
I guess I was hoping that when the instance double verified that the message matches the method signature it would determine that I was trying to use keyword arguments instead of a hash.

It's worth noting that keyword arguments really just a hash (plus some syntactic sugar). Anyhow, verifying doubles do look at keyword arguments when verifying a message you expect/allow or when you send a message to a verifying double.  Here's an example:

``` ruby
class HTML
  def parse(body:, headers: {})
  end
end

RSpec.describe "Verifying doubles" do
  it 'verifies the keyword arguments when allowing a message' do
    html = instance_double(HTML)

    # this works...
    allow(html).to receive(:parse).with(body: "abc", headers: {a: 1})

    # ...but this triggers an "Invalid keyword arguments provided: bad_key" error.
    allow(html).to receive(:parse).with(body: "abc", headers: {a: 1}, bad_key: 1)
  end

  it 'verifies the keyword arguments when receiving a message' do
    html = instance_double(HTML, parse: nil)

    # this works...
    html.parse(body: "abc", headers: {a: 1})

    # ...but this triggers an "Invalid keyword arguments provided: bad_key" error.
    html.parse(body: "abc", headers: {a: 1}, bad_key: 1)
  end
end
```

This works as you'd expect--when allowing a message with arguments that the method signature supports, it works, but when you allow a message with arguments that the method signature does not support, it raises an appropriate error.  And it behaves similarly when you send it a message.

In your original example, you had this:

``` ruby
expect(html).to respond_to(:parse).with_keywords(:body, :headers)
```

This didn't work how you expect, because the `respond_to` matcher is only able to work off of the method signature of the `html` object (in this case, a test double), and RSpec's verifying doubles implement the method signature checks as _runtime_ behavior, rather than actually defining the exact same method signature on the test double.  Essentially, the methods defined on test doubles are all defined to accept `*args`, and then it applies logic to those arguments at runtime to do things like the method signature verification.  There simply isn't a simple, performant way for test doubles to define the method signatures in the same way as you do on normal classes.

As I said before, using a `respond_to` matcher on a test double like this is basically just testing the test double, not testing your class.  You could do something like this:

``` ruby
expect(HTML.new).to respond_to(:parse).with_keywords(:body, :headers)
```

...but I personally wouldn't bother with anything like that.  Such a test seems not to have much value, IMO, and is likely to be brittle.

Myron

To unsubscribe from this group and stop receiving emails from it, send an email to rspec+unsubscribe@googlegroups.com.

To post to this group, send email to rs...@googlegroups.com.
Reply all
Reply to author
Forward
0 new messages