Readme.md in surrogate-0.3.0 vs Readme.md in surrogate-0.3.1

- old
+ new

@@ -1,95 +1,379 @@ -Explanation and examples coming soon, but here is a simple example I wrote up for a lightning talk: +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. + + +Features +======== + +* Declarative syntax +* 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 +* Initialization information is always recorded + + +Usage +===== + +**Endow** a class with surrogate abilities + ```ruby -require 'surrogate' -require 'surrogate/rspec' +class Mock + Surrogate.endow self +end +``` -module Mock - class User - Surrogate.endow self - define(:name) { 'Josh' } - define :phone_numbers - define :add_phone_number do |area_code, number| - @phone_numbers << [area_code, number] - end +Define a **class method** by using `define` in the block when endowing your class. + +```ruby +class MockClient + Surrogate.endow self do + define(:default_url) { 'http://example.com' } end end +MockClient.default_url # => "http://example.com" +``` + +Define an **instance method** by using `define` outside the block after endowing your class. + +```ruby +class MockClient + Surrogate.endow self + define(:request) { ['result1', 'result2'] } +end + +MockClient.new.request # => ["result1", "result2"] +``` + +If you care about the **arguments**, your block can receive them. + +```ruby +class MockClient + Surrogate.endow self + define(:request) { |limit| limit.times.map { |i| "result#{i.next}" } } +end + +MockClient.new.request 3 # => ["result1", "result2", "result3"] +``` + +You don't need a **default if you set the ivar** of the same name + +```ruby +class MockClient + Surrogate.endow self + define(:initialize) { |id| @id = id } + define :id +end +MockClient.new(12).id # => 12 +``` + +**Override defaults** with `will_<verb>` and `will_have_<noun>` + +```ruby +class MockMP3 + Surrogate.endow self + define :play # defaults are optional, will raise error if invoked without being told what to do + 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"} +``` + +**Errors** get raised + +```ruby +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 # => #<StandardError: Remote service unavailable> +end +``` + +**Queue** up return values + +```ruby +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 + +# then back to default behaviour (or error if none provided) +player.move # => 20 +``` + +You can define **initialize** + +```ruby +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 +``` + + +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. + +```ruby +require 'surrogate/rspec' +``` + +Nouns +----- + +Given this mock and assuming the following examples happen within a spec + +```ruby +class MockMP3 + Surrogate.endow self + define(:info) { 'some info' } +end +``` + +Check if **was invoked** with `have_been_asked_for_its` + +```ruby +mp3.should_not have_been_asked_for_its :info +mp3.info +mp3.should have_been_asked_for_its :info +``` + +Invocation **cardinality** by chaining `times(n)` + +```ruby +mp3.info +mp3.info +mp3.should have_been_asked_for_its(:info).times(2) +``` + +Invocation **arguments** by chaining `with(args)` + +```ruby +mp3.info :title +mp3.should have_been_asked_for_its(:info).with(:title) +``` + +Supports RSpec's `no_args` matcher (the others coming in future versions) + +```ruby +mp3.info +mp3.should have_been_asked_for_its(:info).with(no_args) +``` + +Cardinality of a specific set of args `with(args)` and `times(n)` + +```ruby +mp3.info :title +mp3.info :title +mp3.info :artist +mp3.should have_been_asked_for_its(:info).with(:title).times(2) +mp3.should have_been_asked_for_its(:info).with(:artist).times(1) +``` + + +Verbs +----- + +Given this mock and assuming the following examples happen within a spec + +```ruby +class MockMP3 + Surrogate.endow self + define(:play) { true } +end +``` + +Check if **was invoked** with `have_been_told_to` + +```ruby +mp3.should_not have_been_told_to :play +mp3.play +mp3.should have_been_told_to :play +``` + +Also supports the same `with(args)` and `times(n)` that nouns have. + + +Initialization +-------------- + +Query with `have_been_initialized_with`, which is exactly the same as saying `have_been_told_to(:initialize).with(...)` + +```ruby +class MockUser + Surrogate.endow self + define(:initialize) { |id| @id = id } + define :id +end +user = MockUser.new 12 +user.id.should == 12 +user.should have_been_initialized_with 12 +``` + +Initialization is **always recorded**, so that you don't have to override it just to be able to query. + +```ruby +class MockUser < Struct.new(:id) + Surrogate.endow self +end +user = MockUser.new 12 +user.id.should == 12 +user.should have_been_initialized_with 12 +``` + + +Substitutability +---------------- + +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 methods not on the real class. And it will fail +if the real class has or lacks any methods defined on the mock or inherited by the mock. + +Presently, it will ignore methods defined directly in the mock (as it adds quite a few of its own methods, +and generally 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 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 +MockUser.should substitute_for User + +# mock has extra method +MockUser.define :name +MockUser.should_not substitute_for User + +# the same again via inheritance +class UserWithName < User def name()end - def phone_numbers()end - def add_phone_number()end end +MockUser.should substitute_for UserWithName -describe do - it 'ensures the mock lib looks like real lib' do - Mock::User.should substitute_for User - end +# real class has extra methods +class UserWithNameAndAddress < UserWithName + def address()end +end +MockUser.should_not substitute_for UserWithNameAndAddress +``` - let(:user) { Mock::User.new } +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. - example 'you can tell it how to behave and ask what happened with it' do - user.will_have_name "Sally" +```ruby +class User + def initialize(id)end + def id()end + def name()end +end - user.should_not have_been_asked_for_its :name - user.name.should == "Sally" - user.should have_been_asked_for_its :name - 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 +MockUser.should substitute_for User, subset: true + +# but now it fails b/c it has no addres +MockUser.define :address +MockUser.should_not substitute_for User, subset: true ``` -TODO ----- +How do I introduce my mocks? +============================ -* substitutability -* add methods for substitutability +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). -Features for future vuersions ------------------------------ -* change queue notation from will_x_qeue(1,2,3) to will_x(1,2,3) -* arity option -* support for raising errors -* need some way to talk about and record blocks being passed -* support all rspec matchers (RSpec::Mocks::ArgumentMatchers) -* assertions for order of invocations & methods +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. -Future subset substitutability +Special Thanks +============== - # ===== Substitutability ===== +* [Corey Haines](http://coreyhaines.com/) for pairing on it with me +* [8th Light](http://8thlight.com/) for giving me time to work on this during our weekly Wazas, and the general encouragement and interest - # real user is not a suitable substitute if missing methods that mock user has - user_class.should_not substitute_for Class.new - # real user must have all of mock user's methods to be substitutable - substitutable_real_user_class = Class.new do - def self.find() end - def initialize(id) end - def id() end - def name() end - def address() end - def phone_numbers() end - def add_phone_number(area_code, number) end - end - user_class.should substitute_for substitutable_real_user_class - user_class.should be_subset_of substitutable_real_user_class +TODO +---- - # real user class is not a suitable substitutable if has extra methods, but is suitable subset - real_user_class = substitutable_real_user_class.clone - def real_user_class.some_class_meth() end - user_class.should_not substitute_for real_user_class - user_class.should be_subset_of real_user_class +* Add a better explanation for motivations +* Figure out whether I'm supposed to be using clone or dup for the object -.^ (looks like there may also be an `initialize_copy` method I can take advantage of instead of crazy stupid shit I'm doing now) - real_user_class = substitutable_real_user_class.clone - real_user_class.send(:define_method, :some_instance_method) {} - user_class.should_not substitute_for real_user_class - user_class.should be_subset_of real_user_class - # subset substitutability does not work for superset - real_user_class = substitutable_real_user_class.clone - real_user_class.undef_method :address - user_class.should_not be_subset_of real_user_class +Future Features +--------------- + +* Support all RSpec matchers (hash_including, anything, etc. see them in RSpec::Mocks::ArgumentMatchers) +* have some sort of reinitialization that can hook into setup/teardown steps of test suite +* Support arity checking as part of substitutability +* Support for blocks +* Ability to disassociate the method name from the test (e.g. you shouldn't need to change a test just because you change a name) +* declare normal methods as being part of the API (e.g. for inheritance) +* assertions for order of invocations & methods