This project is no longer maintained. If you'd like to take it over, please let me know!
= Dupe
There are lots of great tools out there to ease the burden of prototyping ActiveRecord objects while cuking your application (e.g., thoughtbot's {"Factory Girl"}[http://www.thoughtbot.com/projects/factory_girl]).
But what about prototyping ActiveResource records? That's where Dupe steps in.
== Motivation
If you're going to create a service-oriented rails app with ActiveResource, why not cuke the front end first? Let the behavior of the front-end drive the services you build on the backend. That's exactly what Dupe makes possible.
== Installation
If you want to install this for use in something other than a rails project, simply:
== Rails 3
Dupe versions 0.6.0 and greater only work with Rails 3 / ActiveResource 3. If you're working on a Rails 2.* project, use Dupe version 0.5.3.
= Tutorial
Checkout the {dupe example application}[http://github.com/moonmaster9000/dupe_example_app] for a tutorial on cuking an application with ActiveResource and Dupe.
= Features
==Creating resources
Dupe allows you to quickly create resources, even if you have yet to define them. For example:
irb# require 'dupe' ==> true
irb# b = Dupe.create :book, :title => '2001' ==> <#Duped::Book title="2001" id=1>
irb# a = Dupe.create :author, :name => 'Arthur C. Clarke' ==> <#Duped::Author name="Arthur C. Clarke" id=1>
irb# b.author ==> nil
irb# b.author = a ==> <#Duped::Author name="Arthur C. Clarke" id=1>
irb# b ==> <#Duped::Book author=<#Duped::Author name="Arthur C. Clarke" id=1> title="2001" id=1>
Dupe also provides a way for us to quickly to generate a large number of resources. For example, suppose we have a cucumber scenario that tests paginating through lists of books. To easily create 50 unique books, we could use the Dupe.stub method:
irb# Dupe.stub 50, :books, :like => {:title => proc {|n| "book ##{n} title"}} ==> [<#Duped::Book title="book #1 title" id=1>, <#Duped::Book title="book #2 title" id=2>, ...]
Notice that each book has a unique title, achieved by passing the "proc {|n| "book ##{n} title"}" as the value for the title.
==Finding Resources
Dupe also has a built-in querying system for finding resources you create. In your tests / cucumber step definitions, you'll most likely be using this approach for finding resources. If you're wondering how your app (i.e., ActiveResource) can find resources you create, skip down to the section on ActiveResource.
irb# a = Dupe.create :author, :name => 'Monkey' ==> <#Duped::Author name="Monkey" id=1>
irb# b = Dupe.create :book, :title => 'Bananas', :author => a ==> <#Duped::Book author=<#Duped::Author name="Monkey" id=1> title="Bananas" id=1>
irb# Dupe.find(:author) {|a| a.name == 'Monkey'} ==> <#Duped::Author name="Monkey" id=1>
irb# Dupe.find(:book) {|b| b.author.name == 'Monkey'} ==> <#Duped::Book author=<#Duped::Author name="Monkey" id=1> title="Bananas" id=1>
irb# Dupe.find(:author) {|a| a.id == 1} ==> <#Duped::Author name="Monkey" id=1>
irb# Dupe.find(:author) {|a| a.id == 2} ==> nil
In all cases, notice that we provided the singular form of a model name to Dupe.find. This ensures that we either get back either a single resource (if the query was successful), or nil.
If we'd like to find several resources, we can use the plural form of the model name. For example:
irb# a = Dupe.create :author, :name => 'Monkey', :published => true ==> <#Duped::Author published=true name="Monkey" id=1>
irb# b = Dupe.create :book, :title => 'Bananas', :author => a ==> <#Duped::Book author=<#Duped::Author published=true name="Monkey" id=1> title="Bananas" id=1>
irb# Dupe.create :author, :name => 'Tiger', :published => false ==> <#Duped::Author published=false name="Tiger" id=2>
irb# Dupe.find(:authors) ==> [<#Duped::Author published=true name="Monkey" id=1>, <#Duped::Author published=false name="Tiger" id=2>]
irb# Dupe.find(:authors) {|a| a.published == true} ==> [<#Duped::Author published=true name="Monkey" id=1>]
irb# Dupe.find(:books) ==> [<#Duped::Book author=<#Duped::Author published=true name="Monkey" id=1> title="Bananas" id=1>]
irb# Dupe.find(:books) {|b| b.author.published == false} ==> []
Notice that by using the plural form of the model name, we ensure that we receive back an array - even in the case that the query did not find any results (it simply returns an empty array).
==Finding or Creating Resources
You might have seen this one coming.
Let's assume no genres currently exist. If we call the "find_or_create" method, it will create a new :genre.
irb# Dupe.find_or_create :genre ==> <#Duped::Genre id=1>
If we call it again, it will find the :genre we already created:
irb# Dupe.find_or_create :genre ==> <#Duped::Genre id=1>
You can also pass conditions to find_or_create as a hash:
irb# Dupe.find_or_create :genre, :name => 'Science Fiction', :label => 'sci-fi' ==> <#Duped::Genre label="sci-fi" name="Science Fiction" id=2>
irb# Dupe.find_or_create :genre, :name => 'Science Fiction', :label => 'sci-fi' ==> <#Duped::Genre label="sci-fi" name="Science Fiction" id=2>
== Defining a resource
Though often we may get away with creating resources willy-nilly, it's sometimes quite handy to define a resource, giving it default attributes and callbacks.
=== Attributes with default values
Suppose we're creating a 'book' resource. Perhaps our app assumes every book has a title, so let's define a book resource that specifies just that:
irb# Dupe.define :book do |attrs| --# attrs.title 'Untitled' --# attrs.author --# end ==> #<Dupe::Model:0x17b2694 ...>
Basically, this reads like "A book resource has a title attribute with a default value of 'Untitled'. It also has an author attribute." Thus, if we create a book and we don't specify a "title" attribute, it should create a "title" for us, as well as provide a nil "author" attribute.
irb# b = Dupe.create :book ==> <#Duped::Book author=nil title="Untitled" id=1>
If we provide our own title, it should allow us to override the default value:
irb# b = Dupe.create :book, :title => 'Monkeys!' ==> <#Duped::Book author=nil title="Monkeys!" id=2>
=== Attributes with procs as default values
Sometimes it might be convenient to procedurally define the default value for an attribute:
irb# Dupe.define :book do |attrs| --# attrs.title 'Untitled' --# attrs.author --# attrs.isbn do --# rand(1000000) --# end --# end
Now, every time we create a book, it will get assigned a random ISBN number:
irb# b = Dupe.create :book ==> <#Duped::Book author=nil title="Untitled" id=1 isbn=895825>
irb# b = Dupe.create :book ==> <#Duped::Book author=nil title="Untitled" id=2 isbn=606472>
Another common use of this feature is for associations. Lets suppose we'd like to make sure that a book always has a genre, but a genre should be its own resource. We can accomplish that by taking advantage of Dupe's "find_or_create" method:
irb# Dupe.define :book do |attrs| --# attrs.title 'Untitled' --# attrs.author --# attrs.isbn do --# rand(1000000) --# end --# attrs.genre do --# Dupe.find_or_create :genre --# end --# end
Now when we create books, Dupe will associate them with an existing genre (the first one it finds), or if none yet exist, it will create one.
First, let's confirm that no genres currently exist:
irb# Dupe.find :genre
Dupe::Database::TableDoesNotExistError: The table ':genre' does not exist.
from /Library/Ruby/Gems/1.8/gems/dupe-0.4.0/lib/dupe/database.rb:30:in select' from /Library/Ruby/Gems/1.8/gems/dupe-0.4.0/lib/dupe/dupe.rb:295:in
find'
from (irb):135
Next, let's create a book:
irb# b = Dupe.create :book ==> <#Duped::Book genre=<#Duped::Genre id=1> author=nil title="Untitled" id=1 isbn=62572>
Notice that it create a genre. If we tried to do another Dupe.find for the genre:
irb# Dupe.find :genre ==> <#Duped::Genre id=1>
Now, if create another book, it will associate with the genre that was just created:
irb# b = Dupe.create :book ==> <#Duped::Book genre=<#Duped::Genre id=1> author=nil title="Untitled" id=2 isbn=729317>
=== Attributes with transformers
Occasionally, you may find it useful to have attribute values transformed upon creation.
For example, suppose we want to create books with publish dates. In our cucumber scenario's, we may prefer to simply specify a date like '2009-12-29', and have that automatically transformed into an ruby Date object.
irb# Dupe.define :book do |attrs| --# attrs.title 'Untitled' --# attrs.author --# attrs.isbn do --# rand(1000000) --# end --# attrs.publish_date do |publish_date| --# Date.parse(publish_date) --# end --# end
Now, let's create a book:
irb# b = Dupe.create :book, :publish_date => '2009-12-29' ==> <#Duped::Book author=nil title="Untitled" publish_date=Tue, 29 Dec 2009 id=1 isbn=826291>
irb# b.publish_date ==> Tue, 29 Dec 2009
irb# b.publish_date.class ==> Date
=== Uniquify attributes
If you'd just like to make sure that some attributes get a unique value, then you can use the uniquify method:
irb# Dupe.define :book do |attrs| --# attrs.uniquify :title, :genre, :author --# end
Now, Dupe will do its best to assign unique values to the :title, :genre, and :author attributes on any records it creates:
irb# b = Dupe.create :book ==> <#Duped::Book author="book 1 author" title="book 1 title" genre="book 1 genre" id=1>
irb# b2 = Dupe.create :book, :title => 'Rooby' ==> <#Duped::Book author="book 2 author" title="Rooby" genre="book 2 genre" id=2>
=== Sequences
The "uniquify" method is great if don't care too much about the format of the values it creates. But what if you'd like to ensure that the value of an attribute conforms to a specific format?
irb# Dupe.sequence :email do |n| --# "email-#{n}@somewhere.com" --# end
irb# Dupe.define :user do |user| --# user.uniquify :name --# user.email do --# Dupe.next :email --# end --# end
irb# Dupe.create :user ==> <#Duped::User name="user 1 name" id=1 email="email-1@somewhere.com">
irb# Dupe.create :user ==> <#Duped::User name="user 2 name" id=2 email="email-2@somewhere.com">
=== Callbacks
Suppose we'd like to make sure that our books get a unique label. We can accomplish that with an after_create callback:
irb# Dupe.define :book do |attrs| --# attrs.title 'Untitled' --# attrs.author --# attrs.isbn do --# rand(1000000) --# end --# attrs.publish_date do |publish_date| --# Date.parse(publish_date) --# end --# attrs.after_create do |book| --# book.label = book.title.downcase.gsub(/\ +/, '-') + "--#{book.id}" --# end --# end
irb# b = Dupe.create :book, :title => 'Rooby on Rails' ==> <#Duped::Book author=nil label="rooby-on-rails--1" title="Rooby on Rails" publish_date=nil id=1 isbn=842518>
= ActiveResource
So how does Dupe actually help us to spec/test ActiveResource-based applications? It uses a simple, yet sophisticated "intercept-mocking" technique, whereby failed network requests sent by ActiveResource fallback to the "Duped" network. Consider the following:
irb# Dupe.create :book, :title => 'Monkeys!' ==> <#Duped::Book title="Monkeys!" id=1>
irb# class Book < ActiveResource::Base; self.site = ''; end ==> ""
irb# Book.find(1) ==> #<Book:0x1868a20 @attributes={"title"=>"Monkeys!", "id"=>1}, prefix_options{}
Voila! When the Book class was unable to find the book with id 1, it asked Dupe if it knew about any book resources with id 1. Check out the Dupe network log for a clue as to what happened behind the scenes:
irb# puts Dupe.network.log.pretty_print
Logged Requests:
Request: GET /books/1.xml
Response:
<?xml version="1.0" encoding="UTF-8"?>
<book>
<title>Monkeys!</title>
<id type="integer">1</id>
</book>
Similarly:
irb# Book.find(:all) ==> [#<Book:0x185608c @attributes={"title"=>"Monkeys!", "id"=>1}, prefix_options{}]
irb# puts Dupe.network.log.pretty_print
Logged Requests:
Request: GET /books.xml
Response:
<?xml version="1.0" encoding="UTF-8"?>
<books type="array">
<book>
<title>Monkeys!</title>
<id type="integer">1</id>
</book>
</books>
==Intercept Mocking
Dupe knew how to handle simple find by id and find :all lookups from ActiveResource. But what about other requests we might potentially make?
===GET requests
In this section, you'll learn how to mock custom GET requests.
irb# Dupe.create :author, :name => 'Monkey', :published => true ==> <#Duped::Author name="Monkey" published=true id=1>
irb# Dupe.create :author, :name => 'Tiger', :published => false ==> <#Duped::Author name="Tiger" published=false id=2>
irb# class Author < ActiveResource::Base; self.site = ''; end ==> ""
irb# Author.find :all, :from => :published ==> Dupe::Network::RequestNotFoundError: No mocked service response found for '/authors/published.xml'
Obviously, Dupe had no way of anticipating this possibility. However, you can create your own custom intercept mock for this:
irb# Get %r{/authors/published.xml} do --# Dupe.find(:authors) {|a| a.published == true} --# end ==> #<Dupe::Network::Mock:0x1833e88 @url_pattern=/\/authors\/published.xml/, @verb=:get, @response=#Proc:0x01833f14@(irb):13
irb# Author.find :all, :from => :published ==> [#<Author:0x1821d3c @attributes={"name"=>"Monkey", "published"=>true, "id"=>1}, prefix_options{}]
irb# puts Dupe.network.log.pretty_print
Logged Requests:
Request: GET /authors/published.xml
Response:
<?xml version="1.0" encoding="UTF-8"?>
<authors type="array">
<author>
<name>Monkey</name>
<published type="boolean">true</published>
<id type="integer">1</id>
</author>
</authors>
The "Get" method requires a url pattern and a block. In most cases, your block will return a Dupe.find result. Internally, Dupe will transform that into XML (or JSON). However, if your "Get" block returns a string, Dupe will use that as the response body and not attempt to do any transformations on it.
Suppose instead the service expected us to pass published as a query string parameter:
irb# Author.find :all, :params => {:published => true}
Dupe::Network::RequestNotFoundError: No mocked service response found for '/authors.xml?published=true'
from /Library/Ruby/Gems/1.8/gems/dupe-0.4.0/lib/dupe/network.rb:32:in match' from /Library/Ruby/Gems/1.8/gems/dupe-0.4.0/lib/dupe/network.rb:17:in
request'
from /Library/Ruby/Gems/1.8/gems/dupe-0.4.0/lib/dupe/active_resource_extensions.rb:15:in get' from /Library/Ruby/Gems/1.8/gems/activeresource-2.3.5/lib/active_resource/base.rb:639:in
find_every'
from /Library/Ruby/Gems/1.8/gems/activeresource-2.3.5/lib/active_resource/base.rb:582:in `find'
from (irb):18
We can mock this with the following:
irb# Get %r{/authors.xml\?published=(true|false)$} do |published| --# if published == 'true' --# Dupe.find(:authors) {|a| a.published == true} --# else --# Dupe.find(:authors) {|a| a.published == false} --# end --# end
irb# Author.find :all, :params => {:published => true} ==> [#<Author:0x17db094 @attributes={"name"=>"Monkey", "published"=>true, "id"=>1}, prefix_options{}]
irb# Author.find :all, :params => {:published => false} ==> [#<Author:0x17c68c4 @attributes={"name"=>"Tiger", "published"=>false, "id"=>2}, prefix_options{}]
irb# puts Dupe.network.log.pretty_print
Logged Requests:
Request: GET /authors.xml?published=true
Response:
<?xml version="1.0" encoding="UTF-8"?>
<authors type="array">
<author>
<name>Monkey</name>
<published type="boolean">true</published>
<id type="integer">1</id>
</author>
</authors>
Request: GET /authors.xml?published=false
Response:
<?xml version="1.0" encoding="UTF-8"?>
<authors type="array">
<author>
<name>Tiger</name>
<published type="boolean">false</published>
<id type="integer">2</id>
</author>
</authors>
===POST requests
Out of the box you get a POST intercept mock:
irb# Dupe.define :author
irb# class Author < ActiveResource::Base; self.site = ''; end ==> ""
irb# Author.create :name => "CS Lewis" ==> #<Author:0x1a4ca58 @attributes={"name"=>"CS Lewis", "id"=>1}, @prefix_options={}>
Author.create sent a network POST to /authors.xml and Dupe responded by creating the resource with the requested parameters:
irb# Dupe.find :authors ==> [<#Duped::Author name="CS Lewis" id=1>]
You can also overwrite the default POST intercept mock for your resource by using the Post method:
irb# Post %r{/authors.xml} do |post_data| raise Dupe::UnprocessableEntity.new(:name => " must be present.") unless post_data["name"] Dupe.create :author, post_data end ==> #<Dupe::Network::PostMock:0x1a1afe4 @url_pattern=/\/authors.xml/, @response=#Proc:0x01a1b084@(irb):13, @verb=:post>
Now, when you try to create an Author without a name, it will respond with the appropriate mocked errors.
irb# Dupe.find(:authors) ==> []
irb# a = Author.create ==> a = #<Author:0x1a19fb8 @attributes={}, @errors=#<ActiveResource::Errors:0x1a10bc0 @errors={"base"=>["name must be present."]}, @base=#<Author:0x1a19fb8 ...>>, @prefix_options={}>
irb# a = a.valid? ==> false
irb# a = a.new? ==> true
Because our custom Post mock determined that the resource was invalid, Dupe did not mock the resource:
irb# Dupe.find(:authors) ==> []
When we create the Author with the required attributes, it will now be considered valid.
irb# a = Author.create :name => "CS Lewis" ==> #<Author:0x19f1edc @attributes={"name"=>"CS Lewis", "id"=>1}, @prefix_options={}>
irb# a.valid? ==> true
irb# a.new? ==> false
Since our custom Post mock considered the resource valid, it went ahead and created the resource:
irb# Dupe.find(:authors) ==> [<#Duped::Author name="CS Lewis" id=1>]
===PUT requests
In ActiveResource, when you update a resource that already exists via the "save" method, it translates to a PUT request. Dupe provides basic PUT intercept mocks out of the box, and like GET and POST mocks, it allows you to override the default PUT intercept mock, and create new ones.
Let's again examine an "author" resource:
irb# Dupe.define :author
irb# class Author < ActiveResource::Base; self.site = ''; end ==> ""
irb# Author.create :name => "CS Lewis" # --> Dupe intercepts this POST request ==> #<Author:0x1a4ca58 @attributes={"name"=>"CS Lewis", "id"=>1}, @prefix_options={}>
irb# Dupe.find :authors ==> [<#Duped::Author name="CS Lewis" id=1>]
irb# a = Author.find 1 # --> Dupe intercepts this GET request ==> #<Author:0x1a4ca58 @attributes={"name"=>"CS Lewis", "id"=>1}, @prefix_options={}>
So far, we've created a resource (via dupe's POST intercept mocking), and we've also found the resource we create (via dupe's GET intercept mocking). Now, let's attempt to update (PUT) the resource:
irb# a.name = "Frank Herbert"
irb# a.save # --> Dupe intercepts this PUT request ==> true
Dupe intercepted the PUT request that ActiveResource attempted to send, and updated the Duped resource accordingly:
irb# a ==> #<Author:0x1a4ca58 @attributes={"name"=>"Frank Herbert", "id"=>1}, @prefix_options={}>
irb# Dupe.find :authors ==> [<#Duped::Author name="Frank Herbert" id=1>]
You can also overwrite the default PUT intercept mock for your resource by using the "Put" method:
irb# Put %r{/authors/(\d+).xml} do |id, put_data| raise Dupe::UnprocessableEntity.new(:name => " must be present.") unless put_data[:name] Dupe.find(:author) {|a| a.id == id.to_i}.merge! put_data end ==> #<Dupe::Network::PostMock:0x1a1afe4 @url_pattern=/\/authors.xml/, @response=#Proc:0x01a1b084@(irb):13, @verb=:post>
Now, if we try to update our Author without a name, it will respond with the appropriate errors.
irb# Dupe.find :authors ==> [<#Duped::Author name="Frank Herbert" id=1>]
irb# a = Author.find 1 # --> Dupe intercepts this GET request ==> #<Author:0x1a4ca58 @attributes={"name"=>"Frank Herbert", "id"=>1}, @prefix_options={}>
irb# a.name = nil
irb# a.save ==> false
irb# a.errors.on_base ==> ["name must be present"]
Since our Put intercept mock raise the Dupe::UnprocessableEntity exception, the underlying Duped record remains unchanged, just as we would expect the real service to have operated:
irb# Dupe.find :authors ==> [<#Duped::Author name="Frank Herbert" id=1>]
We can, of course, at this point still update the name to a non-nil value and attempt to save it again:
irb# a.name = "Matt Parker"
irb# a.save ==> true
irb# Dupe.find :authors ==> [<#Duped::Author name="Matt Parker" id=1>]
===DELETE requests
As you might have guessed, Dupe also supports DELETE intercept mocking (ActiveResource::Base#destroy):
irb# Dupe.define :author
irb# class Author < ActiveResource::Base; self.site = ''; end ==> ""
irb# Author.create :name => "CS Lewis" # --> Dupe intercepts this POST request ==> #<Author:0x1a4ca58 @attributes={"name"=>"CS Lewis", "id"=>1}, @prefix_options={}>
irb# Dupe.find :authors ==> [<#Duped::Author name="CS Lewis" id=1>]
irb# a = Author.find 1 # --> Dupe intercepts this GET request ==> #<Author:0x1a4ca58 @attributes={"name"=>"CS Lewis", "id"=>1}, @prefix_options={}>
irb# a.destroy ==> #<ActiveResource::Response:0x181c1c0 @body="", @message="200", @code=200, @headers={"Content-Length"=>"0"}
irb# Dupe.find :authors ==> []
And also, as you might have guessed, you can override the default DELETE intercept mock for a resource:
irb# Delete %r{/books/(\d+).xml} do |id| puts "deleting the book with id #{id}" Dupe.delete(:book) {|b| b.id == id.to_i} end
irb# a = Author.create :name => "some author"
irb# a.destroy ==> "deleting the book with id 1"
== More
API docs available at: http://rdoc.info