Repository URL to install this package:
Add these lines to your application's Gemfile:
source 'https://gem.fury.io/universityofyork/' do gem 'uoy-faculty-spec-helpers', '~> 0.1', require: 'faculty/spec_helpers' end
And then execute:
$ bundle
Or install it yourself as:
$ gem install uoy-faculty-spec-helpers
include_context 'log in as', 'username'
include_context 'xhr request'
for requests requesting application/json
instead of text/html
grant_permissions(*permissions, **filters)
will grant the given permissions to the logged in userFor capybara tests, if you have a <dl>
list, you can use dd_field(page, text)
to search for <dt>text</dt>
in page
and it will return the contents of the next <dd>
; e.g.
<dl>
<dt>Username</dt>
<dd>abc123</dd>
</dl>
it { expect(dd_field(subject, 'Username')).to have_text 'abc123' }
If your app does not have permission to write to the rbac schema, you can use the permissions helpers to grant permissions before your tests run.
In spec_helper.rb
:
module RSpecMixin include Faculty::PermissionHelpers end RSpec.configure do |config| config.include RSpecMixin config.include_context 'database transaction', db_transaction: true end
Ensure DB.transaction
is not set up in an around block. This puts the transaction at the top level, which causes
issues when using multiple DB connections.
Using the permission helper in a test:
context 'when authorized for any structure unit' do include_context 'with permissions', :staffmanager_view it_behaves_like 'the endpoint permits access' end
If you have a test that will insert data, you can still use a transaction, but it must be declared within the permission context (directly on the example works well):
context 'when inserting data' do include_context 'with permissions', :staffmanager_view it 'inserts data correctly', :db_transaction do do_some_data_inserts assert_something end end
These allow a separate connection to the postgres instance as the postgres
user. Tests should run as the appname_app
user, which won't have permission to
all tables.
In spec_helper.rb
, add the following:
config.before(:all) do DatabaseHelper.connect( adapter: 'postgres', host: ENV['TEST_DB'] || 'localhost', database: 'faculty', user: 'postgres' ) end
Usage:
# You'd usually put this in your RSpecMixin include Faculty::DatabaseHelper # With explicit schema dbt.people.person.insert(username: 'abc123') # Define a schema search order let(:dbt_search_order) { %i[people] } dbt.person.insert(username: 'user1', display_name: 'Test Display Name') dbt.person.include?(username: 'abc123') # => true dbt.person.truncate
To define your own helpers:
module MyTables extend Faculty::DatabaseHelper::TableMacros define_table :app_thing, Faculty::DatabaseHelper::ExternalTableHelper, dbt.default: { name: 'name', archived: nil } end
To use them:
include Faculty::DatabaseHelper dbt.app_thing.insert(name: 'another name') dbt.app_thing.insert dbt.app_thing.include?(name: 'any simple set of fields to match') dbt.app_thing.truncate
There are two flavours of helpers - ExternalTableHelper, which performs all
modifications using the postgres (DBA) user, and LocalTableHelper, which uses
the app's DB user (app_name_app
).
LocalTableHelper will work with the transactional test wrapper. Note that
ExternalTableHelper uses a separate connection, so is typically needed in before
and after :all
blocks so that the data is visible within the test transaction.
Extra functionality is exposed by the Helpers class. To activate it:
# In your RSpec.configure block: config.extend Faculty::DatabaseHelper::Helpers
You only need this if you want to use the helpers
This works like an around
block by itself, and lets you do cleanup for
operations that have to be executed outside of the transaction that the
examples are run in. This typically is because they're in a separate DB
session that's running as the postgres
superuser.
Once the helpers are activated, you can use this within a context
or
describe
block like so:
context 'when there is data in a table' do with_cleanup do # Works with dbt row dbt.my_table.insert(...) rows dbt.other_table.insert(...) row dbt.my_schema.my_table.insert(...) # Also works with a fluent interface rows student(username: 'abc123', code: '0123').with_person(display_name: 'Student 1') end end
row
or rows
records any data inserted with your statements, and removes
them in the reverse order to insertion.
They are not identical, you must use rows if the statement inserts multiple records
You cannot use with_cleanup when you have a config.around block starting a DB transaction in spec_helpers (like in Flexi).
⚠ There are constraints when using with_cleanups in multiple nested contexts. In any context where multiple with_cleanup blocks would be run, only the outer one can make changes using an ExternalTableHelper. If any other one tries this, the transaction nesting will cause the spec to hang. LocalTableHelper can still be used. This is a shortcoming of the multiple connection design.
For example, the following would hang:
context 'when there is data in a table' do # This block will cause a transaction to be started for each example with_cleanup do row dbt.people.person.insert(...) end context 'with the person has an affiliation' do with_cleanup do # This will hang as it uses ExternalTableHelper row dbt.people.affiliation.insert(...) end end context 'when the person has a record in my app' do with_cleanup do # If this is a LocalTableHelper, this will not hang; the transaction # will be checkpointed (this has no negative consequence) row dbt.app_user.insert(username: 'rb499') end end end
In Flexi, with_cleanup can be used by adding let(:has_with_cleanup) { true }
to the spec file, which disables the config.around block DB transaction.
You can define shorthand verbs to help make shorter, readable specs. When you do this, you might annoy rubocop's RSpec/EmptyExampleGroup spec as it can't detect these short examples. Either add a regular example or disable the cop.
RSpec.describe('The username validation regex') do subject(:regex) { /^([a-z]+)[0-9]+$/ } extend Faculty::RSpecVerbiage verb(:accepts) { |t| is_expected.to match(t) } verb(:rejects) { |t| is_expected.not_to match(t) } verb(:extracts) { |t, from:| expect(from.scan(regex)).to include([t]) } accepts 'abc123' rejects 'andy.carlton' extracts 'abc', from: 'abc123' end
The documentation output is constructed to be readable, too.
There are two sets of shared examples for different types of tests.
endpoint_behaviours
expect subject
to be last_response
after a rack get
or post
request.page_behaviours
expect subect
to be page
after a capybara visit
request.For example:
RSpec.describe 'GET /this-page-will-never-be-found' do subject do get '/this-page-will-never-be-found' last_response end it_behaves_like 'the endpoint was not found' end
RSpec.describe 'VISIT /this-page-will-never-be-found' do subject do visit '/this-page-will-never-be-found' page end it_behaves_like 'the page was not found' end
When testing a redirect, you can either pass the expected redirect URL directly or,
if your url is defined using a let
helper, you can set redirect_to
within the example group:
it_behaves_like 'the endpoint redirects', to: '/new-url' it_behaves_like 'the endpoint redirects' do let(:redirect_to) { new_url } end
The destination can either be a regex, which is tested against the full URL, or a string URL which is tested depending on the elements present; any elements must exactly match the current page / location header.
For endpoints only, you can allow any URL by omitting to:
and redirect_to
.
This is not possible for pages because there is no state to determine whether a
redirect was issued. For example, the following test doesn't care about the
destination url, since neither redirect_to
nor to:
are set:
it_behaves_like 'the endpoint redirects'
You are also free to use match_url
as a matcher to perform the same URL matching
on arbitrary URLs in tests:
expect('https://example.org/search').to match_url '/search'
Your Gem's version is picked up automatically from lib/spec-helpers.rb
. When any
changes are pushed to master, after the normal CI tasks the pipeline will push
to gemfury automatically. The usual workflow is:
For minor changes, update VERSION
and make the change in a single commit
For anything else, create a branch and set VERSION
to the version you're
aiming to release for. Make the changes; when the branch is merged, the gem
will be uploaded.
Note that gemfury will never overwrite an existing gem version, even if the old one is yanked!
Some tests require a database image; run docker-compose up -d
before running the tests.
Tests can be run via rake: bundle exec rake spec
- this doesn't run
performance tests; they can be run separately via bundle exec rake perf
.
You can also run rspec
normally e.g. bundle exec rspec -fd
.
If you have performance tests that take a while, tag the context
/ describe
block like this:
context 'when foo is bar baz', :perf do ... end
Bug reports and pull requests are welcome at https://github.com/university-of-york/faculty-dev-spec-helpers-gem