Skip to content

Instantly share code, notes, and snippets.

@cvincent
Last active December 12, 2015 09:09
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save cvincent/4749470 to your computer and use it in GitHub Desktop.
Save cvincent/4749470 to your computer and use it in GitHub Desktop.
See "1.description.txt" for rationale.
Having read "Growing Object-Oriented Software Guided by Tests" (GOOS), I
finally stopped worrying and learned to love the "mockist" or "London School"
of TDD. However, the unfortunate fact of this style in Ruby is that tests using
mocks can easily find themselves out of sync with the concrete implementations
instructed into existence by those mocks, especially when making adjustments to
the design of the system. The result is a bunch of green tests when the truth
is that the system may not actually integrate. Of course, this wasn't a problem
in GOOS as the authors used Java (blech), which is a statically-typed, compiled
language, meaning the build would fail if method signatures went out of sync.
One solution is to do integration testing of your subsystems. This is
suboptimal. They may not be as slow as end-to-end acceptance tests, but
integration tests are slow. And there are simply too many possible paths within
an integrated system to be completely confident that the tests are giving the
vital feedback you want. Still, this approach is better than nothing at all.
Another solution is to use something like rspec-fire
(https://github.com/xaviershay/rspec-fire). It allows you to specify the class
for which your mock is standing in. When unit testing in isolation, the option
is ignored, but when running your entire test suite, tests will fail if the
mocks include stubs or expectations which are not implemented by the specified
class. I think this is a big step in the right direction, but it doesn't quite
hit the mark. When using mocks to drive out the design of collaborators, you
should be mocking roles rather than concrete classes, roles which your system
may at some point want more than one class to implement.
Another approach I found is rspec-roles
(https://github.com/joakimk/rspec-roles). This one is halfway there. It's like
the missing half of rspec-fire. It allows you to define roles explicitly and
then assert in your class specs what roles the class plays, but it doesn't
have any impact on your mocks.
To that end, I thought about a combined approach which could hopefully resolve
the issue with minimal effort. It's included in the Gist below. This is purely
a hypothetical syntax, and I haven't started any attempt at actually
implementing it. I thought I'd throw it out for the community to talk about and
maybe get the ball rolling on something like it (or something better).
# A role
# * Describes the role’s interface
# * Entire method signature included in descriptions using a lambda
# * Default return values for stubs; may want this to be a block for dynamic responses
# * Should not take long to load so that isolated specs requiring roles stay fast
require "rspec-crossfire"
describe_role "Jsonable" do
responds_to :to_json, ->(p1, p2 = 5, *args){}, '{"asdf": "default"}'
end
# Mocking the role
# * Comes with default stubs to ease usability
# * Stubs can be overridden with expectations or different return values
# * Stubs and expectations raise errors if expecting incorrect arity
# * Could allow #mock_role to accept more than one role
require "fast_spec_helper"
require "roles/jsonable"
describe DependsUponJsonable do
it "instantiates with a Jsonable" do
jsonable = mock_role("Jsonable")
jsonable.should_receive(:to_json).with(1, 2, "asdf") { '{"asdf": "override"}' }
DependsUponJsonable.new(jsonable)
end
end
# Specifying the role on a concrete class
# * Ensures entire role interface is adhered to
# * Method parameters must match, both names and arity
# * Default values for parameters may vary
# * Perhaps pass in a block returning a real instance of the class so that methods
# implemented by way of metaprogramming can be seen.
require "fast_spec_helper"
require "roles/jsonable"
describe ImplementsJsonable do
implements_role "Jsonable"
end
@cvincent
Copy link
Author

I realize that this looks suspiciously like the explicit interface syntax offered by Java and other languages. Functionally, that's exactly what it is. It's doing for our specs what it would do for the compiler in other languages. I really don't think that's a bad thing. We can have the advantages of rapid prototyping and minimal syntax given by Ruby's dynamic nature, while still capturing the main benefit of using a statically-typed language like Java when we need to be able to trust that the green builds of our business-critical applications really are deployable. In my experience, nobody likes feeling fear whilst invoking cap production deploy.

Were this testing approach implemented, which could probably be done easily enough by combining the existing rspec-fire and rspec-roles libraries, we would be able to make adjustments to our application designs with near impunity, confident that the contracts between our objects and the collaborators they depend upon are not breached, assured that every object is playing the roles expected of it.

If a class's interface changes, the role will need to be updated before the tests will pass, along with any other classes and specs which implement or depend upon the role. A similar effect is had if a class's expectations of its collaborators change. And the tests which fail would be exactly the ones which should, no more and no less. They would point the developer to exactly the place in the code s/he needs to see. Bliss!

This would fill the gap that makes mockist testing seem unappealing to many Rubyists (including myself, before I came to understand the benefits), and would do so whilst adding the minimal amount of additional testing required to get the job done (in the words of one Gary Bernhardt, it's just enough ass). I think the mock_role syntax might also subliminally reinforce the "correct" use of mocks, thus resulting in better-designed applications and happier engineers around the world.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment