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.
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.
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]
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.
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.
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.
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.
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!
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.
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