Out-of-Process Testing of Any REST API

images/rest-out-of-process.png

In the previous section, we tested a REST API by running Cucumber in the same Ruby process as our application. In this section, we are going to explore how to test a REST API when Cucumber is running in a different process than the application. The figure illustrates how this fits together. Cucumber and the HTTP Client library run in one process, and the web application (the dotted boxes) runs in another. We’ll be using the same application that we created in the previous section, but you could just as well use an application written in a different programming language. This is thanks to a handy little library called HTTParty, which can issue HTTP requests to a web server.

We’ll start by changing our Gemfile. We’re going to add the HTTParty library and another library called ChildProcess, which we’ll use to start the web server process:

 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 'httparty', '0.14.0'
  gem 'childprocess', '0.5.9'
 end

We’ve also removed Rack-Test from the Gemfile since we’re no longer going to be using it to talk to the web server.

Run bundle to install the new gems.

We never actually fired up our fruit application, so let’s put in place a missing piece so we can do that.

We’re going to start the application via rackup, which is the Rack framework’s command-line tool for starting Rack-based applications. The rackup command-line tool will look for a config.ru file in the current directory. We won’t go into details about Rack in this book, so we’ll just provide a simple config.ru file so we can start the application. Put the following file in the root directory, next to your fruit_app.rb file:

 require File.dirname(​__FILE__​) + ​'/fruit_app'
 run FruitApp

Now we can start up our app. Let’s run it on port 9999:

 $ ​​rackup​​ ​​--port​​ ​​9999

If you point your web browser to http://localhost:9999/fruits, you should get a JSON response containing {}—we have no fruit in our system. We’re going out of process now, so let’s remove all of the Rack-Test code from the previous section. This boils down to deleting all the code in the features/support/env.rb file. Go ahead and clean out that file, and then let’s run the feature again and see where that leaves us:

 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.018s

Ouch! It looks like we broke something here. We get a uninitialized constant Object::FruitApp error in the first step. This is because when we cleared out our features/support/env.rb file, we also removed the first two lines that load our application code. Should we add them back? It might be tempting, but this is precisely what we are not going to do. Remember, we are now in a situation where Cucumber and our application are meant to run in two different processes. For all Cucumber cares, our application may be an application that isn’t even written in Ruby. We did the right thing by removing everything from features/support/env.rb—we don’t want Cucumber to load a single line of code from our web application!

This brings up an interesting question: how can we make the first step pass? Our app is using an in-memory data store (a simple class variable). We need some way to access this data store from Cucumber, which is going to be running in a different process. There are many ways to make this happen, but the essence of it is that we need to make our application’s data store accessible from a different process.

One option would be to expose a new method over our HTTP API that allowed a client (like our Cucumber tests) to add new fruit to the system with an HTTP POST. POSTing to a special URL to reset a database is not an uncommon approach. Just make sure that if you do this, the URL is disabled in your production environment. Another route would be to refactor our application to use a data store such as MySQL or MongoDB and use a database library to insert data. If we did that, our step definition could put fruit in the system by talking directly to the same database as our web application.

For the sake of simplicity, let’s use the second simplest data store after memory—a file. This takes only a couple of small modifications to fruit_app.rb. We’ll read the fruit from a JSON file and validate that it’s valid JSON while we’re at it:

 require ​'sinatra'
 require ​'json'
 
 class​ FruitApp < Sinatra::Base
  set ​:data​ ​do
  JSON.parse(File.read(​'fruits.json'​))
 end
 
  get ​'/fruits'​ ​do
  content_type ​:json
  FruitApp.data.to_json
 end
 end

The fruits.json file will be read every time a request is made and asks for FruitApp.data. Now it’s easy to do the right thing in the first step by modifying features/step_definitions/fruit_steps.rb to write to our new file database.

 Given(​/^the system knows about the following fruit:$/​) ​do​ |fruits|
  File.open(​'fruits.json'​, ​'w'​) ​do​ |io|
  io.write(fruits.hashes.to_json)
 end
 end

We’ve used another method on Cucumber::Core::Ast::DataTable, hashes, which returns the data in the table as an array of hashes: one hash per row in the table. The column headings in the first row of the table are used as keys in the hash.

Joe asks:
Joe asks:
What’s The Difference Between ChildProcess and ServiceManager? How Should I Decide Which One To Use?

In Chapter 9, Dealing with Message Queues and Asynchronous Components, we showed you how to use the Service Manager gem to start and stop an external process from your tests. Service Manager would also be a good fit for this situation because it ships with the functionality to poll an external service until it is up, something we’ve written by hand here.

ChildProcess is a more low-level library, which means it’s more flexible. That flexibility comes at the cost of needing more code to achieve common tasks, like the code to start a web server in this chapter. We wanted to show you how to use both libraries, but we’d suggest Service Manager is the better choice in most situations.

Now that you have made changes to your application, you need to restart it so that the changes are picked up. It becomes tedious to keep two shells open and switch back and forth to restart our server. Let’s tell Cucumber to start and stop the server instead. Let’s add some code back to our features/support/env.rb file:

 require ​'childprocess'
 require ​'timeout'
 require ​'httparty'
 server = ChildProcess.build(​"rackup"​, ​"--port"​, ​"9999"​)
 server.start
 Timeout.timeout(3) ​do
 loop​ ​do
 begin
  HTTParty.get(​'http://localhost:9999'​)
 break
 rescue​ Errno::ECONNREFUSED => try_again
  sleep 0.1
 end
 end
 end
 
 at_exit ​do
  server.stop
 end

If you need to start a different kind of web service, just substitute the rackup command for the necessary command to start up your web server. The rest of the pattern, polling the service until it’s up, should work perfectly whatever platform your web service runs on.

The code starts the server in the background and waits up to three seconds until it responds. The at_exit hook shuts it down just before Cucumber exits when the tests have been run. With this in place, let’s run cucumber again:

 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
  undefined method ‘get’ for #<Object:0x63756b65>
  Did you mean? gets
  gem (NoMethodError)
  ./features/step_definitions/rest_steps.rb:2
  features/fruit_list.feature:10
  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, 1 skipped, 1 passed)
 0m0.021s

This is a bit like opening up a set of Russian dolls: each time a new step passes, we learn more about what we need to do next. Our first step is passing again, but now the second step is failing. We no longer have a get method available to make our request since we removed our Rack-Test code.

This is the first time we’ll put HTTParty—our HTTP client—to use. We need to modify the steps in features/step_definitions/rest_steps.rb to use HTTParty instead of Rack-Test:

 require ​'httparty'
 When(​/^the client requests GET (.*)$/​) ​do​ |path|
  @last_response = HTTParty.get(​'http://localhost:9999'​ + path)
 end
 
 Then(​/^the response should be JSON:$/​) ​do​ |json|
  expect(JSON.parse(@last_response.body)).to eq JSON.parse(json)
 end

First, we load HTTParty with require ’httparty’. Then, we call HTTParty.get to issue an HTTP request. Here we have to pass a full URL, which is why we tack on ’http://localhost:9999’ before the path passed to the step definition. We store the response in a @last_response instance variable, because HTTParty’s API is a little different from Rack-Test. Finally, in the last step definition we refer to that variable to get the body from the HTTP request. Let’s run the feature again:

 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.030s

Great! If you’re following along, you’ll probably notice that the scenario feels slower to run than the equivalent in-process test. This is the added overhead of waiting for the web server to start up before the scenario starts. Remember, you pay this penalty only once per test run. Running Cucumber out-of-process with HTTParty is just as successful as our in-process approach that used Rack-Test. We didn’t change a single line of our feature files—all the changes are nicely tucked away in our step definitions. The main difference was that this time we executed real HTTP requests, and the tests took a little bit longer to fire up.

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

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