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