Why Gemfury? Push, build, and install  RubyGems npm packages Python packages Maven artifacts PHP packages Go Modules Bower components Debian packages RPM packages NuGet packages

universityofyork / uoy-faculty-spec-helpers   ruby

Repository URL to install this package:

  bin
  lib
  Gemfile
  README.md
  uoy-faculty-spec-helpers.gemspec
 
  README.md

Spec Helpers

Installation

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

Usage

  • 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 user

For 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' }

Permission Helpers

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

Database Helpers

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

Table Helpers

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.

Helper methods

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

with_cleanup

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.

RSpec Verbiage

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.

Shared Examples

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'

Development

Versioning

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!

Running tests

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.

Identifying Performance Tests

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

Contributing

Bug reports and pull requests are welcome at https://github.com/university-of-york/faculty-dev-spec-helpers-gem