Isolated testing with associated collections

I often encounter this kind of pattern in my code, where I have a collection, and I want to send the same message to each member:

class UpdateWidgets
  def self.call(widgets)
    widgets.each do |widget|
      widget.update(:foo)
    end
  end
end

My test usually looks something like this:

RSpec.describe UpdateWidgets, ".call" do
  it "updates each widget" do
    widget = spy(:widget)
    widgets = [widget]

    UpdateWidgets.call(widgets)

    expect(widget).to have_received(:update).with(:foo)
  end
end

But a couple of things donā€™t sit right:

  • The test assumes that widgets is an array, even though it could be of some other type which implements each.
  • The test assumes only one item in the collection. Should I test with multiple?

Is there a better way to test this?

Iā€™ve had similar doubts regarding testing this kind of object.
Iā€™ve come to consider that, in order for them to add value, they need to perform some function that stops them from being pure ā€œdelegation intermediariesā€. Iā€™ll try to reproduce my thinking dealing with this, hope you find it useful.

So, in this case, Iā€™d encapsulate the widgets in the initializer, probably wrapping them in an array there. From this point on, there is no doubt to the reader of the code wheter we are dealing with an Enumerable or not. From then on, we can ensure our call method deals with this in an adequate fashion, calling for instance ā€˜flat_mapā€™. This alone doesnā€™t solve all issues, nor have we gained actual isolation from AR, the updater knows the method to call, which is fine, but that method is part of the AR Api. As a next step we could try to wrap update in our method. As soon as we try to name this method, the code starts pushing back and revealing that, in this case, we have nothing but indirection: ā€˜update_from_updaterā€™, for instance.

At this point, Iā€™d step back and consider how much information I have: do I have a feature coming up, or knowledge of something, that reasonably extends the widget updater? If the answer is no - and Iā€™ve found it is really important to be honest about this - then Iā€™d just remove the object and the tests: I might be left with a slightly less clean controller for instance, but I havenā€™t introduced a concept the domain doesnā€™t need and, as such, kept complexity down.

If the answer is yes, then Iā€™d probably go for dealing with a slightly bad method name and wrap AR#update since I know I will be extending this and have an opportunity to refactor just up the road.

Regarding the tests, I feel like the same notion applies: you feel unsure since you are testing a simple delegation that you have to coherce in setup to make work. If the updater is respnsible for updating, it should be able to figure out what to do given a collection or a single instance. Youā€™d probably write two tests in that case.

This is kinda of long, but hope useful in some way

1 Like