Even this simple app has several pieces. It’s easy to feel overwhelmed as we’re deciding what to test first. Where do we start?
To drive the first example, ask yourself: what’s the core of the project? What’s the one thing we agree our API should do? It should faithfully save the expenses we record.
Let’s encode the first part of that desired behavior in a spec, and then implement the behavior. Place the following code in spec/acceptance/expense_tracker_api_spec.rb:
| require 'rack/test' |
| require 'json' |
| |
| module ExpenseTracker |
| RSpec.describe 'Expense Tracker API' do |
| include Rack::Test::Methods |
| |
| it 'records submitted expenses' do |
| coffee = { |
| 'payee' => 'Starbucks', |
| 'amount' => 5.75, |
| 'date' => '2017-06-10' |
| } |
| |
| post '/expenses', JSON.generate(coffee) |
| end |
| end |
| end |
Note that we can nest RSpec contexts inside modules. In our codebase, we’ll enclose both our app and our specs inside the ExpenseTracker module so that we have easy access to all the classes defined by our app.
Use a Precise Data Type to Represent Currency | |
---|---|
![]() |
To simplify our code examples so you can focus on learning RSpec, we’re using regular Ruby floating-point numbers to represent expense amounts—even though floating-point arithmetic isn’t precise enough to handle money.[25] On a real project, we’d either use the BigDecimal class built into Ruby or a dedicated currency library like the Money gem.[26][27] |
We don’t need to design the entire API up front. Let’s assume we’ll be POSTing some key-value pairs to the /expenses endpoint. As many web APIs do, we’ll support sending and receiving data in JSON format.[28] Because JSON objects convert to Ruby hashes with string keys, our example data will also have string keys. For example, we’ll say { ’payee’ => ’Starbucks’ } instead of { payee: ’Starbucks’ }.
To get the data into and out of our app, we’ll use several different helper methods from Rack::Test::Methods. As you can see, you can include Ruby modules into an RSpec context, just like you’re used to doing inside Ruby classes.
The first Rack::Test helper we’ll use is post. This will simulate an HTTP POST request, but will do so by calling our app directly rather than generating and parsing HTTP packets.
We don’t have an app yet, but let’s go ahead and run our specs anyway. This will give us a hint on what to implement next:
| $ bundle exec rspec |
| F |
| |
| Failures: |
| |
| 1) Expense Tracker API records submitted expenses |
| Failure/Error: post ’/expenses’, JSON.generate(coffee) |
| |
| NameError: |
| undefined local variable or method ‘app’ for ↩ |
| #<RSpec::ExampleGroups::ExpenseTrackerAPI:0x007fef0404f560> |
| |
| truncated |
This error message, and the Rack::Test documentation, tell us that our test suite needs to define an app method that returns an object representing our web app.[30]
We haven’t built this object yet. Let’s assume it will be a class called API in the ExpenseTracker module. Add the following code inside your context, just above the it line:
| def app |
| ExpenseTracker::API.new |
| end |
RSpec contexts are just Ruby classes, which means you can define helper methods like app, and they’ll be available inside all your examples.
Run your specs again to see where they fail:
| $ bundle exec rspec |
| F |
| |
| Failures: |
| |
| 1) Expense Tracker API records submitted expenses |
| Failure/Error: ExpenseTracker::API.new |
| |
| NameError: |
| uninitialized constant ExpenseTracker::API |
| |
| truncated |
We haven’t defined this class yet. Let’s do so.
Unlike Ruby on Rails, Sinatra doesn’t have an established directory naming convention. Let’s follow the Rails convention and put our application code in a folder called app. Place the following code in app/api.rb:
| require 'sinatra/base' |
| require 'json' |
| |
| module ExpenseTracker |
| class API < Sinatra::Base |
| end |
| end |
This class defines the barest skeleton of a Sinatra app. Now, our tests need to load it. Back in your spec, add the following line to the require section at the top:
| require_relative '../../app/api' |
Now, your specs are passing. You may be wondering, “Did we actually test anything? Shouldn’t we expect or assert something?’’
We’ll get to that. Right now, we’re verifying only that the POST request completes without crashing the app. Next, we’ll actually check that we got a valid response back from the app.