Integration as Composition

I’m puzzling over the design for a worker and would appreciate your comments on it. I started with the pain of an ugly test, made an interesting refactoring, and decided to drop the test entirely, but I’m not at all sure this is the right decision.

In my mailing list archive Chibrary, I want to sum up the number of threads and messages in a month to present on archive pages. The MonthCountWorker takes a Sym, a unique identifier for a list’s slug + year + month, fetchs the threads for that month, sums them, and stores the sum. The code is trivial, right?:

 
class MonthCountWorker
  include Sidekiq::Worker
 
  def perform serialized_sym
    sym = SymRepo.deserialize serialized_sym
    month = ThreadRepo.month sym
    mc = MonthCount.from month
    MonthCountRepo.new(mc).store
  end
end

But the test I started writing is awful! The SymRepo.deserialize and MonthCount.from calls are pure queries that work in-memory. Following Metz’s excellent guidelines I want to write tests to check their return values. ThreadRepo.month is a nullipotent query that hits the database, so I’d like to stub it out. And finally MonthCountRepo is a command to store the result; I want to set an expectation that it happened.

Imagine writing tests for this. Everything has to stub out the database query, and to confirm that sym, month, and mc have the right values I’d have to set expectations on the next thing in line. I don’t truly want an expectation, but this procedural code has no seams to get at the values flowing through it. There are no boundaries.

I decided to introduce method boundaries and suddenly perform looks like functional composition (and Mike Busch mentioned it reminds him of literate programming):

 
class MonthCountWorker
  include Sidekiq::Worker
 
  def perform serialized_sym
    store monthcount of_month identified_as serialized_sym
  end
 
  def identified_as serialized_sym
    SymRepo.deserialize serialized_sym
  end
 
  def of_month sym
    ThreadRepo.month(sym)
  end
 
  def monthcount month
    MonthCount.from month
  end
 
  def store mc
    MonthCountRepo.new(mc).store
  end
end

Yes, that’s valid Ruby, I checked. The fact that it was such unfamiliar style I had to check tells me I’m either doing something awful or something awesome.

After reflecting on it a moment, I recognize it as function composition, though Ruby doesn’t have first-class support for it.

And now all the little pieces are isolated. The perform method is pure, pointfree composition; I don’t know if I’d test it at all. The individual methods can be easily tested with whatever technique is appropriate.

What tests are appropriate, though? All these methods are already covered over in the unit tests for each class; any test would be redundant, wasted effort.

The only new code here is the composition. This is high-level integration; the only appropriate test would be an integration test that runs against a prepopulated database and queries that the right results are stored back to it.

Or I could take the route Bernhardt mentions in his Boundaries talk and not bother testing it at all. There’s only one way these parts can be composed (not that Ruby has the compile-time type checking to enforce it…), and there really isn’t much value in a test that I would never expect to fail. I want tests to return the investment of time I put into writing and maintaining them. This composition is unlikely to fail, can only cause cosmetic issues, and would be straightforward to debug because it has many seams and no conditionals.

Ultimately my answer to testing this was to rewrite it to a compositional style and not bother testing it. If I get breaks and spend more than a minute or two fixing it in the next few months, I’ll know I chose the wrong answer. What’s your answer?

Want more? I'm not as good at forgetting to update @pushcx on Twitter.