End-to-End Testing React Apps
Long live RSpec
The Rails monolith is considered by some to be a classic go-to, by others to be passé. But if your app uses a trendy front end framework such as React or Vue.JS to talk to a API, chances are you’re not writing proper end-to-end tests.
Newer JavaScript testing tools, such as Enzyme and Jest. create abstractions that allow developers to test the “view” layer. But testing from the outside-in means that before we reach in and start testing internal components, we start with end-to-end tests: tests that simulate what it’s like to be a user of your app.
End-to-end tests are the most important class of tests that we have, as they test the functionality that our clients and customers expect from the products that we build. In fact, prematurely writing low-level unit and integration tests for internal components can be counterproductive, especially for parts of your app that you are iterating on, as they make refactoring and restructuring more taxing.
The Beauty of End to End tests
We’d like to have tests that look less like this:
const mockTryGetValue = jest.fn(() => false);
const mockTrySetValue = jest.fn();
jest.mock('save-to-storage', () => ({
SaveToStorage: jest.fn().mockImplementation(() => ({
tryGetValue: mockTryGetValue,
trySetValue: mockTrySetValue,
})),
}));
describe('MyComponent', () => {
it('should set storage on save button click', () => {
mockTryGetValue.mockReturnValueOnce(true);
const component = mount(<MyComponent />);
component.find('button#my-button-three').simulate('click');
expect(mockTryGetValue).toHaveBeenCalled();
expect(component).toMatchSnapshot();
component.unmount();
});
});
And more like this:
visit "/my-item/"
click_on "Edit"
expect(page).to have_content("Editing My Item")
By testing our JavaScript web app with RSpec, we rely on a robust library with a simple, readable DSL that has been battle tested for years. By implementing end-to-end test coverage, our developers are able to refactor and rewrite the internals of our app at will with confidence that our app is performing as expected, increasing both stability and in the long run, velocity.
Introduce Ruby to your Repo
Open your React/Vue/SPA repository and follow the instructions below:
$ bundle init
We will need the following packages to get started:
Let’s go ahead and install them!
$ bundle
Rspec
Now initialize rspec
$ rspec --init
Now running rspec should show basically nothing:
$ rspec
Sinatra
Next, let’s set up a simple Sinatra app, which loads our JavaScript app.
This file should be essentially a copy of your index.html
file.
The following example assumes:
- Your React app is compiling to
/dist/app.js
. - It injects into
#app
.
Obviously, update the code below to match your app if necessary.
Routing
Now we add basic routing for our Sinatra app.
Note: If you don’t use hashUrls for routing, you may have to work a bit harder on this file.
Configuring RSpec
Now we set up RSpec to initialize and run our Sinatra app, using a configuration of Chrome that will proxy cache any API calls through a gem called Puffing Billy.
You can read more about using Puffing Billy in their documentation.
Write your first test
We’re ready to finally write our first test case:
Type
$ rspec
to run your tests.
What about my API calls?
If you’ve used RSpec before, you’re probably used to using something like VCR for recording and playing back external http calls to APIs and other services.
That won’t work here, because the calls aren’t coming from a backend client like Rails–they’re AJAX calls coming from the browser.
Instead, we’re using a library called Puffing Billy.
The first time you run the test, Puffing Billy will make calls to your actual
API, and cache the responses in spec/fixtures/feature
.
To prevent it from sending and caching requests without your intent, set the
non_whitelisted_requests_disabled
configuration parameter to true
:
# spec/spec_helper.rb
Billy.configure do |config|
...
config.non_whitelisted_requests_disabled = true
...
end
Other concerns
Fixtures
One major difference between this setup and a traditional Rails/RSpec setup is that you can’t generate fixtures on the fly.
For example, in a traditional Rails/RSpec setup, I might use FactoryBot
to
create test fixtures.
If you’re interacting with an API seated in a separate repo, you don’t have this ability, because you cannot access the backend from your test code. So you need to take some care to set up test fixtures remotely.
Your setup may differ, or you may choose to use your staging, QA, or production server to generate test API responses. Personally I use mockable.io, and setup my test code to use corresponding endpoints from that origin. Setting your app up to use mockable is beyond the scope of this article.
Other setups
Some setups may not take kindly to using Sinatra, such as certain apps using
websockets or GraphQL. In these cases, you can write scripts to manually
start/stop your app in the background before running tests. Then use your apps
actual development URL as the base_url
in your RSpec tests.
In some cases you may wish to hit actual endpoints i.e. a dedicated test server. In this case you can ignore the Puffing Billy parts of this document.
That’s all folks!
Thanks for reading! I hope this changes your life.