We’ve seen how to run Sequel migrations manually from the command line. You’ll need to configure RSpec to run them automatically, so that the database structure is in place before your first integration spec runs.
The following code will make sure the database structure is set up and empty, ready for your specs to add data to it:
| Sequel.extension :migration |
| Sequel::Migrator.run(DB, 'db/migrations') |
| DB[:expenses].truncate |
First, we run all the migration files in to make sure that all the database tables exist with their current schema. Then, we remove any leftover test data from the table using the truncate method. That way, each run of the spec suite starts with a clean database—no matter what happened before.
The only problem is that it’s not obvious where to put these lines. Up to this point, you’ve tended to keep setup routines like this in one of two places:
Database migrations sit somewhere in between these two extremes. We want to load them for any spec that touches the database, but not for unit specs—those need to stay snappy, even as our database migrations grow over the lifetime of the application.
The RSpec convention for this kind of “partially shared” code is to put it in a folder called spec/support; then we can load it from whatever spec files need it. Create a new file called spec/support/db.rb with the following content:
| RSpec.configure do |c| |
| c.before(:suite) do |
| Sequel.extension :migration |
| Sequel::Migrator.run(DB, 'db/migrations') |
| DB[:expenses].truncate |
| end |
| end |
This snippet defines a suite-level hook. We first encountered before hooks in Hooks. A typical hook will run before each example. This one will run just once: after all the specs have been loaded, but before the first one actually runs. That’s what before(:suite) hooks are for.
Bootstrap Your Environment for Easy Testing | |
---|---|
Your spec suite should set up the test database for you, rather than requiring you to run a separate setup task. People testing your code (including you!) can easily forget to run the extra step, or might not even know they need to. |
Now that we have defined our hook, we are ready to define our spec and load the support file. Create a file called spec/integration/app/ledger_spec.rb with the following code:
| require_relative '../../../app/ledger' |
| require_relative '../../../config/sequel' |
| require_relative '../../support/db' |
| |
| module ExpenseTracker |
| RSpec.describe Ledger do |
| let(:ledger) { Ledger.new } |
| let(:expense) do |
| { |
| 'payee' => 'Starbucks', |
| 'amount' => 5.75, |
| 'date' => '2017-06-10' |
| } |
| end |
| |
| describe '#record' do |
| # ... contexts go here ... |
| end |
| end |
| end |
The :ledger and :expense setup will be the same for each example, so we’ve used let to initialize this data.
Now, it’s time to spell out the behavior we want in the first example. We want to tell the ledger to save the expense, and then actually read the database from disk and make sure the expense really got saved:
| context 'with a valid expense' do |
| it 'successfully saves the expense in the DB' do |
| result = ledger.record(expense) |
| |
| expect(result).to be_success |
| expect(DB[:expenses].all).to match [a_hash_including( |
| id: result.expense_id, |
| payee: 'Starbucks', |
| amount: 5.75, |
| date: Date.iso8601('2017-06-10') |
| )] |
| end |
| end |
We’re using a couple of new matchers here. The first, be_success, simply checks that result.success? is true. This matcher is built into RSpec; you’ll learn more about it in Dynamic Predicates.
The second matcher, match [a_hash_including(...)], expects our app to return data matching a certain structure; in this case, a one-element array of hashes with certain keys and values. This expression is another use of RSpec’s composable matchers; here, you’re passing the a_hash_including matcher into the match one.
We’re deviating a bit from general TDD practice in this snippet. Normally, each example would only have one expectation in it—otherwise, one failure can mask another. Here, we’ve got two expectations in the same example.
There’s a bit of a trade-off to consider here. Any spec that touches the database is going to be slower, particularly in its setup and teardown steps. If we follow “one expectation per example” too rigorously, we’re going to be repeating that setup and teardown many times. By judiciously combining a couple of assertions, we’re keeping our suite speedy.
Let’s see what we’re giving up to get this performance boost. Go ahead and run your specs:
| $ bundle exec rspec spec/integration/app/ledger_spec.rb |
| truncated |
| |
| Failures: |
| |
| 1) ExpenseTracker::Ledger#record with a valid expense successfully saves ↩ |
| the expense in the DB |
| Failure/Error: expect(result).to be_success |
| expected nil to respond to ‘success?‘ |
| # ./spec/integration/app/ledger_spec.rb:23:in ‘block (4 levels) in ↩ |
| <module:ExpenseTracker>’ |
| |
| Finished in 0.02211 seconds (files took 0.15418 seconds to load) |
| 1 example, 1 failure |
| |
| Failed examples: |
| |
| rspec ./spec/integration/app/ledger_spec.rb:20 # ↩ |
| ExpenseTracker::Ledger#record with a valid expense successfully saves the ↩ |
| expense in the DB |
| |
| Randomized with seed 27984 |
As you’d expect, the spec failed on the first assertion. We never even see the second assertion, because by default, RSpec aborts the test on the first failure.
It would be nice to record the first failure, but continue to try the second expectation. Good news! The :aggregate_failures tag does just that. Change your example declaration to the following:
| it 'successfully saves the expense in the DB', :aggregate_failures do |
Now, RSpec’s output shows both failures underneath a description of the example:
| $ bundle exec rspec spec/integration/app/ledger_spec.rb |
| truncated |
| |
| Failures: |
| |
| 1) ExpenseTracker::Ledger#record with a valid expense successfully saves ↩ |
| the expense in the DB |
| Got 1 failure and 1 other error: |
| |
| 1.1) Failure/Error: expect(result).to be_success |
| expected nil to respond to ‘success?‘ |
| # ./spec/integration/app/ledger_spec.rb:23:in ‘block (4 levels) ↩ |
| in <module:ExpenseTracker>’ |
| |
| 1.2) Failure/Error: id: result.expense_id, |
| |
| NoMethodError: |
| undefined method ‘expense_id’ for nil:NilClass |
| # ./spec/integration/app/ledger_spec.rb:25:in ‘block (4 levels) ↩ |
| in <module:ExpenseTracker>’ |
| |
| Finished in 0.02142 seconds (files took 0.15952 seconds to load) |
| 1 example, 1 failure |
| |
| Failed examples: |
| |
| rspec ./spec/integration/app/ledger_spec.rb:20 # ↩ |
| ExpenseTracker::Ledger#record with a valid expense successfully saves the ↩ |
| expense in the DB |
| |
| Randomized with seed 41929 |
We’re likely to want our other integration tests to get this benefit. Move the :aggregate_failures property up one level from the example to the group:
| RSpec.describe Ledger, :aggregate_failures do |
RSpec refers to these properties of specs as metadata. When you define metadata—arbitrary symbols or hashes—on an example group, all the examples in that group inherit it, as do any nested groups. We previously saw an example of metadata being specified as a hash in Tag Filtering. Here we are defining our metadata using just a symbol as a shortcut. Internally, RSpec expands this into a hash like { aggregate_failures: true }.
Now, it’s time to fill in the behavior. The Ledger class’s record method needs to store the expense in the database, then return a RecordResult indicating what happened:
| def record(expense) |
| DB[:expenses].insert(expense) |
| id = DB[:expenses].max(:id) |
| RecordResult.new(true, id, nil) |
| end |
When you rerun RSpec, your integration spec should pass now.