Interface Mocking
UPDATE: This is a gem now: rspec-fire The code in the gem is better than that presented here.
Here is a screencast I put together in response to a recent Destroy All Software screencast on test isolation and refactoring, showing off an idea I’ve been tinkering around with for automatic validation of your implicit interfaces that you stub in tests.
Here is the code for InterfaceMocking
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 |
module InterfaceMocking # Returns a new interface double. This is equivalent to an RSpec double, # stub or, mock, except that if the class passed as the first parameter # is loaded it will raise if you try to set an expectation or stub on # a method that the class has not implemented. def interface_double(stubbed_class, methods = {}) InterfaceDouble.new(stubbed_class, methods) end module InterfaceDoubleMethods include RSpec::Matchers def should_receive(method_name) ensure_implemented(method_name) super end def should_not_receive(method_name) ensure_implemented(method_name) super end def stub!(method_name) ensure_implemented(method_name) super end def ensure_implemented(*method_names) if recursive_const_defined?(Object, @__stubbed_class__) recursive_const_get(Object, @__stubbed_class__). should implement(method_names, @__checked_methods__) end end def recursive_const_get object, name name.split('::').inject(Object) {|klass,name| klass.const_get name } end def recursive_const_defined? object, name !!name.split('::').inject(Object) {|klass,name| if klass && klass.const_defined?(name) klass.const_get name end } end end class InterfaceDouble < RSpec::Mocks::Mock include InterfaceDoubleMethods def initialize(stubbed_class, *args) args << {} unless Hash === args.last @__stubbed_class__ = stubbed_class @__checked_methods__ = :public_instance_methods ensure_implemented *args.last.keys # __declared_as copied from rspec/mocks definition of `double` args.last[:__declared_as] = 'InterfaceDouble' super(stubbed_class, *args) end end end RSpec::Matchers.define :implement do |expected_methods, checked_methods| match do |stubbed_class| unimplemented_methods( stubbed_class, expected_methods, checked_methods ).empty? end def unimplemented_methods(stubbed_class, expected_methods, checked_methods) implemented_methods = stubbed_class.send(checked_methods) unimplemented_methods = expected_methods - implemented_methods end failure_message_for_should do |stubbed_class| "%s does not publicly implement:\n%s" % [ stubbed_class, unimplemented_methods( stubbed_class, expected_methods, checked_methods ).sort.map {|x| " #{x}" }.join("\n") ] end end RSpec.configure do |config| config.include InterfaceMocking end |