[![Build Status](https://secure.travis-ci.org/JoshCheek/surrogate.png?branch=master)](http://travis-ci.org/JoshCheek/surrogate) <% setup do %> $LOAD_PATH.unshift '../lib', __FILE__ require 'surrogate' <% end %> <% context 'generic it block' do %> require 'surrogate/rspec' describe 'whatever' do it 'does something' do __CODE__ end end <% end %> About ===== Handrolling mocks is the best, but involves more overhead than necessary, and usually has less helpful error messages. Surrogate addresses this by endowing your objects with common things that most mocks need. Currently it is only integrated with RSpec. This codebase should be considered volatile until 1.0 release. The outer interface should be fairly stable, with each 0.a.b version having backwards compatibility for any changes to b (ie only refactorings and new features), and possible interface changes (though probably minimal) for changes to a. Depending on the internals of the code (anything not shown in the readme) is discouraged at this time. If you do want to do this (e.g. to make an interface for test/unit) let me know, and I'll inform you / fork your gem and help update it, for any breaking changes that I introduce. Features ======== * Mock does not diverge from real class because compares signatures of mocks to signatures of the real class. * Support default values * Easily override values * RSpec matchers for asserting what happend (what was invoked, with what args, how many times) * RSpec matchers for asserting the Mock's interface matches the real object * Support for exceptions * Queue return values Usage ===== **Endow** a class with surrogate abilities ```ruby <% test 'endowing', with: :magic_comments do %> class Mock Surrogate.endow self end <% end %> ``` Define a **class method** by using `define` in the block when endowing your class. ```ruby <% test 'class api method', with: :magic_comments do %> class MockClient Surrogate.endow self do define(:default_url) { 'http://example.com' } end end MockClient.default_url # => "http://example.com" <% end %> ``` Define an **instance method** by using `define` outside the block after endowing your class. ```ruby <% test 'instance api method', with: :magic_comments do %> class MockClient Surrogate.endow self define(:request) { ['result1', 'result2'] } end MockClient.new.request # => ["result1", "result2"] <% end %> ``` Ruby's `attr_accessor`, `attr_reader`, `attr_writer` are mimicked with `define_accessor`, `define_reader`, `define_writer`. You can pass a block to `define_accessor` and `define_reader` if you would like to give it a default_value. ```ruby <% test 'attr_* replacements', with: :magic_comments do %> class MockClient Surrogate.endow self define_accessor :default_url end client = MockClient.new client.default_url # => nil client.default_url = 'http://whatever.com' client.default_url # => "http://whatever.com" <% end %> ``` If you expect **arguments**, your block should receive them. This prevents the issues with dynamic mocks where arguments and parameters can diverge. It may seem like more work when you have to write the arguments explicitly, but you only need to do this once, and then you can be sure they match on each invocation. ```ruby <% test 'api method with arguments', with: :magic_comments do %> class MockClient Surrogate.endow self define(:request) { |limit| limit.times.map { |i| "result#{i.next}" } } end MockClient.new.request 3 # => ["result1", "result2", "result3"] <% end %> ``` You don't need a **default if you set the ivar** of the same name (replace `?` with `_p` for predicates, and `!` with `_b` for bang methods, since you can't have question marks or bangs in ivar names). ```ruby <% test 'overriding default by setting the ivar', with: :magic_comments do %> class MockClient Surrogate.endow self define(:initialize) { |id| @id, @connected_p, @reconnect_b = id, true, true } define :id define :connected? define :reconnect! end MockClient.new(12).id # => 12 <% end %> ``` **Override defaults** with `will_` and `will_have_` ```ruby <% test 'overriding default by invoking the method', with: :magic_comments do %> class MockMP3 Surrogate.endow self define :play # default behavior is to do nothing, same as empty block yielding nothing define :info end mp3 = MockMP3.new # verbs mp3.will_play true mp3.play # => true # nouns mp3.will_have_info artist: 'Symphony of Science', title: 'Children of Africa' mp3.info # => {:artist=>"Symphony of Science", :title=>"Children of Africa"} <% end %> ``` **Errors** get raised ```ruby <% test 'errors get raised', with: :magic_comments do %> class MockClient Surrogate.endow self define :request end client = MockClient.new client.will_have_request StandardError.new('Remote service unavailable') begin client.request rescue StandardError => e e # => # end <% end %> ``` **Queue** up return values ```ruby <% test 'queue up return values', with: :magic_comments do %> class MockPlayer Surrogate.endow self define(:move) { 20 } end player = MockPlayer.new player.will_move 1, 9, 3 player.move # => 1 player.move # => 9 player.move # => 3 <% end %> ``` You can define **initialize** ```ruby <% test 'defining initialize', with: :magic_comments do %> class MockUser Surrogate.endow self do define(:find) { |id| new id } end define(:initialize) { |id| @id = id } define(:id) { @id } end user = MockUser.find 12 user.id # => 12 <% end %> ``` Use **clone** to avoid altering state on the class. You can pass it key/value pairs to mass initialize the clone (I think it's probably best to avoid this kind of state on classes, but do support it) ```ruby <% test 'clone to avoid mutating state', with: :rspec, context: 'generic it block' do %> class MockUser Surrogate.endow self do define(:find) { new } end end # using a clone user_class = MockUser.clone user_class.find user_class.was told_to :find MockUser.was_not told_to :find # initializing the clone MockUser.clone(find: nil).find.should be_nil <% end %> ``` **Mass initialize** with `.factory(key: value)`, this can be turned off by passing `factory: false` as an option to Surrogate.endow, or given a different name if the value is the new name. ```ruby <% test 'factory method', with: :magic_comments do %> # default factory method class MockUserWithFactory Surrogate.endow self define(:name) { 'Samantha' } define(:age) { 83 } end user = MockUserWithFactory.factory name: 'Jim', age: 26 user.name # => "Jim" user.age # => 26 # use a different name class MockUserWithRenamedFactory Surrogate.endow self, factory: :construct define(:name) { 'Samantha' } define(:age) { 83 } end user = MockUserWithRenamedFactory.construct name: 'Milla' user.name # => "Milla" user.age # => 83 <% end %> ``` RSpec Integration ================= Currently only integrated with RSpec, since that's what I use. It has some builtin matchers for querying what happened. Load the RSpec matchers. <% setup do %> require 'surrogate/rspec' <% end %> ```ruby require 'surrogate/rspec' ``` Last Instance ------------- Access the last instance of a class ```ruby <% test "last instance", with: :magic_comments do %> class MockMp3 Surrogate.endow self end mp3_class = MockMp3.clone # because you don't want to mutate the singleton mp3 = mp3_class.new mp3_class.last_instance.equal? mp3 # => true <% end %> ``` Nouns ----- Given this mock and assuming the following examples happen within a spec ```ruby <% test "mock mp3 code shouldn't blow up", with: :magic_comments do %> class MockMP3 Surrogate.endow self define(:info) { |song='Birds Will Sing Forever'| 'some info' } end <% end %> ``` <%# need to figure out how to make context and setup blocks conditionally visible %> <% context 'mp3 in spec' do %> class MockMP3 Surrogate.endow self define(:info) { |song='Birds Will Sing Forever'| 'some info' } end describe 'the example' do let(:mp3) { MockMP3.new } it 'executes' do __CODE__ end end <% end %> Check if **was invoked** with `was asked_for` (`was` is an alias of `should`, and `asked_for` is a custom matcher) ```ruby <% test 'noun invocation', with: :rspec, context: 'mp3 in spec' do %> mp3.was_not asked_for :info mp3.info mp3.was asked_for :info <% end %> ``` Invocation **cardinality** by chaining `times(n)` ```ruby <% test 'noun cardinality', with: :rspec, context: 'mp3 in spec' do %> mp3.info mp3.info mp3.was asked_for(:info).times(2) <% end %> ``` Invocation **arguments** by chaining `with(args)` ```ruby <% test 'noun invocation with args', with: :rspec, context: 'mp3 in spec' do %> mp3.info :title mp3.was asked_for(:info).with(:title) <% end %> ``` Supports RSpec's matchers (`no_args`, `hash_including`, etc) ```ruby <% test 'rspec matchers integration', with: :rspec, context: 'mp3 in spec' do %> mp3.info mp3.was asked_for(:info).with(no_args) <% end %> ``` Cardinality of a specific set of args `with(args)` and `times(n)` ```ruby <% test 'times and with', with: :rspec, context: 'mp3 in spec' do %> mp3.info :title mp3.info :title mp3.info :artist mp3.was asked_for(:info).with(:title).times(2) mp3.was asked_for(:info).with(:artist).times(1) <% end %> ``` Verbs ----- Given this mock and assuming the following examples happen within a spec ```ruby <% test 'mp3 that plays in in spec', with: :magic_comments do %> class MockMP3 Surrogate.endow self define(:play) { true } end <% end %> ``` <% context 'mp3 that plays in spec' do %> class MockMP3 Surrogate.endow self define(:play) { true } end describe 'the example' do let(:mp3) { MockMP3.new } it 'executes' do __CODE__ end end <% end %> Check if **was invoked** with `was told_to` ```ruby <% test 'have_been_told_to', with: :rspec, context: 'mp3 that plays in spec' do %> mp3.was_not told_to :play mp3.play mp3.was told_to :play <% end %> ``` Also supports the same `with(args)` and `times(n)` that nouns have. Initialization -------------- Query with `was initialized_with`, which is exactly the same as saying `was told_to(:initialize).with(...)` ```ruby <% test 'initialization test', with: :rspec, context: 'generic it block' do %> class MockUser Surrogate.endow self define(:initialize) { |id| @id = id } define :id end user = MockUser.new 12 user.id.should == 12 user.was initialized_with 12 <% end %> ``` Predicates ---------- Query qith `was asked_if`, all the same chainable methods from above apply. ```ruby <% test 'initialization test', with: :rspec, context: 'generic it block' do %> class MockUser Surrogate.endow self define(:admin?) { false } end user = MockUser.new user.should_not be_admin user.will_have_admin? true user.should be_admin user.was asked_if(:admin?).times(2) <% end %> ``` Substitutability ---------------- Facets of substitutability: method existence, argument types, (and soon argument names) After you've implemented the real version of your mock (assuming a [top-down](http://vimeo.com/31267109) style of development), how do you prevent your real object from getting out of synch with your mock? Assert that your mock has the **same interface** as your real class. This will fail if the mock inherits methods which are not on the real class. It will also fail if the real class has any methods which have not been defined on the mock or inherited by the mock. Presently, it will ignore methods defined directly in the mock (as it considers them to be helpers). In a future version, you will be able to tell it to treat other methods as part of the API (will fail if they don't match, and maybe record their values). ```ruby <% test 'substitutability example', with: :rspec, context: 'generic it block' do %> class User def initialize(id)end def id()end end class MockUser Surrogate.endow self define(:initialize) { |id| @id = id } define :id end # they are the same User.should substitute_for MockUser # they differ MockUser.define :name User.should_not substitute_for MockUser # signatures don't match (you can turn this off by passing `types: false` to substitute_for) class UserWithWrongSignature def initialize()end # no id def id()end end UserWithWrongSignature.should_not substitute_for MockUser # parameter names don't match class UserWithWrongParamNames def initialize(name)end # real one takes an id def id()end end UserWithWrongParamNames.should_not substitute_for MockUser, names: true <% end %> ``` Sometimes you don't want to have to implement the entire interface. In these cases, you can assert that the methods on the mock are a **subset** of the methods on the real class. ```ruby <% test 'subset substitutability example', with: :rspec, context: 'generic it block' do %> class User def initialize(id)end def id()end def name()end end class MockUser Surrogate.endow self define(:initialize) { |id| @id = id } define :id end # doesn't matter that real user has a name as long as it has initialize and id User.should substitute_for MockUser, subset: true # but now it fails b/c it has no address MockUser.define :address User.should_not substitute_for MockUser, subset: true <% end %> ``` Blocks ------ When your method is invoked with a block, you can make assertions about the block. _Note: Right now, block error messages have not been addressed (which means they are probably confusing as shit)_ Before/after hooks (make assertions here) ```ruby <% test 'block example', with: :rspec do %> class MockService Surrogate.endow self define(:create) {} end describe 'something that creates a user through the service' do let(:old_id) { 12 } let(:new_id) { 123 } it 'updates the user_id and returns the old_id' do user_id = old_id service = MockService.new service.create do |user| to_return = user_id user_id = user[:id] to_return end service.was told_to(:create).with { |block| block.call_with({id: new_id}) # this will be given to the block block.returns old_id # provide a return value, or a block that receives the return value (where you can make assertions) block.before { user_id.should == old_id } # assertions about state of the world before the block is called block.after { user_id.should == new_id } # assertions about the state of the world after the block is called } end end <% end %> ``` How do I introduce my mocks? ============================ This is known as dependency injection. There are many ways you can do this, you can pass the object into the initializer, you can pass a factory to your class, you can give the class that depends on the mock a setter and then override it whenever you feel it is necessary, you can use RSpec's `#stub` method to put it into place. Personally, I use [Deject](https://rubygems.org/gems/deject), another gem I wrote. For more on why I feel it is a better solution than the above methods, see it's [readme](https://github.com/JoshCheek/deject/tree/938edc985c65358c074a7c7b7bbf18dc11e9450e#why-write-this). But why write this? =================== Need to put an explanation here soon. In the meantime, I wrote a [blog](http://blog.8thlight.com/josh-cheek/2011/11/28/three-reasons-to-roll-your-own-mocks.html) that touches on the reasons. Special Thanks ============== * [Kyle Hargraves](https://github.com/pd) for changing the name of his internal gem so that I could take Surrogate * [David Chelimsky](http://blog.davidchelimsky.net/) for pairing with me to make Surrogate integrate better with RSpec * [Enova](http://www.enovafinancial.com/) for giving me time and motivation to work on this during Enova Labs. * [8th Light](http://8thlight.com/) for giving me time to work on this during our weekly Wazas, and the general encouragement and interest * The people who have paired with me on this: [Corey Haines](http://coreyhaines.com/) on substitutability, [Kori Roys](https://github.com/koriroys) on `#last_instance`, [Wai Lee](https://github.com/skatenerd) on the new assertion syntax * The people who have contributed: [Michael Baker](with m://github.com/michaelbaker) on the readme.