Skip to content

moonmaster9000/dupe_example_app

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

13 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Dupe tutorial

In this tutorial, you will learn how to quickly and cleanly mock services with Dupe and Cucumber. We’ll start with simple resource creation methods, and then DRY up our work using Dupe’s facilities for quick resource prototyping.

Context

Let’s assume we’re going to create an application for browsing a library. The trick: we’ll access the library books not through a database, but over services. For this, we’ll use “ActiveResource”, the rails library for managing restful resources.

Setting up our application

If you haven’t already, install the following gems: cucumber, cucumber-rails, rspec, rspec-rails, webrat, dupe

If you are pre rails-3.1 heres directions on setting up our application. This is not necesary in 3.1:

# rails library
# cd library
# script/generate cucumber
# script/generate rspec
# script/generate dupe

Next, add dupe to your gem list in config/environments/cucumber.rb:

config.gem "dupe"

Also, since we’ll be using HAML for our views in this tutorial, add “haml” to your gem list in config/environments.rb

config.gem "haml"

Lastly, since you won’t be needing ActiveRecord, you might as well go ahead and remove it from the list of loaded libraries (config/environment.rb):

config.frameworks -= [ :active_record]

Feature 1: List all books

During this first feature, we’ll learn about the “Dupe.create” method for generating service resources.

Lets create a (naively) simple feature: viewing all books in the library.

First, create the file features/books.feature, and add the following declarative scenario to it:

Feature: Viewing all books in the library

  Scenario: the books index page
    Given the library has lots of books
    When I go to the books index page
    Then I should see all of the books in the library

Next, run the feature (cucumber features/), watch it fail, then copy the missing step definitions into features/step_definitions/book_steps.rb.

Given /^the library has lots of books$/ do
  pending
end

Then /^I should see all of the books in the library$/ do
  pending
end

At this point, we need to mock some resources. In a normal database-driven rails app, we would probably use a library like “Factory Girl” to help us quickly prototype some data, but our application is service-driven, we’ll use “Dupe” to fake our services for the purposes of testing.

Let’s first consider translating “Given the library has lots of books” into a step definition. All we really want to do is to ensure that the library has a few books in it, so let’s use Dupe to create a couple of books:

Given /^the library has lots of books$/ do
  Dupe.create :book, :name => "Rooby Rocks"
  Dupe.create :book, :name => "Rails Rocks too!"
end

We’ve created two books, each with different names. When Dupe creates a resource, it will automatically assign a unique (sequentially assigned) “id” attribute to it, much like a database would.

Next, let’s implement “Then I should see all of the books in the library”:

Then /^I should see all of the books in the library$/ do
  Then %{I should see "Rooby Rocks"}
  Then %{I should see "Rails Rocks too!"}
end

All we’ve done here is translate our declarative statement into two more detailed (“imperative”) statements. “I should see” matches a webrat step definition (if you don’t know what webrat is, read up on it on github).

Let’s run our cucumber scenario again:

# cucumber features/
Feature: Viewing all books in the library

  Scenario: the books index page                      # features/books.feature:3
    Given the library has lots of books               # features/step_definitions/book_steps.rb:1
    When I go to the books index page                 # features/step_definitions/webrat_steps.rb:10
      Can't find mapping from "the books index page" to a path.
      Now, go and add a mapping in ./features/support/paths.rb (RuntimeError)
      ./features/support/paths.rb:22:in `path_to'
      ./features/step_definitions/webrat_steps.rb:11:in `/^I go to (.+)$/'
      features/books.feature:5:in `When I go to the books index page'
    Then I should see all of the books in the library # features/step_definitions/book_steps.rb:5


    Logged Requests:


Failing Scenarios:
cucumber features/books.feature:3 # Scenario:

It failed, but don’t worry, that was expected. We have to tell webrat how to translate “the books index page” into a path. Open up features/support/paths.rb and add the following when condition into it:

when /the books index page/
  books_path

You’ll also need to add a route for books to routes.rb:

ActionController::Routing::Routes.draw do |map|
  map.resources :books
end

And though I’d normally recommend rspecc’ing the controller/models/views, for the sake of brevity, to help keep this tutorial focused on Dupe, we’ll simply create them:

# app/controllers/books_controller.rb

class BooksController < ApplicationController
  def index 
    @books = Book.find :all
  end
end
# app/models/book.rb

class Book < ActiveResource::Base
  # you can also just set this to an empty string, it doesn't
  # really matter since we don't yet have a service backend
  self.site = 'http://some.service.provider.com'
end
# app/views/books/index.haml

.books
  - @books.each do |book|
    .book
      .name
        = book.name

Great! Now if we run “cucumber features/” again, they should all pass.

Updating Feature 1 to see the authors of books

Now let’s suppose that our client has asked us to update this feature. In addition to seeing the titles of books, they also want to see the authors that wrote the books.

In order to accomplish this, let’s assume that a book has_one author, and update our step definitions accordingly:

Given /^the library has lots of books$/ do
  Dupe.create :book, :name => "Rooby Rocks", :author => (Dupe.create :author, :name => 'Matz')
  Dupe.create :book, :name => "Rails Rocks too!", :author => (Dupe.create :author, :name => 'DHH')
end

Then /^I should see all of the books in the library$/ do
  Then %{I should see "Rooby Rocks"}
  Then %{I should see "by Matz"}
  Then %{I should see "Rails Rocks too!"}
  Then %{I should see "by DHH"}
end

Run “cucumber features/”, watch it fail, then update your view to make it pass:

# app/views/books/index.haml

.books
  - @books.each do |book|
    .book
      .name
        = book.name
      .author
        .name
          = "by #{book.author.name}"

Now it should pass.

Refactor

During this section, we’ll learn about Dupe model definitions (similar to Factory Girl’s “factory” definitions).

Already, our step definitions are starting to look slightly unwieldy. Just imagine if the client asked us to display genres, publishers, descriptions, etc. Our “Given the library has lots of books” step definition would be clogged with resource creations.

But don’t worry, we’ve barely scratched the surface of what’s possible with Dupe. Like Factory Girl, Dupe comes with a host of tools that make it possible for you to quickly create resources for the purposes of testing.

We can start by defining exactly what a book should consist of. Create a new file features/dupe/definitions/books.rb:

# features/dupe/definitions/books.rb

Dupe.define :book do |book|
  book.name
  book.author
end

Basically, all we’ve said is that any book object should have “name” and “author” attributes. Now a “Dupe.create :book” will create a Duped book object with “name” and “author” attributes. The easiest way to see what I’m talking about is to fire up irb and try it out:

irb# require 'dupe'
  ==> true

irb# Dupe.define :book do |book|
 --#   book.name
 --#   book.author
 --# end

irb# Dupe.create :book
  ==> <#Duped::Book author=nil id=1 name=nil>

irb# Dupe.create :book
  ==> <#Duped::Book author=nil id=2 name=nil>

Lets update our step definitions and see what happens:

Given /^the library has lots of books$/ do
  Dupe.stub 2, :books
end

Then /^I should see all of the books in the library$/ do
  Dupe.find(:books).each do |book|
    Then %{I should see "#{book.name}"}
    Then %{I should see "by #{book.author.name}"}
  end
end

Here I’ve introduced two new methods: Dupe.stub, and Dupe.find. Dupe.stub allows us to quickly generate an arbitrary number of resource (in this case two). It also supports a third argument of options, including a record template. Check out the API docs for more info (http://moonmaster9000.github.com/dupe/api/).

Dupe.find allows us to find resources we’ve created. Later we’ll see how to pass a proc to this method to filter the result set. In this case, Dupe.find(:books) will return an array of all books we’ve created.

If you run “cucumber features/”, it will fail. Why? “Dupe.stub 2, :books” created two book resources based on our book definition. However, our definition didn’t specify any default values for the “name” and “author” attributes, so they simply got nil values. Obviously, we need actual data. Let’s update our definition:

# features/dupe/definitions/books.rb

Dupe.define :book do |book|
  book.name 'default name'
  book.author do 
    Dupe.create :author
  end
end

Dupe.define :author do |author|
  author.name 'default name'
end

Now we’re saying that a book should have both “name” and “author” attributes, that the “name” attribute should default to ‘default name’, and that the “author” attribute should default to a newly created “author” resource. We’ve also specified that an :author resource should have a “name” attribute defaulted to ‘default name’.

Again, lets drop down to irb to get a better picture of what this will give us:

irb# require 'dupe'
  ==> true

irb# Dupe.define :book do |book|
 --#   book.name 'default name'
 --#   book.author do 
 --#     Dupe.create :author
 --#   end
 --# end

irb# Dupe.define :author do |author|
 --#   author.name 'default name'
 --# end

irb# Dupe.create :book
  ==> <#Duped::Book author=<#Duped::Author name="default name" id=1> name="default name" id=1>

irb# Dupe.create :book
  ==> <#Duped::Book author=<#Duped::Author name="default name" id=2> name="default name" id=2>

irb# Dupe.create :book, :name => 'Rooby'
  ==> <#Duped::Book author=<#Duped::Author name="default name" id=3> name="Rooby" id=3>

irb# Dupe.create :book, :name => 'Rails', :author => (Dupe.create :author, :name => 'DHH')
  ==> <#Duped::Book author=<#Duped::Author name="DHH" id=4> name="Rails" id=4>

You can see that each time we created a book, the book got both a name and an author. Dupe gave these attributes their appropriate default values when we didn’t manually specify what the “name” or “author” attribute should be.

Now let’s run our cucumber feature again (“cucumber features/”).

They pass! But wait! What did that view actually contain? It basically would have looked like:

  default name, by default name
  default name, by default name

That’s a pretty good indication that we haven’t created very good test data, especially we could change the view to this and still have the feature passing:

# app/views/books/index.haml

.books
  - @books.each do |book|
    .book
      .name
        = book.name
      .author
        .name
          = "by #{book.name}"

Notice that I’ve changed the display to set the author name to the book name. Since we created such poor test data, our feature still passes.

So, how do we rectify this situation? Lets use the “uniquify” method:

Dupe.define :book do |book|
  book.uniquify :name
  book.author do 
    Dupe.create :author
  end
end

Dupe.define :author do |author|
  author.uniquify :name
end

Notice the lines “book.uniquify :name” and “author.uniquify :name”. Basically, Dupe will attempt to default the book name and author name on a newly created resource to a unique value. (I’ll leave it to you to drop down to irb and test that out).

Now run cucumber features (I’m assuming you still have the improper books/index.haml view). They fail as expected! No we’ve got better test data. Lets go back and fix our view:

# app/views/books/index.haml

.books
  - @books.each do |book|
    .book
      .name
        = book.name
      .author
        .name
          = "by #{book.author.name}"

Now run “cucumber features/” and watch them pass.

Feature 2: Viewing all the books by a particular author.

During this feature, we’ll learn more about “Dupe.stub” and cyclically referential data.

Lets assume our client would also like a page for viewing all the books by a particular author. We’ll start with a new scenario:

# features/authors.feature

Feature: Viewing content related to authors

  Scenario: the books written by a particular author page
    Given an author with many books
    When I view the books page for that author
    Then I should see the name of that author
    And I should see all of the books written by that author

Next, let’s define the step definitions for this:

Given /^an author with many books$/ do
  @author = Dupe.create :author
  @author.books = Dupe.stub 10, :books, :like => {:author => @author}
end

When /^I view the books page for that author$/ do
  When %{I go to the books page for the author with id "#{@author.id}"}
end

Then /^I should see the name of that author$/ do
  Then %{I should see "#{@author.name}"}
end

Then /^I should see all of the books written by that author$/ do
  @books.each do |book|
    Then %{I should see "#{book.name}"}
  end
end

Notice that I’ve passed that third argument to stub I mentioned a while ago. For a detailed explanation, check the api docs, but suffice to say, I’m basically asking Dupe to give me 10 books, each with an author attribute with a value equal to @author.

This means that you can model fully referential objects with Dupe just like you would expect to find in a database. In this case, we’re modeling “book has_one author, author has_many books”. So how does Dupe turn a cyclical structure like our @author (our @author has many books, each of which have an :author attribute that points back to @author) into a flat, non-referential structure like XML? Though ActiveSupport’s to_xml method out of the box doesn’t support this feature, inside of Dupe is a special hash pruning algorithm that removes cyclical edges from the record before converting it into XML, resulting in XML essentially the same as what you would get with ActiveRecord’s to_xml method.

Next, we’ll need to setup some paths, routes, and controllers. Since there’s nothing new in really any of this, I’ll breeze through it:

# features/support/paths.rb

  when /the books page for the author with id "(\d+)"/
    books_author_path $1
# config/routes.rb

ActionController::Routing::Routes.draw do |map|
  map.resources :books
  map.resources :authors, :member => "books"
end
# app/controllers/authors_controller.rb

class AuthorsController < ApplicationController
  
  def books
    @author = Author.find params[:id]
  end
  
end
# app/views/authors/books.haml

.author
  .name
    = @author.name
  .books
    - @author.books.each do |book|
      .book
        .name
          = book.name
# app/models/author.rb

class Author < ActiveResource::Base
  self.site = ''
end

And that’s it! It’s as simple as that.

Feature 3: Searching books by author

During this feature, we’ll learn about Dupe “intercept mocking”.

Let’s suppose our client wants the ability to search authors:

# features/search_authors.feature

Feature: finding authors
  
  Scenario: searching for a particular author
    Given the site has many authors
    When I type some text into the author search form
    And I submit the form
    Then I should see any authors whose name at least partially matches my search text

Next, lets fill out the step definitions:

# features/step_definitions/search_author_steps.rb

Given /^the site has many authors$/ do
  Dupe.create(
    :authors, 
    [
      {:name => 'Famous Rubyist'}, 
      {:name => 'Infamous Rubyist'}, 
      {:name => 'Weird Haskeller'}
    ]
  )
end

When /^I type some text into the author search form$/ do
  When %{I go to the author search page}
  When %{I fill in "Search:" with "Ruby"}
end

When /^I submit the form$/ do
  When %{I press "Search"}
end

Then /^I should see any authors whose name at least partially matches my search text$/ do
  Then %{I should see "Famous Rubyist"}
  Then %{I should see "Infamous Rubyist"}
  Then %{I should not see "Weird Haskeller"}
end

Notice that you can use the create method to create several resources in one call by passing an array of hashes to it.

Next, lets create the pertinent paths, routes, controller actions, and views:

# features/support/paths.rb

when /the author search page/
  search_authors_path
# config/routes.rb  

ActionController::Routing::Routes.draw do |map|
  map.resources :books
  map.resources :authors, :member => 'books', :collection => 'search'
end
# app/controllers/authors_controller.rb

def search
  if params[:q]
    @authors = Author.find :all, :params => {:q => params[:q]}, :from => :search
  end
end  
# app/views/authors/search.haml

= form_tag :method => "get" do 
  = label_tag :q, "Search:" 
  = text_field_tag :q 
  = submit_tag "Search"
  
- if @authors
  .authors
    - @authors.each do |author|
      .author
        .name
          = author.name

Notice that we’ve used the :from option in our Author.find call in the search action. Why? :from is your architectural friend. Had we simply left the call as “Author.find :all, :params => {:q => params[:q]}”, we would have forced the authors index action on our backend to optionally filter results using a query string. And perhaps if we stopped there, we would have been fine with that. But it’s likely that in a real app, you’ll end up needing all kinds of services for filtering out results. Piling all of those services into a single index action on the backend will create one giant, fat, messy action. Using :from in this case, ActiveResource will instead send a request like “/authors/search.xml?q=Some+Search+Text”, allowing us to easily route that to a “search” action in our backend authors controller.

If you run this cucumber feature, you’ll get the following error:

  $ cucumber features/search_authors.feature 
  
  Feature: finding authors

    Scenario: searching for a particular author                                          
      Given the site has many authors                                                    
      When I type some text into the author search form                                  
      And I submit the form                                                             
        No mocked service response found for '/authors/search.xml?q=Ruby' (Dupe::Network::RequestNotFoundError)
        ./app/controllers/authors_controller.rb:10:in `search'
        (eval):2:in `click_button'
        ./features/step_definitions/webrat_steps.rb:15:in `/^I press "([^\"]*)"$/'
        features/search_authors.feature:6:in `And I submit the form'
      Then I should see any authors whose name at least partially matches my search text


  Failing Scenarios:
  cucumber features/search_authors.feature:3 # Scenario: searching for a particular author
  

Why did it fail? Basically, Dupe is telling us that it doesn’t know how to interpret the request “/authors/search.xml?q=Ruby”, which was caused by the line in our AuthorsController search action “@authors = Author.find(:all, :params => {:q => params[:q]}, :from => :search)”.

Dupe, by default, can only anticipate simple find(:all) and find() requests. To handle this service mock, we can create a custom intercept mock in features/dupe/custom_mocks/authors.rb:

# features/dupe/custom_mocks/authors.rb

Get %r{/authors/search\.xml\?q=([^&]+)$} do |search_text|
  Dupe.find(:authors) {|a| a.name.downcase.include? search_text.downcase}
end

Not unlike a cucumber step definition, this will create a regular expression matcher for the service url, which we then translate into a Dupe resource query. Notice that we pass a block to our Dupe.find method call. Checkout the API docs for more info, but essentially, this translates as “Find all the authors whose name includes the string X”.

Now run your feature again and watch it pass!

What About the Backend?

Let’s assume we’re done cuking all the features for our frontend. Obviously, our app is only half complete. We can’t deploy it yet, because we don’t yet have any real services.

Since we’ve used ActiveResource to create a service-oriented app, we can write a backend in any language/framework we desire.

But what about the format of the xml our backend returns? Your backend developers can turn on Dupe request logging (set “Dupe.debug = true” in your features/dupe/definitions/definitions.rb) to see the format of example requests. With Dupe logging on, at the end of each scenario, Dupe will spit out both the request urls and the response XML that it mocked during the course of that scenario.

Conclusion

Hopefully, this has given you more than enough information to get you started on cuking your own service-oriented application.

Dupe has many more features to offer, checkout out the README.rdoc at http://github.com/moonmaster9000/dupe

About

This is an example rails app that uses Dupe for cuking ActiveResource.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published