samedi 30 juin 2018

How should I test a convenience method who's sole purpose is to call another method?

Disclaimer

This question is subjective. I know we're supposed avoid subjective questions on Stack, but it's something that a) I've been struggling with for a while and don't know where else to ask and b) I think would be considered a constructive subjective question and therefore permitted on Stack.


The Problem

I don't know how to go about testing a convenience method who's only purpose is to call another method on a different object.

As far as I can tell their are two ways to go about this, both with downsides:

  1. Retest all of the exact same logic for both the convenience method and the method it calls.

  2. Test that the convenience method calls and returns the original method with the expected arguments.

Option 1 makes less sense to me, in particular it doesn't make sense when the origin method (the one that the convenience method wraps) performs slightly complex logic and requires multiple unit tests. Why would I want the exact same logic tested in multiple places? What if that logic changes? Wouldn't it be better to just test that in one place rather than several?

I tend to lean towards option 2, but obviously the glaring red flag their is your testing implementation, not behavior. Which as I've heard time and time again (and tend to agree with for the most part,) you should not do.


An Example Of The Problem

Ok, now on to an example to help clarify what I'm talking about.

I'm currently working on an API of sorts that helps programmers interact with Alexa. The fundamental way to set responses to Alexa in my API is through a response object on an instance of a class called Alexa. For example, to set the output speech to 'hello world' you would use the following:

@alexa = Alexa.new
@alexa.response.set_speech("hello world")

In addition to this, I provide a convenience method on the Alexa class called #say that has the exact same result:

@alexa.say("hello world")

Here's what's going on behind the scenes:

# Alexa class
class Alexa
  attr_reader :response

  def initialize
    @response = Response.new
  end

  def say(speech)
    @response.set_speech(speech)
  end
end

# Response class
class Response
  attr_reader :response_hash

  def initialize
    @response_hash = {}
  end

  def set_speech(speech)
    @response_hash[:speech] = speech
  end
end

Notice how the Alexa#say method is a wrapper method who's only responsibility is to call response#set_output_speech with the passed in arguments.

Ok, now on to the tests.

For the set_speech method, the testing is easy:

RSpec.describe Response do
  describe '#set_speech' do
    it 'adds the passed in argument to the response hash under key :speech' do
      subject = Response.new
      subject.set_speech("hello world")
      expect(subject.response_hash[:speech]).to eq("hello world")
    end
  end
end

For the say method, I'm torn.

I could either retest the behavior of the method, which is not DRY and leads to unit tests that are dependent on other methods and objects:

RSpec.describe Alexa do
  describe '#say' do
    it 'adds the passed in argument to the response hash of @response under key :speech' do
      subject = Alexa.new
      subject.say("hello world")
      expect(subject.response.response_hash[:speech]).to eq("hello world")
    end
  end
end

Or I could test that say calls response#set_speech with the provided options, which as far as I can tell is testing implementation, and not behavior:

RSpec.describe Alexa do
  describe '#say' do
    it 'calls #set_speech on @response' do
      subject = Alexa.new
      expect(subject.response).to receive(:set_speech).with("hello world")
      subject.say("hello world")
    end
  end
end


My Question

TL;DR - How should I go about testing a convenience method whose sole purpose is to call a different method?

Is it better to retest the behavior and have repetitive and dependent unit tests, or to test that the convenience method calls the original method, and thus test implementation instead of behavior?

Or perhaps there is a third option I haven't come across yet?

Since this is a subjective question I would love more than just a 'use option 1' or 'use option 2' answer, try to explain why you think one technique is better than the other. Maybe even add in a story or two from your experience where you've seen the benefits of one approach over the other. Thanks!

Aucun commentaire:

Enregistrer un commentaire