Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Dupe rides on top of ActiveResource to allow you to cuke the client side of a service-oriented app without having to worry about whether or not the service is live or available while cuking.

This branch is 0 commits ahead and 30 commits behind master

Fetching latest commit…

Cannot retrieve the latest commit at this time

README.rdoc

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”).

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:

# gem install dupe

If you're going to use this in a rails project, add this to your cucumber.rb environment (config/environments/cucumber.rb)

config.gem 'dupe'

Then run this rake task to install the gem:

# rake gems:install RAILS_ENV=cucumber

Lastly, from your rails project root, run:

# script/generate dupe

That last command will create the following directories:

  • RAILS_ROOT/features/dupe

  • RAILS_ROOT/features/dupe/definitions

  • RAILS_ROOT/features/dupe/custom_mocks

It will also place example Dupe definitions in RAILS_ROOT/features/dupe/definitions/definitions.rb as well as example custom mocks in RAILS_ROOT/features/dupe/custom_mocks/custom_mocks.rb

Lastly, it will add a load_dupe.rb file (that loads up the definitions and custom mocks) to RAILS_ROOT/features/support/load_dupe.rb

Tutorial

Checkout the dupe example application 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. 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

Consult the API documentation at moonmaster9000.github.com/dupe/api/

TODO List

  • We need “Put”, “Post”, and “Delete”, and “Head” intercept mocking methods. Currently we only have “Get”.

  • We need a rake task that will run your cucumber scenarios and create service documentation based on the dupe log output (i.e., example requests and example responses) that the programmers implementing the service can use as a reference.

Something went wrong with that request. Please try again.