Ensure passing Jasmine specs from your Ruby E2E tests
Jasmine is a great way to unit test your JavaScript components without writing an expensive end-to-end test for every small requirement.
After we integrated Jasmine into a Rails app we often add an E2E test that opens that Jasmine runner and expects all specs to pass. This way we see Jasmine failures in our regular test runs.
RSpec
In a [feature spec](https://web.archive.org/web/20150201092849/http://www.rel...
GoodJob: Ensure you get notified about background exceptions
GoodJob and ActiveJob rescue exceptions internally, preventing exception_notification from triggering. This can cause silent job failures.To get notified, subscribe to ActiveJob events and configure GoodJob's on_thread_error
hook. This lets you manually call your exception notifier for every retry, discard, or internal GoodJob error.
# config/initializers/good_job.rb
# Manually notify on job failures, as they are handled internally by ActiveJob/GoodJob.
ActiveSupport::Noti...
Defining custom RSpec matchers
There are three ways to define your own RSpec matchers, with increasing complexibility and options:
1) Use RSpec::Matchers.define
RSpec::Matchers.define :be_a_multiple_of do |expected|
match do |actual|
actual % expected == 0
end
# optional
failure_message do |actual|
"expected that #{actual} would be a multiple of #{expected}"
end
# optional
failure_message_when_negated do |actual|
"expected that #{actual} would not be a multiple of #{expected}"
end
end
- This is automatically available i...
Verifying doubles in RSpec 3
RSpec 3 has verifying doubles. This breed of mock objects check that any methods being stubbed are present on an instance of a given class. They also check methods aren't called with the wrong number of arguments.
This dual approach allows you to move very quickly and test components in isolation, while
giving you confidence that your doubles are not a complete fiction.
You should always prefer using a verifying double to using an old-school mock
...
Adding Jasmine JavaScript specs to a Webpack(er) project
The goal is to get Jasmine specs running in a Rails project using Webpacker, with the browser based test runner. Should be easily adaptable to a pure Webpack setup.
Step 1: Install Jasmine
yarn add jasmine-core
Step 2: Add two separate packs
Since we do not want to mix Jasmine into our regular Javascript, we will create two additional packs. The first only contains Jasmine and the test runner. The second will contain our normal application code and the specs themselves.
We cannot...
Ruby: How to use prepend for cleaner monkey patches
Let's say you have a gem which has the following module:
module SuperClient
def self.foo
'Foo'
end
def bar
'Bar'
end
end
For reasons you need to override foo
and bar
.
Keep in mind: Your code quality is getting worse with with each prepend
(other developers are not happy to find many library extensions). Try to avoid it if possible.
- Add a
lib/ext/super_client.rb
to your project (see How to organize monkey patches in Ruby on Rails projects) - Add the extension, which ov...
How to find out which type of Spec you are
When you need to find out in which kind of spec you are during run-time, it's definitely possible. It's a lot easier in RSpec 2+.
For example, consider this global before
block where you'd want to run some code for specific specs only:
config.before do
# stuff
that_fancy_method
# more stuff
end
RSpec 2+
If you want to run such a block for a specific type of specs, you can use filters:
config.before do
# stuff
# more stuff
end
config.before :type =...
Capybara: Preventing server errors from failing your test
When your Rails application server raises error, Capybara will fail your test when it clears the session after the last step. The effect is a test that passes all steps, but fails anyway.
Capybara's behavior will help you to detect and fix errors in your application code. However, sometimes your application will explode with an error outside your control. Two examples:
- A JavaScript library references a source map in its build, but forgets to package the source map
- CarrierWave cleans up an upload or cache file after the record was delet...
Generating and streaming ZIP archives on the fly
When your Rails application offers downloading a bunch of files as ZIP archive, you basically have two options:
- Write a ZIP file to disk and send it as a download to the user.
- Generate a ZIP archive on the fly while streaming it in chunks to the user.
This card is about option 2, and it is actually fairly easy to set up.
We are using this to generate ZIP archives with lots of files (500k+) on the fly, and it works like a charm.
Why stream downloads?
Offering downloads of large archives can be cumbersome:
- It takes time to b...
Rspec: around(:all) and around(:each) hook execution order
Background
-
before(:all)
runs the block once before all of the examples. -
before(:each)
runs the block once before each of your specs.
Summary
-
around(:suite)
does not exist. -
around(:all)
runs afterbefore(:all)
and beforeafter(:all)
. -
around(:each)
runs beforebefore(:each)
and afterafter(:each)
.
As this is not 100% obvious (and not yet documented) it is written down in this card. In RSpec 3 :each
has the alias :example
and :all
the alias :context
.
Example
RSpec.configure do |config|
...
JSON APIs: Default design for common features
When you build a JSON API you need to come up with a style to represent attributes, pagination, errors or including associated objects. Instead of reinventing the wheel, you may reuse successful API designs.
JSON API
JSON:API specifies common features found in most JSON APIs, like pagination, ordering and nested resources. The spec looks very similar to how one would build an API with Rails and uses similar patterns. Even if you don't plan on supporting the whole spec, it can still make sense to know how th...
Don't require files in random order
A common pattern in Ruby is to to require all files in a specific diretory, using something like
Dir.glob(Rails.root.join('lib/ext/**/*.rb')).each do |filename|
require filename
end
However, this causes files to be required in an order determined by the file system. Since load order can be important, this may lead to different behavior on different machines which are hard to debug.
Simply add a .sort
:
Dir.glob(Rails.root.join('lib/ext/**/*.rb')).sort.each do |filename|
require filename
end
Ruby 3
...
Cucumber: Identifying slow steps that drag down your test speed
In most projects I know, Cucumber test suite speed is not an issue. Of course, running 350 features takes its time, but still each test for itself is reasonably fast. There is nothing you can do to fundamentally speed up such a test (of course, you should be using parallel_tests).
However, in projects that go beyond clicking around in a web GUI and checking results, there might be various measures to speed things up. Katapult tests for example could be sped up more than 4 times by re...
Enabling view rendering for controller specs
Views are normally (for good reason) not rendered in controller specs. If you need it to happen, use:
RSpec 1 (Rails 2):
RSpec 2 (Rails 3):
Note that you can't use that inside it
blocks but need to put it in the nesting example group, like this:
describe '#update' do
cont...
Heads up: RSpec-Mocks' #stub_const will define intermediate modules that have not been loaded yet
The issue: You are using stub_const
to change a constant value for your test.
stub_const "SomeClass::CONST", 'test'
All of a sudden, tests fail with undefined method 'some_method' for #<SomeClass:0x00000000101433a8>
.
The reason
When using stub_const before the Class containing the constant has been loaded, a module is automatically created with the name.
Since RSpec does no autoloading, it will create a SomeClass
module by itself. This is arguably a good idea.
As a workaround, use stub_const
in your Rails specs li...
Capistrano 3: Running a command on all servers
This Capistrano task runs a command on all servers.
bundle exec cap production app:run cmd='zgrep -P "..." RAILS_ROOT/log/production.log'
Code
# lib/capistrano/tasks/app.rake
namespace :app do
# Use e.g. to grep logs on all servers:
# b cap production app:run_cmd cmd='zgrep -P "..." RAILS_ROOT/log/production.log'
#
# * Use RAILS_ROOT as a placeholder for the remote Rails root directory.
# * Append ` || test $? =1;` to grep calls in order to avoid exit code 1 (= "nothing found")
# * To be able to process ...
Carrierwave: How to remove container directories when deleting a record
When deleting a record in your Rails app, Carrierwave automatically takes care of removing all associated files.
However, the file's container directory will not be removed automatically. If you delete records regularly, this may be an annoyance.
Here is a solution which was adapted from the Carrierwave GitHub wiki and cleans up any empty parent directories it can find.
class ExampleUploader < CarrierWave...
Migration from the Asset Pipeline to Webpacker
This is a short overview of things that are required to upgrade a project from the Asset Pipeline to Webpacker. Expect this upgrade to take a few days even the diff is quite small afterwards.
Preparations
1. Find all libraries that are bundled with the asset pipeline. You can check the application.js
and the application.css
for require
and import
statements. The source of a library is most often a gem or a vendor directory.
2. Find an working example for each library in the application and write it down.
3. Find out the ver...
Disable SimpleCov if you only run a fraction of your tests
Coverage reports are rarely useful if you run only small parts of your test suite.
Just do not load SimpleCov in this case, and you will see less noise in your test output:
if RSpec.configuration.files_to_run.count > 5
require "simplecov"
SimpleCov.start 'rails'
end
See also
Why has_many :through associations can return the same record multiple times
An association defined with has_many :through
will return the same record multiple times if multiple join models for the same record exist (a n:m relation). To prevent this, you need to add ->{ uniq }
as second argument to has_many
(below Rails 4 it is a simple option: has_many :xyz, :uniq => true
).
Example
Say you have an Invoice
with multiple Items
. Each Item
has a Product
:
class Invoice < ActiveRecord::Base
has_many :items
has_many :products, :through => :items
end
class Item < ActiveRecord::Base
...
Heads up: Quering array columns only matches equally sorted arrays
Given you have an array column like this:
create_table "users", force: :cascade do |t|
t.integer "movie_ids", default: [], array: true
end
You might think that the following queries yield the same result:
User.where(movie_ids: [16, 17])
User.where(movie_ids: [17, 16])
Turn's out - they are not! They do care about array ordering more than I do.
To query for identical arrays independent of their order you have to either:
- Sort both the query and database content. If you're on Rails 7.1 you can use the new [`normal...
Testing ActiveRecord callbacks with RSpec
Our preferred way of testing ActiveRecord is to simply create/update/destroy the record and then check if the expected behavior has happened.
We used to bend over backwards to avoid touching the database for this. For this we used a lot of stubbing and tricks like it_should_run_callbacks
.
Today we would rather make a few database queries than have a fragile test full of stubs.
Example
Let's say your User
model creates a first Project
on cr...
Use DatabaseCleaner with multiple test databases
There is a way to use multiple databases in Rails.
You may have asked yourself how you're able to keep your test databases clean, if you're running multiple databases with full read and write access at the same time. This is especially useful when migrating old/existing databases into a new(er) one.
Your database.yml
may look like this:
default: &default
adapter: postgresql
encoding: unicode
username: <%= ENV['DATABASE_USER'] %>
host: <%= ENV['DATABASE...
Heads up: pg_restore --clean keeps existing tables
When restoring a PostgreSQL dump using pg_restore
, you usually add the --clean
flag to remove any existing data from tables.
Note that this only removes data from tables that are part of the dump and will not remove any extra tables. You need to do that yourself.
When is this relevant?
As an example: You want to load a staging dump into your development machine. On your development machine, you have run migrations that introduced more tables which do not yet exist on staging. pg_restore
with --clean
will loa...