Testing Solidus
This guide will teach you how to test different aspects of your Solidus application by leveraging the best practices and tooling provided by 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 its 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.
Testing Solidus with Solidus Starter Frontend installed
In case you installed Solidus selecting the Starter Frontend as storefront, you will notice that
you have some specs already in place. You can find them in the spec
folder of your application.
Most of the steps covered in the rest of the guide are already done for you, but it's still a good idea to read through the guide to understand how your test suite is configured and the reasons behind the choices we made.
Please note that the main entry point for the configuration provided by the Starter Frontend is placed
at spec/solidus_starter_frontend_spec_helper.rb
instead of spec/spec_helper.rb
and spec/rails_helper.rb
as a regular Rails application would do. That's because we wanted to still let the freedom to build your
own test suite, independent from the one we provide.
Configuring your test environment from scratch
If you didn't use the Starter Frontend and want to start from scratch, here's the bare minimum you'll need to get started with testing your Solidus app:
group :development, :test do
gem 'rspec-rails'
gem 'factory_bot_rails', '~> 4.8'
end
group :test do
gem 'capybara', '>= 3.26'
end
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:
$ rails g rspec:install
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:
Dir[Rails.root.join('spec', 'support', '**', '*.rb')].sort.each { |f| require f }
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:
module AmazingStore
class Application < Rails::Application
# ...
if defined?(FactoryBotRails)
initializer after: "factory_bot.set_factory_paths" do
require 'spree/testing_support/factory_bot'
# The paths for Solidus' core factories.
solidus_paths = Spree::TestingSupport::FactoryBot.definition_file_paths
# Optional: Any factories you want to require from extensions.
extension_paths = [
# MySolidusExtension::Engine.root.join("lib/my_solidus_extension/testing_support/factories"),
# or individually:
# MySolidusExtension::Engine.root.join("lib/my_solidus_extension/testing_support/factories/resource.rb"),
]
# Your application's own factories.
app_paths = [
Rails.root.join('spec/factories'),
]
FactoryBot.definition_file_paths = solidus_paths + extension_paths + app_paths
end
end
end
end
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
:
RSpec.configure do |config|
config.include FactoryBot::Syntax::Methods
end
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:
RSpec.configure do |config|
config.before(:each, type: :system) do |example|
if example.metadata[:js]
driven_by :selenium_chrome_headless
else
driven_by :rack_test
end
end
end
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:
RSpec.describe "Product page", type: :system do
it "shows the product's description" do
visit "/products/solidus-shirt"
expect(page).to have_text("Solidus-branded T-shirt")
end
it "allows me to add a product to my cart", :js do
visit "/products/solidus-shirt"
click_button "Add to Cart"
expect(page).to have_text("Product was added to the cart!")
end
end
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:
# ...
require 'spree/testing_support/authorization_helpers'
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:
RSpec.describe 'The product admin' do
stub_authorization!
it 'allows me to view the products' do
product = create(:product)
visit spree.admin_path
click_link 'Products'
expect(page).to have_content(product.name)
end
end
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:
RSpec.describe 'The product admin' do
custom_authorization! do
can :read, Spree::Product
end
it 'allows me to view the products' do
sign_in create(:user)
product = create(:product)
visit spree.admin_path
click_link 'Products'
expect(page).to have_content(product.name)
end
it 'does not allow me to edit products' do
sign_in create(:user)
product = create(:product)
visit spree.edit_admin_product_path(product)
expect(page).to have_content('Access denied')
end
end
(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:
# ...
require 'spree/testing_support/capybara_ext'
Interacting with icons
You can use the click_icon
helper to find and click on a specific FontAwesome icon:
RSpec.describe 'The option types admin' do
it 'allows me to delete the option types' do
sign_in create(:admin_user)
option_type = create(:option_type)
visit spree.admin_path
click_link 'Products'
click_link 'Option Types'
click_icon 'trash'
expect(page).to have_content('has been successfully removed')
end
end
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:
RSpec.describe 'The product admin' do
it 'allows me to edit a product' do
sign_in create(:admin_user)
product1 = create(:product)
product2 = create(:product)
visit spree.admin_products_path
# Clicks the edit icon for the first product
within_row(1) { click_icon 'edit' }
# ...
end
end
The column_text
helper can be used to retrieve the text from a specific column in a table row:
RSpec.describe 'The product admin' do
it 'displays the product SKU' do
sign_in create(:admin_user)
product1 = create(:product)
product2 = create(:product)
visit spree.admin_products_path
within_row(1) do
expect(column_text(1)).to eq(product1.sku)
end
end
end
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):
RSpec.describe 'The orders admin' do
it 'allows me to filter by variant' do
sign_in create(:admin_user)
product = create(:product)
order = create(:order) do |o|
create(:line_item, order: o, variant: product.master)
end
visit spree.admin_orders_path
select2_search product.sku, from: 'Variant'
click_button 'Filter results'
expect(page).to have_content(order.number)
end
end
Testing meta tags
You can use the have_meta
helper to expect the current page to have a specific meta tag:
RSpec.describe 'The product page' do
it 'uses the product description in the meta description' do
product = create(:product)
visit spree.product_path(product)
expect(page).to have_meta(:description, product.description)
end
end
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:
# ...
require 'spree/testing_support/order_walkthrough'
You can now use the helper whenever you want:
RSpec.describe 'The checkout flow' do
it 'renders the delivery step' do
user = create(:user)
sign_in user
# Generate an order in the `delivery` state
order = Spree::TestingSupport::OrderWalkthrough.up_to(:address)
order.update!(user: user)
visit spree.checkout_path
expect(page).to have_current_path('/checkout/delivery')
end
end
The OrderWalkthrough.up_to
call will create a new order and it will simulate what a user would do
if they went 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:
# ...
require 'spree/testing_support/preferences'
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:
RSpec.describe 'The product page' do
it 'renders the price in EUR' do
# Stub the global `currency` setting of the store
stub_spree_preferences(currency: 'EUR')
product = create(:product)
visit spree.product_path(product)
expect(page).to have_content('€100,00')
end
it 'renders the price in USD' do
# Stub the global `currency` setting of the store
stub_spree_preferences(currency: 'USD')
product = create(:product)
visit spree.product_path(product)
expect(page).to have_content('$100.00')
end
end
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:
RSpec.describe 'The backend' do
it 'is available in English' do
# Stub the `locale` setting of the backend
stub_spree_preferences(Spree::Backend::Config, locale: 'en')
visit spree.admin_path
expect(page).to have_content('Email')
end
it 'is available in French' do
# Stub the `locale` setting of the backend
stub_spree_preferences(Spree::Backend::Config, locale: 'fr')
visit spree.admin_path
expect(page).to have_content('Courriel')
end
end
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:
# ...
require 'spree/testing_support/url_helpers'
RSpec.configure do |config|
config.include Spree::TestingSupport::UrlHelpers
end
You can now access the helpers via the spree.
shortcut:
RSpec.describe 'The product page' do
it 'is accessible' do
product = create(:product)
visit spree.product_path(product)
expect(page).to have_content(product.name)
end
end
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-driven behavior is an awesome way to decouple orthogonal logic, but it can be tricky to test. Take a look at the section about how to test event subscribers to see how to do it in Solidus.