Saving expenses is all fine and good, but it’d be nice to retrieve them. We want to allow users to fetch expenses by date, so let’s post a few expenses with different dates and then request the expenses for one of those dates. We expect the app to respond with just the expenses recorded on that date.
Posting one expense after another will get really old if we have to keep repeating all that code. Let’s extract that helper logic into a post_expense helper method inside the RSpec.describe block:
| def post_expense(expense) |
| post '/expenses', JSON.generate(expense) |
| expect(last_response.status).to eq(200) |
| |
| parsed = JSON.parse(last_response.body) |
| expect(parsed).to include('expense_id' => a_kind_of(Integer)) |
| expense.merge('id' => parsed['expense_id']) |
| end |
This is basically the same code as before, except we’ve added a call to merge at the end. This line just adds an id key to the hash, containing whatever ID gets auto-assigned from the database. Doing so will make writing expectations easier later on; we’ll be able to compare for exact equality.
Now, change the coffee expense to the following, shorter code:
| coffee = post_expense( |
| 'payee' => 'Starbucks', |
| 'amount' => 5.75, |
| 'date' => '2017-06-10' |
| ) |
Using the same helper, let’s post one expense on the same date and one on a different date:
| zoo = post_expense( |
| 'payee' => 'Zoo', |
| 'amount' => 15.25, |
| 'date' => '2017-06-10' |
| ) |
| |
| groceries = post_expense( |
| 'payee' => 'Whole Foods', |
| 'amount' => 95.20, |
| 'date' => '2017-06-11' |
| ) |
Finally, you can query all the expenses for June 10th, and make sure the results contain only the values from that date. Add the following highlighted lines inside the same spec we’ve been working in, right after the zoo and groceries expenses:
| it 'records submitted expenses' do |
| # POST coffee, zoo, and groceries expenses here |
| |
» | get '/expenses/2017-06-10' |
» | expect(last_response.status).to eq(200) |
» | |
» | expenses = JSON.parse(last_response.body) |
» | expect(expenses).to contain_exactly(coffee, zoo) |
| end |
We’re using the same techniques from before: driving the app, grabbing the last_response from Rack::Test, and looking at the results. There are lots of ways to compare collections in RSpec. Here, we want to check that the array contains the two expenses we want—and only those two—without regard to the order. The contain_exactly matcher captures this requirement.
If we had a specific business requirement that the expenses be in a certain sequence, we could compare the collections with eq instead, as in eq [coffee, zoo]. Here, we don’t care about the order. Using a matcher more flexible than eq makes our spec more resilient, giving us the latitude to change the ordering in the future without fighting a broken test.
Go ahead and run the latest version of your spec:
| $ bundle exec rspec |
| F |
| |
| Failures: |
| |
| 1) Expense Tracker API records submitted expenses |
| Failure/Error: expect(last_response.status).to eq(200) |
| |
| expected: 200 |
| got: 404 |
| |
| (compared using ==) |
| |
| truncated |
Since we haven’t defined a way for clients to read back data yet, we’re getting another 404 status code. Let’s add a route to the Sinatra app that returns an empty JSON array:
| get '/expenses/:date' do |
| JSON.generate([]) |
| end |
Now, when we rerun our specs, we get an incorrect response, rather than an HTTP error:
| $ bundle exec rspec |
| F |
| |
| Failures: |
| |
| 1) Expense Tracker API records submitted expenses |
| Failure/Error: expect(expenses).to contain_exactly(coffee, zoo) |
| |
| expected collection contained: [{"payee"=>"Starbucks", ↩ |
| "amount"=>5.75, "date"=>"2017-06-10", "id"=>42}, {"payee"=>"Zoo", ↩ |
| "amount"=>15.25, "date"=>"2017-06-10", "id"=>42}] |
| actual collection contained: [] |
| the missing elements were: [{"payee"=>"Starbucks", ↩ |
| "amount"=>5.75, "date"=>"2017-06-10", "id"=>42}, {"payee"=>"Zoo", ↩ |
| "amount"=>15.25, "date"=>"2017-06-10", "id"=>42}] |
| |
| truncated |
There’s not much point in putting off the inevitable. We’re going to have to write some code to save and load expenses.