Implementing a Simple Search Without Ajax

In Chapter 14, Bootstrapping Rails, we dabbled a little bit in Capybara’s API. We used the visit(path) method to go to a user’s page, and we verified that messages were displayed on the screen using Capybara’s have_content matcher. Let’s add a feature that will allow users to search for messages. This will expose us to more of the goodness Capybara has to offer.

Start with the code we left off with at the end of the previous chapter and add a new search feature to features/search.feature:

 Feature​: Search
 Scenario​: Find messages by content
 Given a User has posted the following messages​:
  | content |
  | I am making dinner |
  | I just woke up |
  | I am going to work |
  When I search for ​"I am"
 Then the results should be​:
  | content |
  | I am making dinner |
  | I am going to work |

If you skipped the previous chapter, you can download the code to jump right in at this point.

We’ve used a simple example where two out of the three messages that the user has posted will match the search. Notice that we haven’t referred specifically to any button clicks or other user interface details in this scenario. Instead, we’ve been careful to use the language of our own domain: words like posted, search, and results.

When you run this feature, you will see that all the steps are undefined.

Preparing Something to Search For

Our search isn’t going to find anything unless some messages already exist, so before we get to do anything with Capybara, we have to add some messages to the database. Knowing how to set up the database before each scenario is essential to testing web applications, so we’ll show you how here. Add the following step definition to features/step_definitions/user_steps.rb:

 Given(​/^a User has posted the following messages:$/​) ​do​ |messages|
  user = FactoryGirl.create(​:user​)
  messages_attributes = messages.hashes.map ​do​ |message_attrs|
  message_attrs.merge({​:user​ => user})
 end
  Message.create!(messages_attributes)
 end

This step definition is a little complicated. Let’s examine it, starting at the bottom. ActiveRecord’s create! method is invoked with an Array of Hash objects, which lets us create several Message records in one fell swoop. Cucumber’s Table[61] class (our messages object) conveniently provides the hashes method to convert it to just that—an Array of Hash. To link those messages to a user, we modify the Array by adding a :user entry in each Hash. The first line of the step definition creates that User. The messages_attributes variable will contain something like this:

 [
  {​"content"​=>​"I am making dinner"​, ​:user​=>​#<User id: 1>},
  {​"content"​=>​"I just woke up"​, ​:user​=>​#<User id: 1>},
  {​"content"​=>​"I am going to work"​, ​:user​=>​#<User id: 1>}
 ]

Navigating, Filling in Fields, and Clicking Buttons

With the first Given out of the way, let’s get to work with our first custom Capybara step definition.

We decided to write our steps in a declarative style. When I search for "I am" is a great example of a declarative step. What widgets the user interacts with will be nicely hidden inside a single step definition. This makes our Gherkin much simpler and expressive, but it also makes our scenarios much easier to maintain. In the future, we might have many scenarios for search, and if we decide to change the user interface, we only have to update our single step definition instead of having to update a whole lot of scenarios.

To do this, we have to break the declarative step into concrete user interaction steps that Capybara can deal with:

  1. Navigate to the search page.
  2. Fill in the search criteria in the search field.
  3. Click the Search button.

All of these actions are easy to perform with Capybara. We’ll implement this in a new file called features/step_definitions/search_steps.rb:

 When(​/^I search for "([^"]*)"$/​) ​do​ |query|
  visit(​'/search'​)
  fill_in(​'query'​, ​:with​ => query)
  click_button(​'Search'​)
 end

We are making several assumptions here that can be used to guide the implementation of the search. First, we expect the web app to respond to the /search path. Second, we expect an input field named query to be on the rendered page. Third, there should be a Search button we can click. None of this stuff exists yet, so we have set ourselves up for failure—the good kind of failure that tells us what to do next:

Fixing the Controller Code

Let’s run Cucumber and examine the output:

 Feature: Search
 
  Scenario: Find messages by content
  Given a User has posted the following messages:
  | content |
  | I am making dinner |
  | I just woke up |
  | I am going to work |
  When I search for "I am"
  No route matches [GET] "/search" (ActionController::RoutingError)
  ./features/step_definitions/search_steps.rb:2
  features/search.feature:8
  Then the results should be:
  | content |
  | I am making dinner |
  | I am going to work |
  Undefined step: "the results should be:" (Cucumber::Undefined)
  features/search.feature:9
 
 Failing Scenarios:
 cucumber features/search.feature:2
 
 1 scenario (1 failed)
 3 steps (1 failed, 1 undefined, 1 passed)
 0m0.137s
 
 You can implement step definitions for undefined steps with these snippets:
 
 Then(/^the results should be:$/) do |table|
  # table is a Cucumber::MultilineArgument::DataTable
  pending # Write code here that turns the phrase above into concrete actions
 end

Our second step is failing, but this doesn’t mean there is something wrong with our step definition. It’s our application that is wrong. In Chapter 14, Bootstrapping Rails, we described the overall flow you go through when you’re making a step pass in a Rails application, but let’s repeat it quickly here.

The exception from Rails tells us that we need to add a route:

 Rails.application.routes.draw ​do
  resources ​:users
  resource ​:search​, ​:only​ => ​:show​, ​:controller​ => ​:search
 end

Now, Cucumber will complain about a missing controller, so we’ll add that:

 class​ SearchController < ApplicationController
 def​ show
 end
 end

We have no view, so we’ll add that as well. Let’s just enter some hard-coded text for now:

 Search page

With the route, controller, and dummy view, we are able to get a little further.

Let’s Give Capybara Something to Work On

Cucumber now fails with the following output:

 Feature: Search
 
  Scenario: Find messages by content
  Given a User has posted the following messages:
  | content |
  | I am making dinner |
  | I just woke up |
  | I am going to work |
  When I search for "I am"
  Unable to find field "query" (Capybara::ElementNotFound)
  ./features/step_definitions/search_steps.rb:3
  features/search.feature:8
  Then the results should be:
  | content |
  | I am making dinner |
  | I am going to work |
  Undefined step: "the results should be:" (Cucumber::Undefined)
  features/search.feature:9
 
 Failing Scenarios:
 cucumber features/search.feature:2
 
 1 scenario (1 failed)
 3 steps (1 failed, 1 undefined, 1 passed)
 0m0.402s
 
 You can implement step definitions for undefined steps with these snippets:
 
 Then(/^the results should be:$/) do |table|
  # table is a Cucumber::MultilineArgument::DataTable
  pending # Write code here that turns the phrase above into concrete actions
 end

Capybara is telling us that it can’t find the search field where we’re entering the search criteria. As you see from the error message, Capybara says it knows how to locate a text field based on id, name, or label. We don’t need a label for our input field, so all we need to add is a field with a name="query" attribute.

We also need a Search button, so we’ll put everything in a form that issues a GET request to the same page.[62] Our modified view now looks like this:

 <form id=​"search"​ method=​"get"​ action=​"/search"​>
  <fieldset>
  <input type=​"text"​ id=​"query"​ name=​"query"​ />
  <input type=​"submit"​ value=​"Search"​ />
  </fieldset>
 </form>

Now Cucumber passes the search step, and the last verification step remains undefined:

 Feature: Search
 
  Scenario: Find messages by content
  Given a User has posted the following messages:
  | content |
  | I am making dinner |
  | I just woke up |
  | I am going to work |
  When I search for "I am"
  Then the results should be:
  | content |
  | I am making dinner |
  | I am going to work |
  Undefined step: "the results should be:" (Cucumber::Undefined)
  features/search.feature:9
 
 1 scenario (1 undefined)
 3 steps (1 undefined, 2 passed)
 0m0.423s
 
 You can implement step definitions for undefined steps with these snippets:
 
 Then(/^the results should be:$/) do |table|
  # table is a Cucumber::MultilineArgument::DataTable
  pending # Write code here that turns the phrase above into concrete actions
 end

Verifying Page Content

Our last step definition will require some extra thought. We are going to use the value of the query URL parameter to run a database query and display the results in a list (an <ol> element with <li> elements for each result).

To compare the contents of the list on the search page with the expected values in the Gherkin table, we need to extract the interesting bits from the markup. This is easy once you have done it a few times, but at first it can be a little daunting because it involves Capybara’s Capybara::Node::Finders API and the use of CSS selectors.

This leaves us in a little bit of a tricky situation. We’re about to write nontrivial step definition code to test nontrivial application code. Where do we start? A useful technique in situations like this is to create a sketch of the solution. We are going to hard-code the markup we expect to see in our view, without any application logic at all. This will serve as scaffolding to support our test. Let’s just modify our app/views/search/show.html.erb view so it contains exactly what we want:

 <form id=​"search"​ method=​"get"​ action=​"/search"​>
 
  <fieldset>
  <input type=​"text"​ id=​"query"​ name=​"query"​ />
  <input type=​"submit"​ value=​"Search"​ />
  </fieldset>
 
  <ol class=​"results"​>
  <li><a href=​"/users/2/messages/20"​>I am making dinner</a></li>
  <li><a href=​"/users/2/messages/21"​>SHOULD NOT BE HERE</a></li>
  <li><a href=​"/users/2/messages/22"​>I am going to work</a></li>
  </ol>
 
 </form>

Actually, this isn’t exactly what we want. The view has the markup structure we want but not the correct data! (There is a row too many.) This is a very powerful technique. We are deliberately making our hard-coded view be slightly wrong. This allows us to verify that our step definition will detect the incorrect (hard-coded) result and display a proper error message. Then we can focus on writing the step definition code correctly without worrying that we have made a mistake in either the application logic or the step definition logic, not knowing where the mistake was.

Extracting Data from the Page

Let’s start by constructing an Array of Array to hold the expected search results from the page. (We’ll see in a minute how this can be used to compare against a Gherkin table):

 Then(​/^the results should be:$/​) ​do​ |expected_results|
  results = [[​'content'​]] + page.all(​'ol.results li'​).map ​do​ |li|
  [li.text]
 end
 
  puts results.join(​"​​ ​​"​)
  pending
 end

The page.all(’ol.results li’) call will return an Array of Capybara::Element objects, each one representing an <li> element that contains a single <a> element. We’re calling map on the Array to convert each Capybara::Element to another Array of Strings. Finally, we’re using Cucumber’s puts method to print it out the console so we can verify that our conversion is what we intended.

The [[’content’]] in the beginning is there to ensure our first row is the same as the one we’re sending in from Gherkin—having the same values in the top row is necessary in order to compare a Gherkin table with actual values. Running Cucumber prints the following:

 Feature: Search
 
  Scenario: Find messages by content
  Given a User has posted the following messages:
  | content |
  | I am making dinner |
  | I just woke up |
  | I am going to work |
  When I search for "I am"
  Then the results should be:
  content
  I am making dinner
  SHOULD NOT BE HERE
  I am going to work
  | content |
  | I am making dinner |
  | I am going to work |
  TODO (Cucumber::Pending)
  ./features/step_definitions/search_steps.rb:14
  features/search.feature:9
 
 1 scenario (1 pending)
 3 steps (1 pending, 2 passed)
 0m0.405s

That looks about right! We are successfully extracting data from our web page.

Using Table Diffs

Now that we are confident that we are properly extracting data from the page, let’s compare it to our Gherkin table that contains the expected results. The table object that gets passed to your step definition is an instance of Cucumber::Ast::Table, and one of the lesser known but extremely powerful methods on this class is the diff! method. It takes a single argument that it expects to be an Array of Array representing rows and columns. If all the values are equal, the step definition passes. If not, the step definition fails, and a diff is printed out. Let’s put it to use:

 Then(​/^the results should be:$/​) ​do​ |expected_results|
  results = [[​'content'​]] + page.all(​'ol.results li'​).map ​do​ |li|
  [li.text]
 end
 
  expected_results.diff!(results)
 end

Will Cucumber give us a proper error message?

 Feature: Search
 
  Scenario: Find messages by content
  Given a User has posted the following messages:
  | content |
  | I am making dinner |
  | I just woke up |
  | I am going to work |
  When I search for "I am"
  Then the results should be:
  | content |
  | I am making dinner |
  | I am going to work |
  Tables were not identical:
 
  | content |
  | I am making dinner |
  | (+) SHOULD NOT BE HERE |
  | I am going to work |
  (Cucumber::MultilineArgument::DataTable::Different)
  ./features/step_definitions/search_steps.rb:13
  features/search.feature:9
 
 Failing Scenarios:
 cucumber features/search.feature:2
 
 1 scenario (1 failed)
 3 steps (1 failed, 2 passed)
 0m0.447s

It looks like it does. If you have colors enabled in your command window, you will see the surplus row colored gray. If there had been missing rows, they would have been yellow. Likewise for columns. We have built up confidence that our somewhat complicated step definition will properly detect errors. Now let’s get rid of the hard-coded parts and implement the search properly. We’ll start by changing the view to loop over an array of Message and produce the same markup as our hard-coded markup:

 <form id=​"search"​ method=​"get"​ action=​"/search"​>
 
  <fieldset>
  <input type=​"text"​ id=​"query"​ name=​"query"​ />
  <input type=​"submit"​ value=​"Search"​ />
  </fieldset>
 
  <ol class=​"results"​>
 <%​ @messages.each ​do​ |message| ​%>
  <li>​<%=​ link_to(message.content, [message.user, message]) ​%>​</li>
 <%​ ​end​ ​%>
  </ol>
 
 </form>

If we run Cucumber again, we will get an error message from the view. This is because we haven’t assigned anything to the @messages variable in our controller.

In our controller we are just going to take the query parameter and ask the model class to do the search for us:

 class​ SearchController < ApplicationController
 def​ show
  @messages = Message.like(params[​:query​])
 end
 end

We could have implemented the search logic in the controller, but it is a wiser design decision to put that in the model. That allows us to unit test it in isolation later if we want. Here is what it looks like:

 class​ Message < ActiveRecord::Base
  belongs_to ​:user
 
 def​ self.like(content)
  content.nil? ? [] : where([​'content LIKE ?'​, ​"%​​#{​content​}​​%"​])
 end
 end

We’re making sure the like method returns an empty array in case the argument is nil, which it will be if the request doesn’t include a query parameter.

Now that the query logic is implemented, we will get an error message from Rails saying it can’t generate the link. This is because we haven’t defined a route for the messages yet. That’s easy to do:

 Rails.application.routes.draw ​do
  resources ​:users​ ​do
  resources ​:messages
 end
  resource ​:search​, ​:only​ => ​:show​, ​:controller​ => ​:search
 end

And finally Cucumber is happy:

 Feature: Search
 
  Scenario: Find messages by content
  Given a User has posted the following messages:
  | content |
  | I am making dinner |
  | I just woke up |
  | I am going to work |
  When I search for "I am"
  Then the results should be:
  | content |
  | I am making dinner |
  | I am going to work |
 
 1 scenario (1 passed)
 3 steps (3 passed)
 0m0.437s

Try This

Before we head on to learn about Ajax, here are some small exercises you can try, to learn more about Capybara and Cucumber’s table diffing:

  • Add an extra column in the results table.

  • Add a feature for posting messages through the UI so you can try the app—we haven’t run it yet!

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.221.179.220