Ryan Lue Ryan Lue - 4 years ago 94
Ruby Question

How to unit test a class that depends heavily on other classes?



My understanding is that unit testing should test classes in isolation, focusing on granular behavior and substituting objects of other classes using doubles/mocks wherever possible. (Please correct me if I'm wrong here.)

I'm writing a gem with a class called

MatchList
.
MatchList::new
takes two arguments, each an instance of another class called
MatchPhrase
.
MatchPhrase
contains some behavior that
MatchList
depends heavily on (i.e., if you feed anything other than a
MatchPhrase
to
MatchList::new
, you're going to get a bunch of "undefined method" errors).

My current (naive?) test setup uses
let
statements to assign variables for use in my examples:

let(:query) { MatchPhrase.new('Good Eats') }
let(:candidate) { MatchPhrase.new('Good Grief') }
let(:match_list) { MatchList.new(query, candidate) }


How do I write this unit test? Am I right in thinking it should be done without invoking the
MatchPhrase
class? Is that even possible?




For reference, here is what the
MatchList
class looks like:

class MatchList < Array
attr_reader :query, :this_phrase, :that_phrase

def initialize(query, candidate)
super(query.length)
@query = query
@this_phrase = query.dup
@that_phrase = candidate
find_matches until none?(&:nil?)
end

private

def find_matches
query.each.with_index do |this_token, i|
next unless self[i].nil?
that_token = this_token.best_match_in(that_phrase)
next if that_token.match?(that_token) &&
this_token != that_token.best_match_in(this_phrase)
self[i] = this_token.match?(that_token) ? that_token : NilToken.new
this_phrase.delete_once(this_token)
that_phrase.delete_once(that_token)
end
end
end

Answer Source

Mocking should be done for legitimate reasons and not as a matter of principle.

If there is only one collaborator class and your primary class is heavily coupled to it, mocking out the collaborator as a matter of principle may result in more fragility than benefit as the mock will not reflect the behavior of the collaborator.

Mocks and stubs are good candidates when you can reason against the mock's interface instead of an implementation. Let's ignore the existing code and look at the interfaces in use here:

  • MatchList.new takes a query and candidate
  • query is an Enumerable containing objects which implements best_match_in?(something)
  • The objects in query also implement delete_once(something)
  • candidate also implements delete_once(something)
  • best_match_in? returns something that implements match? and best_match_in?

Looking at the interfaces in use, MatchList appears to rely pretty heavily on the implementation of the query and candidate objects. Smells like a case of feature envy to me. Perhaps this functionality should reside within MatchPhrase instead.

In this case, I would write unit tests using actual MatchPhrase objects with a note to refactor this code.

Recommended from our users: Dynamic Network Monitoring from WhatsUp Gold from IPSwitch. Free Download