In-Process Testing of Rack-Based REST APIs

Let’s start with the first approach, where both Cucumber and your application run in the same Ruby process. We’ll be dealing with a simple REST API for storing and retrieving fruit. It’s probably not the most useful system in the world but should allow us to illustrate the fundamentals of REST and Cucumber.

We’re going to build our web service from scratch. Create a new project with a features directory in it, and create this file:

 Feature​: Fruit list
  In order to make a great smoothie
  I need some fruit.
 
 Scenario​: List fruit
 Given the system knows about the following fruit​:
  | name | color |
  | banana | yellow |
  | strawberry | red |
  When the client requests GET /fruits
 Then the response should be JSON​:
 """
  [
  {"name": "banana", "color": "yellow"},
  {"name": "strawberry", "color": "red"}
  ]
  """

In the rest of this section we are going to implement this feature. We’ll see how Cucumber can issue HTTP requests to our application.

Standing Up a Skeleton App—and Storing Some Fruit

When we run our List fruit feature with cucumber, we’ll see the usual snippets:

 Given(​/^the system knows about the following fruit:$/​) ​do​ |table|
 # table is a Cucumber::Core::Ast::DataTable
  pending ​# Write code here that turns the phrase above into concrete actions
 end
 
 When(​/^the client requests GET /fruits$/​) ​do
  pending ​# Write code here that turns the phrase above into concrete actions
 end
 
 Then(​/^the response should be JSON:$/​) ​do​ |string|
  pending ​# Write code here that turns the phrase above into concrete actions
 end

Before we go ahead and paste the previous snippets into a Ruby file and fill in the blanks, let’s look at each one. The first snippet is a domain-specific step definition—it’s about our fruit system. The last two seem like more general-purpose step definitions for HTTP REST operations. As discussed in Organizing the Code, we’ll put each of them in a file that describes the domain concept they are related to. This makes it easier in the future to find our step definitions. Paste the first one in a features/step_definitions/fruit_steps.rb file and the last two in a features/step_definitions/rest_steps.rb file.

Let’s start by making the first step pass. To keep things simple, we are not going to use a database to store our fruit—we can store them in memory. This means that in our Given step, we’ll just store our fruit in a (class) variable:

 Given(​/^the system knows about the following fruit:$/​) ​do​ |fruits|
  FruitApp.data = fruits.hashes
 end

Of course, this is going to cause the Given step to go from pending to failing, because we don’t yet have a FruitApp class:

 Feature: Fruit list
  In order to make a great smoothie
  I need some fruit.
 
  Scenario: List fruit
  Given the system knows about the following fruit:
  | name | color |
  | banana | yellow |
  | strawberry | red |
  uninitialized constant FruitApp (NameError)
  ./features/step_definitions/fruit_steps.rb:2
  features/fruit_list.feature:6
  When the client requests GET /fruits
  Then the response should be JSON:
  """
  [
  {"name": "banana", "color": "yellow"},
  {"name": "strawberry", "color": "red"}
  ]
  """
 
 Failing Scenarios:
 cucumber features/fruit_list.feature:5
 
 1 scenario (1 failed)
 3 steps (1 failed, 2 skipped)
 0m0.017s

We’re going to implement our little REST web service with the Sinatra web framework because it is so simple. If you were to use a different Rack-based web framework such as Rails, the Cucumber features and Ruby files under the features directory would be exactly the same, but there would be much more application code for us to include in the book. Let’s start by installing all the gems we’re going to need. Create a Gemfile in the root of the project:

 source 'https://rubygems.org'
 
 gem 'sinatra', '2.0.0.beta.2'
 gem 'json', '2.0.2'
 
 group :test do
  gem 'cucumber', '3.0.0.pre.1'
  gem 'rspec', '3.5.0'
  gem 'rack-test', '0.6.3'
 end

We need Sinatra and JSON to run our web service; we need Cucumber to run the tests, RSpec to make assertions in the step definitions, and Rack-Test to automate the web service from our step definitions. Now tell Bundler to install the gems:

 $ ​​bundle

With that out of the way, we can get on with building our web server. Create a file named fruit_app.rb in the directory above the features directory. Add the following code to that file:

 require ​'sinatra'
 
 class​ FruitApp < Sinatra::Base
  set ​:data​, ​''
 end

We’ve created a class FruitApp that inherits from Sinatra’s base class; this will become our web server. We need to tell Cucumber to load that file. We’ll create a features/support/env.rb file to do that:

 require File.join(File.dirname(​__FILE__​), ​'..'​, ​'..'​, ​'fruit_app'​)

The first line loads the fruit_app.rb file from the root directory of our project—two directories up from this file. Try to run cucumber now. It should make our first step pass. It’s time to get into the REST business!

Poking Our Application with Rack-Test

Since we are running Cucumber in the same Ruby process as the application itself, we have the luxury of being able to talk to it directly, without going through HTTP or a web server. This is possible thanks to a nifty little Ruby gem called rack-test. Rack-Test is a library that is designed to be used in tests and that acts as a façade[55] to your web application, very much in the same way as a regular web server does. Rack-Test has methods for mimicking HTTP requests from code. As shown in the figure, we’ll be talking to our application through Rack-Test, which exposes Rack’s API for talking to our application just like an HTTP server would.

images/rest-in-process.png

Let’s put Rack-Test to use in the /^the client requests GET /fruits$/ step definition in the rest_steps.rb file, and while we’re at it, let’s turn the request path into a variable with a capture group—this makes our step definition usable for other paths too.

 When(​/^the client requests GET (.*)$/​) ​do​ |path|
  get(path)
 end

We’re calling Rack-Test’s get method here, but how exactly does Cucumber know about this method? Well, it doesn’t actually, so we have to wire it up. The get method is defined in a module called Rack::Test::Methods, which gets loaded when we require ’rack/test’. To wire it all up, we need to modify our env.rb file a little more:

 require File.join(File.dirname(​__FILE__​), ​'..'​, ​'..'​, ​'fruit_app'​)
 require ​'rack/test'
 
 module​ AppHelper
 # Rack-Test expects the app method to return a Rack application
 def​ app
  FruitApp
 end
 end
 
 World(Rack::Test::Methods, AppHelper)

Let’s take a look at this code. The last line with the World call registers two Ruby modules within Cucumber: first Rack::Test::Methods, which defines the HTTP simulation methods like get, and then our own helper module that we define right above. Both these modules will be mixed into Cucumber’s World, as we’ve explained in Chapter 8, Support Code. The reason we need our own little AppHelper is that Rack-Test needs to know which Rack application it should talk to, and it expects an app method to be defined in the same object that mixes in the other Rack-Test methods. This method must return the class of our web app.

Now that we have Rack-Test properly wired up, let’s see what Cucumber says:

 Feature: Fruit list
  In order to make a great smoothie
  I need some fruit.
 
  Scenario: List fruit
  Given the system knows about the following fruit:
  | name | color |
  | banana | yellow |
  | strawberry | red |
  When the client requests GET /fruits
  Then the response should be JSON:
  """
  [
  {"name": "banana", "color": "yellow"},
  {"name": "strawberry", "color": "red"}
  ]
  """
  TODO (Cucumber::Pending)
  ./features/step_definitions/rest_steps.rb:6
  features/fruit_list.feature:11
 
 1 scenario (1 pending)
 3 steps (1 pending, 2 passed)
 0m0.047s

Our second step is passing, so it looks like our GET request worked well. But wait a minute! How is that possible? We haven’t implemented anything in our fruit_app.rb yet. Why is the step passing? We’ll find out in the next section.

Comparing JSON

Although it might seem a little surprising that our second step is passing at this point, let’s not worry about why. Instead, let’s continue to make our final step pass, and everything will become clear. In this step, we want to compare the response from the GET request with our expected JSON. Rack-Test always lets us access the response of the last request by invoking the last_response method, which returns an object that holds various information about the response. We’re interested in the body, which is where we expect our web app to write the JSON:

 Then(​/^the response should be JSON:$/​) ​do​ |json|
  expect(last_response.body).to eq json
 end

Let’s run Cucumber again. We’re getting an error message this time, and it’s a quite long one. Let’s take some time to study it.

 Feature: Fruit list
  In order to make a great smoothie
  I need some fruit.
 
  Scenario: List fruit
  Given the system knows about the following fruit:
  | name | color |
  | banana | yellow |
  | strawberry | red |
  When the client requests GET /fruits
  Then the response should be JSON:
  """
  [
  {"name": "banana", "color": "yellow"},
  {"name": "strawberry", "color": "red"}
  ]
  """
 
  expected: "[ {"name": "banana", "color": "yellow"},
  {"name": "strawberry", "color": "red"} ]"
  got: "<!DOCTYPE html> <html> <head> <style type="text/css">
  body { text-align:center;font-family...#x2F;fruits&#x27; do
  &quot;Hello World&quot; end end </pre> </div> </body> </html> "
 
  (compared using ==)
 
  Diff:
  @@ -1,5 +1,25 @@
  -[
  - {"name": "banana", "color": "yellow"},
  - {"name": "strawberry", "color": "red"}
  -]
  +<!DOCTYPE html>
  +<html>
  +<head>
  + <style type="text/css">
  + body { text-align:center;font-family:helvetica,arial;font-size:22px;
  + color:#888;margin:20px}
  + #c {margin:0 auto;width:500px;text-align:left}
  + </style>
  +</head>
  +<body>
  + <h2>Sinatra doesn’t know this ditty.</h2>
  + <img src=’http://example.org/__sinatra__/404.png’>
  + <div id="c">
  + Try this:
  + <pre># in fruit_app.rb
  +class FruitApp
  + get &#x27;&#x2F;fruits&#x27; do
  + &quot;Hello World&quot;
  + end
  +end
  +</pre>
  + </div>
  +</body>
  +</html>
  (RSpec::Expectations::ExpectationNotMetError)
  ./features/step_definitions/rest_steps.rb:6
  features/fruit_list.feature:11
 
 Failing Scenarios:
 cucumber features/fruit_list.feature:5
 
 1 scenario (1 failed)
 3 steps (1 failed, 2 passed)
 0m0.050s

Cucumber is telling us that it expected to get back the JSON we specified in our feature, but instead it got some HTML back. What is going on here?

What we’re seeing here is actually Sinatra’s 404 Page Not Found page, which it shows for any HTTP request it doesn’t know how to serve. This begins to explain why the second step was passing. Cucumber and Rack-Test were able to make a GET request, but since we haven’t yet defined a route for /fruits, the response of that request was a 404 Page Not Found. We actually see this in the HTML in the error message if we look carefully.

What may surprise you is that Rack-Test didn’t raise an exception since it got a 404 Page Not Found response, instead of the 200 Successful we’re expecting. No HTTP error responses in the 4xx and 5xx response code ranges will cause Ruby errors to be raised by Rack-Test. This is a deliberate design decision by the creator of Rack-Test, and it’s benefit is that you can easily write tests for edge cases where an error is the expected behavior, and happy-path scenarios like this one.

Joe asks:
Joe asks:
I Thought Scenarios Should Avoid Technical Terms like JSON

It’s important to make a scenario readable by a stakeholder. However, for a REST interface, the stakeholder is going to be another programmer writing a client for the REST interface. In such cases, it’s fine to expose technical details. For a human user interface, such as an HTML web application, we should use nontechnical terms. There’s more about this in Chapter 15, Using Capybara to Test Ajax Web Applications.

Now that we understand what is going on, let’s implement the /fruits route in our web service. Back in your fruit_app.rb, edit it so it looks like the following:

 require ​'sinatra'
 require ​'json'
 class​ FruitApp < Sinatra::Base
  set ​:data​, ​''
  get ​'/fruits'​ ​do
  content_type ​:json
  FruitApp.data.to_json
 end
 end

Running Cucumber again brings us just a few yards from the goal.

 Feature: Fruit list
  In order to make a great smoothie
  I need some fruit.
 
  Scenario: List fruit
  Given the system knows about the following fruit:
  | name | color |
  | banana | yellow |
  | strawberry | red |
  When the client requests GET /fruits
  Then the response should be JSON:
  """
  [
  {"name": "banana", "color": "yellow"},
  {"name": "strawberry", "color": "red"}
  ]
  """
 
  expected: "[ {"name": "banana", "color": "yellow"},
  {"name": "strawberry", "color": "red"} ]"
  got:
  "[{"name":"banana","color":"yellow"},{"name":"strawberry"
  ,"color":"red"}]"
 
  (compared using ==)
 
  Diff:
  @@ -1,5 +1,2 @@
  -[
  - {"name": "banana", "color": "yellow"},
  - {"name": "strawberry", "color": "red"}
  -]
  +[{"name":"banana","color":"yellow"},{"name":"strawberry","color":"red"}]
  (RSpec::Expectations::ExpectationNotMetError)
  ./features/step_definitions/rest_steps.rb:6
  features/fruit_list.feature:11
 
 Failing Scenarios:
 cucumber features/fruit_list.feature:5
 
 1 scenario (1 failed)
 3 steps (1 failed, 2 passed)
 0m0.050s

Now the scenario is failing simply because the actual JSON has no spaces and newlines, while the JSON from our scenario does. Of course, we could make the scenario pass by removing whitespace and newlines from the scenario too, but that would make it very hard to read. Remember, our features are supposed to serve as documentation too, not only automated tests. In any case, it’s the data that’s significant in this scenario, not the whitespace. What we really want to do is to parse the two JSON strings into an object structure and compare those. This is just a small change:

 Then(​/^the response should be JSON:$/​) ​do​ |json|
  expect(JSON.parse(last_response.body)).to eq JSON.parse(json)
 end
Matt says:
Matt says:
Comparing Data Is Less Brittle Than Comparing Strings

The lesson we learn in this chapter about parsing both strings into JSON documents before comparing them is a useful one to remember for other situations. When you compare two strings in a test, you’re often leaving yourself open to brittle failures that aren’t telling you anything useful. Parsing the string from the Gherkin feature into more meaningful data can often mean you have more robust tests that fail only when they should.

Finally, our first REST feature is passing!

 Feature: Fruit list
  In order to make a great smoothie
  I need some fruit.
 
  Scenario: List fruit
  Given the system knows about the following fruit:
  | name | color |
  | banana | yellow |
  | strawberry | red |
  When the client requests GET /fruits
  Then the response should be JSON:
  """
  [
  {"name": "banana", "color": "yellow"},
  {"name": "strawberry", "color": "red"}
  ]
  """
 
 1 scenario (1 passed)
 3 steps (3 passed)
 0m0.040s

Using Rack-Test makes it easy to test a REST interface with Cucumber. If we were to write another scenario—perhaps testing how the application deals with duplicate fruit—we would have to make sure that any data stored in the previous scenario is cleaned away before the next one starts. With our simple application, we could easily do this in a Before hook, which we described in detail in Using Hooks. Since Cucumber is running in the same process as the application, cleaning away previous data would be simple—just call FruitApp.data = [] in a Before block. If our application was using a proper database, we would delete all of the rows/records that were created in the scenario instead, as described in Chapter 10, Databases.

Testing in-process is great when you have the option available to you, but there are many more web services written in other languages than Ruby that you’ll still want to be able to test. In the next section, we’ll decouple ourselves from the Sinatra application and run it out of process so you can see how to test a stand-alone web service.

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

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