Solidus
Search…
Testing Solidus

Introduction to testing

For most of the examples in this guide, we have also provided automated tests. While this is a bit unusual in product documentation, we wanted to give you something you can use as inspiration when testing your own customizations. Feel free to adapt our tests to fit your own style!
We can't emphasize enough the importance of writing tests for your Solidus app: Solidus is a large, complex framework, and you are bound to miss gotchas and edge cases when customizing it, no matter how much QA you do. Skipping on automated tests means asking for trouble, especially in eCommerce, where downtime translates directly into financial loss.
While writing tests may seem like a useless distraction in the short term, it will make you more productive in the long term, by allowing you to change code faster, with more confidence and with less manual work. Having good test coverage will also help you tremendously when upgrading Solidus.

Our testing philosophy

In this paragraph, you'll find some opinionated advice about how to test your Solidus application. This advice is the result of years spent evolving and maintaining large-scale eCommerce apps, and will almost certainly be a good starting point for anyone starting to work with Solidus.

Unit vs. system tests

In general, there's no hard rule on whether something should be tested with a unit test or a system test. Each developer and development team has their own style and philosophy, and you should find your own and adapt it over time, as your application grows and your needs evolve.
Our recommendation is to have a balanced test diet: write unit tests for all possible scenarios (happy paths, failure paths, edge cases, etc.), and write system tests for at least the happy path of all customer- or admin-facing features. This will help you ensure your application is working well in the real world, and not just when testing each component in isolation.
We're using "unit tests" in this guide in a loose way to refer to a test that primarily tests a single module/class rather than its interactions with the rest of the codebase.
In certain cases, you may also want to write lower-level integration tests which don't exercise the UI, but call multiple components without attempting to isolate them. A good use case would be testing that a given set of promo rules and actions works as expected when the promotion is applied to a real order.

Test coverage

~80% or higher is a good test coverage to aim for, but take it with a grain of salt.
In general, coverage metrics are not an optimal measure of test quality, as they don't tell you anything about where the code is being exercised and how its outputs and side effects are being measured: you can have very high test coverage and still have tons of blind spots in your application, because you're calling your code but not verifying its behavior.
Rather than obsessing over test coverage, create guidelines around how to write meaningful, effective tests. High test coverage will come as a natural byproduct.

Configuring your test environment

Here's the bare minimum you'll need to get started with testing your Solidus app:
Gemfile
1
group :development, :test do
2
gem 'rspec-rails'
3
gem 'factory_bot_rails', '~> 4.8'
4
end
5
6
group :test do
7
gem 'capybara', '>= 3.26'
8
end
Copied!
Let's go over the reason we're recommending these tools and how to set them up.

Test framework: RSpec

RSpec is the preferred testing framework in the Solidus world. While it's certainly possible to test a Solidus application with other frameworks (e.g., MiniTest), all of our test helpers have been written to support RSpec, so we strongly recommend using it.
To properly configure RSpec, run the following command after installing the rspec-rails gem:
1
$ rails g rspec:install
Copied!
After installing RSpec, take a look at spec/spec_helper.rb and spec/rails_helper.rb, as they contain some default configurations which you may want to uncomment. At the very least, make sure you uncomment these lines:
spec/rails:helper.rb
1
Dir[Rails.root.join('spec', 'support', '**', '*.rb')].sort.each { |f| require f }
Copied!
It will automatically load each file in spec/support before starting your test suite. This allows you to import test helpers and configurations from other gems without polluting your main RSpec configuration.
Throughout the rest of this guide, we'll assume you are loading files in spec/support.
For more information on RSpec and its usage, please see the official documentation.

Factories: FactoryBot

Very often, you'll want to generate an instance of a user, order, product or any other type of Solidus model in a test. Instead of forcing you to generate the data manually every time, we provide a set of convenience factories you can import in your app:
config/application.rb
1
module AmazingStore
2
class Application < Rails::Application
3
# ...
4
5
# Don't initialize FactoryBot if it's not in the current Bundler group.
6
if defined?(FactoryBotRails)
7
initializer after: 'factory_bot.set_factory_paths' do
8
require 'spree/testing_support/factory_bot'
9
10
# The paths for Solidus factories.
11
solidus_paths = Spree::TestingSupport::FactoryBot.definition_file_paths
12
13
# Optional: Any factories you want to require from extensions.
14
extension_paths = [
15
# MySolidusExtension::Engine.root.join("lib/solidus_content/factories/order.rb"),
16
# MySolidusExtension::Engine.root.join("lib/solidus_content/factories/product.rb"),
17
]
18
19
# Your application's own factories.
20
app_paths = [
21
# Rails.root.join('lib/factories'),
22
Rails.root.join('spec/factories'),
23
]
24
25
FactoryBot.definition_file_paths = solidus_paths + extension_paths + app_paths
26
end
27
end
28
end
29
end
Copied!
Finally, you'll want to import the FactoryBot DSL methods. This allows you to call create, build, build_stubbed and attributes_for in your tests without prefixing them with FactoryBot:
spec/support/factory:bot.rb
1
RSpec.configure do |config|
2
config.include FactoryBot::Syntax::Methods
3
end
Copied!
For more information on FactoryBot and its usage, please see the official documentation.

System tests: Capybara

Capybara is an acceptance test framework that simulates how a real user would interact with your app. Rails uses Capybara to implement system tests, which are tests where you interact with the UI of your application rather than directly calling individual modules.
When configured properly, system tests can also execute JavaScript code, just like a real browser would do. In order for JavaScript to be executed, you'll need to tell Capybara to switch to a JavaScript-capable browser for JS tests:
spec/support/capybara.rb
1
RSpec.configure do |config|
2
config.before(:each, type: :system) do |example|
3
if example.metadata[:js]
4
driven_by :selenium_chrome_headless
5
else
6
driven_by :rack_test
7
end
8
end
9
end
Copied!
The configuration above tells Capybara to use the default Rack::Test browser for non-JS tests, and Chrome for JS tests. This enables you to do the following:
spec/system/product:page:spec.rb
1
RSpec.describe "Product page", type: :system do
2
it "shows the product's description" do
3
visit "/products/solidus-shirt"
4
5
expect(page).to have_text("Solidus-branded T-shirt")
6
end
7
8
it "allows me to add a product to my cart", :js do
9
visit "/products/solidus-shirt"
10
11
click_button "Add to Cart"
12
13
expect(page).to have_text("Product was added to the cart!")
14
end
15
end
Copied!
In the example above, the first test, which doesn't require JavaScript, will be run with Rack::Test, which is faster. The second test will be run through a headless Chrome instance with JS capabilities.
For more information on system tests and Capybara, you can refer to the RSpec guides and Capybara's official documentation.

Using the built-in helpers

Solidus comes with a set of useful helpers you can use in your tests. You can find all of them under the spree/testing_support path. We suggest including the ones you need in your RSpec configuration to save some time when writing tests.
You will notice Solidus has more helpers than we are documenting here. This is because some of the helpers in spree/testing_support are mostly meant for internal use (i.e., for testing the Solidus codebase itself), and wouldn't be very useful in another test suite.
You may still use the undocumented helpers if you find them useful, but keep in mind they may change over time.

Authorization helpers

These helpers allow you to bypass Solidus' authorization system. This makes testing easier, since you don't have to stub the current user or the current ability.
In order to use the helpers, first include them in your RSpec configuration:
spec/support/solidus.rb
1
# ...
2
3
require 'spree/testing_support/authorization_helpers'
Copied!
Once you've included them, you can use them both in controller specs and system specs.
Stubbing or customizing the authorization system during testing can lead to unexpected bugs in production. Instead of stubbing the authorization system, just use Devise's helpers to sign in as a user with the right permissions.

Stubbing the authorization system

The stub_authorization! method bypasses the authentication and authorization systems completely by stubbing the current user and allowing you to perform any action on any resource:
1
RSpec.describe 'The product admin' do
2
stub_authorization!
3
4
it 'allows me to view the products' do
5
product = create(:product)
6
7
visit spree.admin_path
8
click_link 'Products'
9
10
expect(page).to have_content(product.name)
11
end
12
end
Copied!

Defining a custom authorization block

The custom_authorization! method, on the other hand, allows you to define a custom authorization block. You'll still need to authenticate the current user, if you use it:
1
RSpec.describe 'The product admin' do
2
custom_authorization! do
3
can :read, Spree::Product
4
end
5
6
it 'allows me to view the products' do
7
sign_in create(:user)
8
product = create(:product)
9
10
visit spree.admin_path
11
click_link 'Products'
12
13
expect(page).to have_content(product.name)
14
end
15
16
it 'does not allow me to edit products' do
17
sign_in create(:user)
18
product = create(:product)
19
20
visit spree.edit_admin_product_path(product)
21
22
expect(page).to have_content('Access denied')
23
end
24
end
Copied!
(Note that the sign_in helper is provided by Devise, not Solidus.)

Capybara helpers

These helpers make it easier to interact with the UI in system specs, especially when testing the Solidus backend. They can come in really useful if you've customized the backend and want to test some piece of functionality. They also suppress annoying Puma logs in the test output.
As always, the first step is to include them in your app:
spec/support/solidus.rb
1
# ...
2
3
require 'spree/testing_support/capybara_ext'
Copied!

Interacting with icons

You can use the click_icon helper to find and click on a specific FontAwesome icon:
1
RSpec.describe 'The option types admin' do
2
it 'allows me to delete the option types' do
3
sign_in create(:admin_user)
4
option_type = create(:option_type)
5
6
visit spree.admin_path
7
click_link 'Products'
8
click_link 'Option Types'
9
click_icon 'trash'
10
11
expect(page).to have_content('has been successfully removed')
12
end
13
end
Copied!

Interacting with tables

The within_row helper can be used to scope the Capybara context to a specific row within an index table in the backend:
1
RSpec.describe 'The product admin' do
2
it 'allows me to edit a product' do
3
sign_in create(:admin_user)
4
product1 = create(:product)
5
product2 = create(:product)
6
7
visit spree.admin_products_path
8
# Clicks the edit icon for the first product
9
within_row(1) { click_icon 'edit' }
10
11
# ...
12
end
13
end
Copied!
The column_text helper can be used to retrieve the text from a specific column in a table row:
1
RSpec.describe 'The product admin' do
2
it 'displays the product SKU' do
3
sign_in create(:admin_user)
4
product1 = create(:product)
5
product2 = create(:product)
6
7
visit spree.admin_products_path
8
9
within_row(1) do
10
expect(column_text(1)).to eq(product1.sku)
11
end
12
end
13
end
Copied!

Interacting with select2 inputs

The Solidus backend uses the Select2 jQuery plugin for nicer-looking, Ajax-enabled select boxes. Because Select2 inputs are not regular inputs, some additional code is required when interacting with them.
You can use the select2_search helper to search and select a specific option from a Select2 input (this would be equivalent to typing the option's text in the search field, then selecting the result):
1
RSpec.describe 'The orders admin' do
2
it 'allows me to filter by variant' do
3
sign_in create(:admin_user)
4
product = create(:product)
5
order = create(:order) do |o|
6
create(:line_item, order: o, variant: product.master)
7
end
8
9
visit spree.admin_orders_path
10
select2_search product.sku, from: 'Variant'
11
click_button 'Filter results'
12
13
expect(page).to have_content(order.number)
14
end
15
end
Copied!

Testing meta tags

You can use the have_meta helper to expect the current page to have a specific meta tag:
1
RSpec.describe 'The product page' do
2
it 'uses the product description in the meta description' do
3
product = create(:product)
4
5
visit spree.product_path(product)
6
7
expect(page).to have_meta(:description, product.description)
8
end
9
end
Copied!

Order helpers

Sometimes, you need to generate an order in a given state. Solidus ships with a set of order factories you can use to generate different types of orders, and these should be your first choice.
However, you will sometimes need a bit more granularity than what the factories provide (e.g., when testing the checkout flow). A common use case is wanting to generate an order in a certain state, only with the information the user would have provided up until that state (e.g., generate an order in the delivery state, only with address information). That's exactly what the OrderWalkthrough helper is for.
The OrderWaltkhrough helper is extremely opinionated: it assumes you haven't modified the order state machine in significant ways, and that your checkout flow resembles the standard, multi-page checkout flow from the starter frontend.
If you have altered the order state machine or checkout flow significantly, you may want to use the order factories instead, or write your own helper, in order for your tests to better resemble real-world usage.
First of all, include the helper in your test suite:
spec/support/solidus.rb
1
# ...
2
3
require 'spree/testing_support/order_walkthrough'
Copied!
You can now use the helper whenever you want:
1
RSpec.describe 'The checkout flow' do
2
it 'renders the delivery step' do
3
user = create(:user)
4
sign_in user
5
# Generate an order in the `delivery` state
6
order = Spree::TestingSupport::OrderWalkthrough.up_to(:address)
7
order.update!(user: user)
8
9
visit spree.checkout_path
10
11
expect(page).to have_current_path('/checkout/delivery')
12
end
13
end
Copied!
The OrderWalkthrough.up_to call will create a new order and it will simulate what a user would do if they want to the checkout flow and only completed the address state. The order will have line items and an address on it, and it will be in the delivery state, ready for shipping method selection.

Preference helpers

Sometimes, it can be useful to stub the Solidus configuration, to test how your store would behave with certain configuration options. This is usually the case for payment methods and other resources which can be configured by store operators without developer intervention: when that's the case, you want to make sure operators can't take any action that could cause a store malfunction, which requires testing under different configurations.
If you need to stub the configuration, first of all require the relevant helper:
spec/support/solidus.rb
1
# ...
2
3
require 'spree/testing_support/preferences'
Copied!
You can now use the stub_spree_preferences helper anywhere in your code. The helper accepts a hash of preferences, in which case the preferences will be stubbed on the global configuration:
1
RSpec.describe 'The product page' do
2
it 'renders the price in EUR' do
3
# Stub the global `currency` setting of the store
4
stub_spree_preferences(currency: 'EUR')
5
product = create(:product)
6
7
visit spree.product_path(product)
8
9
expect(page).to have_content('€100,00')
10
end
11
12
it 'renders the price in USD' do
13
# Stub the global `currency` setting of the store
14
stub_spree_preferences(currency: 'USD')
15
product = create(:product)
16
17
visit spree.product_path(product)
18
19
expect(page).to have_content('$100.00')
20
end
21
end
Copied!
The helper can also accept a configuration class and a hash of preferences, in which case the preferences will be stubbed on the provided configuration class:
1
RSpec.describe 'The backend' do
2
it 'is available in English' do
3
# Stub the `locale` setting of the backend
4
stub_spree_preferences(Spree::Backend::Config, locale: 'en')
5
6
visit spree.admin_path
7
8
expect(page).to have_content('Email')
9
end
10
11
it 'is available in French' do
12
# Stub the `locale` setting of the backend
13
stub_spree_preferences(Spree::Backend::Config, locale: 'fr')
14
15
visit spree.admin_path
16
17
expect(page).to have_content('Courriel')
18
end
19
end
Copied!

URL helpers

By default, your tests will not have access to the Solidus routes, because they're part of a Rails engine and not of your main application. You could access them with Spree::Core::Engine.routes.url_helpers, but you can include the URL helpers if you want to save a few characters:
spec/support/solidus.rb
1
# ...
2
3
require 'spree/testing_support/url_helpers'
4
5
RSpec.configure do |config|
6
config.include Spree::TestingSupport::UrlHelpers
7
end
Copied!
You can now access the helpers via the spree. shortcut:
1
RSpec.describe 'The product page' do
2
it 'is accessible' do
3
product = create(:product)
4
5
visit spree.product_path(product)
6
7
expect(page).to have_content(product.name)
8
end
9
end
Copied!

Testing your Solidus app

Since Solidus applications are just regular Rails applications, there's nothing special you need to do to test them: just write tests as you'd usually do and you'll be fine! With that said, there are some aspects to keep in mind when testing certain parts of your app, especially if you want to have an easy time upgrading Solidus.

Testing service objects

If you have implemented a service object to replace one of Solidus' default implementations, make sure to test it just as you would do with any other service object.
If you're inheriting from Solidus' default implementation, you should also test any behavior that's inherited from the original class, to make sure you haven't altered functionality in undesired ways. This will usually not be needed, because the customizable service objects in Solidus have small, well-defined interfaces, but keep it in mind!

Testing overrides

Testing an override is similar to testing a service objects that inherits from the original implementation: for full test coverage, you'll have to test both your customization and any original functionality that comes from the default implementation. This will ensure you haven't broken the original functionality in any way, and it will make it easier to upgrade Solidus.
This kind of gotcha is why you should prefer customizing Solidus via the extension hooks or the event bus instead of relying on overrides.

Testing the storefront

There's nothing special you need to do here: when testing your storefront, do it with system specs, as you would do for any regular Rails app. Focus on user-facing functionality and integration testing (i.e., don't write controller tests unless you have a very good reason to).

Testing the backend

Just like for the storefront, you should use system specs for testing your backend customizations. You may also want to leverage some of the built-in helpers Solidus provides for testing the backend UI.

Testing event subscribers

Event subscribers are an awesome way to decouple orthogonal logic, but once you start using them extensively, they can be tricky to test in isolation. Let's see an example.
Let's assume you are working on a store that integrates with a taxation service. When an order is finalized, you want to send the order's information to the sales tax reporting API, so you can properly report your sales tax at the end of the quarter.
The event bus is the perfect fit for this use case, so you write the following event subscriber:
taxation/order:subscriber.rb
taxation/order:subscriber:spec.rb
app/subscribers/awesome:store/taxation/order:subscriber.rb
1
module AwesomeStore::Taxation::OrderSubscriber
2
include Spree::Event::Subscriber
3
4
event_action :report_order_tax, event_name: 'order_finalized'
5
6
def report_order_tax(payload)
7
# send the order information to a sales tax API
8
end
9
end
Copied!
spec/subscribers/awesome:store/taxation/order:subscriber:spec.rb
1
RSpec.describe AwesomeStore::Taxation::OrderSubscriber do
2
describe 'on order_finalized' do
3
it 'sends the order to the taxation API' do
4
order = build_stubbed(:order)
5
6
Spree::Event.fire 'order_finalized', order: order
7
8
# verify the order has been sent to the API
9
end
10
end
11
end
Copied!
Everything works well. Then, one day, you get a second requirement: when an order is finalized, you also need to send its information to an external 3PL API, so that it can be shipped to customers.
Given that the logic is very similar, you just write another subscriber:
fulfillment/order:subscriber.rb
fulfillment/order:subscriber:spec.rb
app/subscribers/awesome:store/fulfillment/order:subscriber.rb
1
module AwesomeStore::Fulfillment::OrderSubscriber
2
include Spree::Event::Subscriber
3
4
event_action :send_order_to_3pl, event_name: 'order_finalized'
5
6
def send_order_to_3pl(payload)
7
# send the order information to a 3PL API
8
end
9
end
Copied!
spec/subscribers/awesome:store/fulfillment/order:subscriber:spec.rb
1
RSpec.describe AwesomeStore::Fulfillment::OrderSubscriber do
2
describe 'on order_finalized' do
3
it 'sends the order to the 3PL API' do
4
order = build_stubbed(:order)
5
6
Spree::Event.fire 'order_finalized', order: order
7
8
# verify the order has been sent to the API
9
end
10
end
11
end
Copied!
This looks good at a superficial glance, but there's a problem: instead of just running the subscriber under test, both tests will now run both subscribers! This makes your tests slower and harder to debug when a subscriber fails, and it may also force you to duplicate any setup logic for one subscriber to in all the other subscribers that listen to the same event.
To solve the problem, you can add the following helper to your RSpec configuration:
spec/support/subscriber:helpers.rb
1
module TestingSupport
2
module SubscriberHelpers
3
def perform_subscribers(only: [])
4
Spree::Config.events.subscriber_registry.deactivate_all_subscribers
5
6
Array(only).each(&:activate)
7
8
yield
9
ensure
10
reinitialize_subscribers(RSpec.current_example)
11
end
12
13
private
14
15
def reinitialize_subscribers(example)
16
Spree::Config.events.subscriber_registry.deactivate_all_subscribers
17
18
if example.metadata[:type].in?(%i[system feature request])
19
Spree::Config.events.subscriber_registry.activate_all_subscribers
20
end
21
end
22
end
23
end
24
25
RSpec.configure do |config|
26
config.include Helpers::Subscribers
27
config.before do |example|
28
reinitialize_subscribers(example)
29
end
30
end
Copied!
This helper is in the process of being ported to Solidus, so you can simply include it like you do for all the other helpers. In the meantime, though, you'll have to do some old-fashioned copy-pasting if you want to use it!
This snippet will deactivate all subscribers when running your unit tests, except for the ones you explicitly enable via the perform_subscribers helper. Subscribers will still be active as usual in your system and request specs, so that you make sure your application works well in integration.
Here's how you can use it:
taxation/order:subscriber:spec.rb
fulfillment/order:subscriber:spec.rb
spec/subscribers/awesome:store/taxation/order:subscriber:spec.rb
1
RSpec.describe AwesomeStore::Taxation::OrderSubscriber do
2
describe 'on order_finalized' do
3
it 'sends the order to the taxation API' do
4
order = build_stubbed(:order)
5
6
perform_subscribers(only: described_class) do
7
Spree::Event.fire 'order_finalized', order: order
8
end
9
10
# verify the order has been sent to the API
11
end
12
end
13
end
Copied!
spec/subscribers/awesome:store/fulfillment/order:subscriber:spec.rb
1
RSpec.describe AwesomeStore::Fulfillment::OrderSubscriber do
2
describe 'on order_finalized' do
3
it 'sends the order to the 3PL API' do
4
order = build_stubbed(:order)
5
6
perform_subscribers(only: described_class) do
7
Spree::Event.fire 'order_finalized', order: order
8
end
9
10
# verify the order has been sent to the API
11
end
12
end
13
end
Copied!
This will make sure only the subscriber under test is executed when the order_finalized event is fired. As a result, your subscriber test is now fully isolated!
Last modified 3d ago