Automated system testing with Rspec in a Ruby on Rails app :: Full-stack web development "Hello World" tutorials for entrepreneurs: Part 10 of 10
This post is the final part of a 10-part series within a series that is designed to teach full-stack web development for entrepreneurs. Here we’ll introduce the topic of automated testing with Rspec within a Ruby on Rails app. We’ll work through writing “system tests” as an example, which allows us to mimic the behavior of manually clicking through a browser and filling things out. Our system tests will be used to ensure our previously developed forms in Rails, React, and Turbo won’t break with future code commits.
RSpec
Up until this point, after we’ve made changes to our code, we’ve had to manually fire up our Rails server and test that our app still works by clicking things, creating quotes on all our forms, deleting quotes, etc… As you can imagine, as we write real-world applications this process of manual testing doesn’t scale very well. We want the ability to automate the process of visiting pages, clicking links, filling out forms, etc… This is where automated testing comes in, and we’ll be using RSpec within our Rails app to do so.
Let’s dive right in by opening our Gemfile and on a new line 33 add:
gem 'rspec-rails', '~> 4.0.2'
We are adding this within our “group :development, :test” because we don’t need Rspec built for our production environment. We only need it when developing and testing our application.
Now go ahead and:
- bundle install
- rails g rspec:install
This sets up the rspec/ folder and initial config files.
Let’s go ahead and delete the entire test folder in the base of our Rails app, since that folder is for the default Rails Minitest suite that we are not going to use. Moving forward the “spec” folder will be our test folder.
Now, before we get into writing Rspec code our app, let’s go in and modify a few files to make the process easier for our test suite to find certain elements and avoid showing us various errors and warnings.
First, in our app>javascript>react_quotes_app>quotes_view.js file let’s add this ID to our button on line 73:
<button id="RQA_quote_submit" type="submit">Create Quote</button>
and on line 81 add an ID to our list like this:
<ul id="RQA_quotes_list">
Next, on our app>models>quote.rb file, within line 19 we can remove the “.html.erb” extension to our file partial:
partial: 'welcome/quote'
And in our app>views>welcome>_quotes_form.html.erb file let’s add in IDs to our elements like this:
<%= form_with model: @quote, url: '/create-quote', id: "#{prefix}rails_quote_form", data: {controller: 'hello', action: 'turbo:submit-end->hello#reset'} do |f| %>
Quote: <br /> <%= f.text_area :content, required: true, id: "#{prefix}quote_content" %> <br />
Author: <br /> <%= f.text_field :author_name, required: true, id: "#{prefix}quote_author_name" %>
<br /><br />
<%= f.submit "Create Quote", id: "#{prefix}create_quote_submit" %>
<% end %>
This allows our automated tests to fill out the right forms (and it’s best practice not to have two elements in the DOM have the same ID anyway)
Also, in our app>views>welcome>index.html.erb file let’s wrap line 24 in a conditional, since when our test suite runs it won’t have any quotes in our database to show:
<% if quote.present? %>
<p>"<%= quote.content %>" -<%= quote.author_name %></p>
<% end %>
Finally, in our config>environments>test.rb file we need to activate caching to make StimulusReflect happy, so starting on line 29 make it read:
# config.action_controller.perform_caching = false
# config.cache_store = :null_store
config.action_controller.perform_caching = true
config.action_controller.enable_fragment_cache_logging = true
config.cache_store = :memory_store
config.public_file_server.headers = {
'Cache-Control' => "public, max-age=#{2.days.to_i}"
}
That’s all we need for some initial setup tweaks.
Now, let’s create a folder in rspec/ called “system” and in it put a file called example_app_spec.rb with the following to start testing our initial JavaScript button at the top of our example app:
require "rails_helper"
RSpec.describe 'Example App' do
describe 'JavaScript Hello World Button' do
it 'Appends "Hello World!" to hello_world_list' do
visit '/'
expect(find('#hello_world_list')).to_not have_text('Hello World!')
find('#click_me_button').click
expect(find('#hello_world_list')).to have_text('Hello World!')
end
end
end
Save the file and let’s go ahead and:
- bundle exec rspec
You should see a Chrome browser spin up, automatically test that “Hello World!” doesn’t exist in that list, click the button, and detect that “Hello World!” indeed shows up.
Rspec will be happy and tell you that your test has passed with a friendly green font.
Good. Let’s keep going. Before the final “end” in that file, let’s add the following to test our Stimulus button:
describe 'Stimulus Hello World Button' do
it 'Appends "Hello World from a Stimulus controller" to the hello.list' do
visit '/'
expect(find('ul[data-target="hello.list"]')).to_not have_text('Hello World from a Stimulus controller')
find('button[data-action="hello#addHelloWorld"]').click
expect(find('ul[data-target="hello.list"]')).to have_text('Hello World from a Stimulus controller')
end
end
Save the file and let’s go ahead and again:
- bundle exec rspec
You’ll see the two tests passing now.
For the last three sets of tests we are going to fill out each of our forms, submit them to create quotes, check that the quote showed up in all our lists, click on the quote, and then check that the quote got deleted from all our lists.
Go ahead and copy/paste in the following, and we’ll discuss below what’s going on here:
describe 'Rails Quotes' do
it 'Prepends and deletes a quote from all lists' do
visit '/'
fill_in 'quote_content', with: 'Hello Rails World'
fill_in 'quote_author_name', with: 'John Doe'
find('#create_quote_submit').click
expect(find('#quotes_list')).to have_text('Hello Rails World')
expect(find('#RQA_quotes_list')).to have_text('Hello Rails World')
expect(find('#quotes_list_turbo')).to have_text('Hello Rails World')
find('#quotes_list > li:nth-child(1)').click
expect(find('#quotes_list')).to_not have_text('Hello Rails World')
expect(find('#RQA_quotes_list', visible: false)).to_not have_text('Hello Rails World')
expect(find('#quotes_list_turbo')).to_not have_text('Hello Rails World')
end
end
describe 'React Quotes' do
it 'Prepends and deletes a quote' do
visit '/'
fill_in 'RQA_quote_content', with: 'Hello React World'
fill_in 'RQA_quote_author_name', with: 'John Doe'
find('#RQA_quote_submit').click
expect(find('#quotes_list')).to have_text('Hello React World')
expect(find('#RQA_quotes_list')).to have_text('Hello React World')
expect(find('#quotes_list_turbo')).to have_text('Hello React World')
find('#RQA_quotes_list > li:nth-child(1)').click
expect(find('#quotes_list')).to_not have_text('Hello React World')
expect(find('#RQA_quotes_list', visible: false)).to_not have_text('Hello React World')
expect(find('#quotes_list_turbo')).to_not have_text('Hello React World')
end
end
describe 'Turbo Quotes' do
it 'Prepends and deletes a quote' do
visit '/'
fill_in 'turbo-quote_content', with: 'Hello Turbo World'
fill_in 'turbo-quote_author_name', with: 'John Doe'
find('#turbo-create_quote_submit').click
expect(find('#quotes_list')).to have_text('Hello Turbo World')
expect(find('#RQA_quotes_list')).to have_text('Hello Turbo World')
expect(find('#quotes_list_turbo')).to have_text('Hello Turbo World')
find('#quotes_list_turbo > li:nth-child(1)').click
expect(find('#quotes_list')).to_not have_text('Hello Turbo World')
expect(find('#RQA_quotes_list', visible: false)).to_not have_text('Hello Turbo World')
expect(find('#quotes_list_turbo')).to_not have_text('Hello Turbo World')
end
end
When we write something like fill_in 'quote_content' or find('#create_quote_submit') we are using “selectors” to pick out elements in the DOM in order to do something to them or check something about them.
Also, when we write find('#quotes_list > li:nth-child(1)').click, what we are doing is telling our test suite to find the first “child” element inside our quotes_list element. This is yet another example of a selector.
Practically, when writing system tests you write code to do all the things you would do manually in the browser anyway. This can not only save time when developing, but is extremely helpful to ensure future code you or your team commits doesn't break existing functionality.
Let’s go ahead and do a final:
- bundle exec rspec
You’ll see the browser do a lot more action automatically now, and then show:
Those two “Nothing Morphs” notifications are printed into the console when our automated tests click on our Rails and Turbo quotes to delete them. If you recall, our React quotes use a totally different mechanism to remove the quotes, which is why there are only two of these notifications and not three.
Now, above we just did an example of a handful of system tests, but there are a wide variety of other types of tests to ensure different parts of the application perform correctly. Testing nomenclature can be tricky, but what we just did above can be called “acceptance testing”, “end-to-end testing”, or “feature testing”; these types of tests mimic user behavior.
However, there are a totally different set of tests that are meant to ensure different “units” of your application work in isolation (often referred to as “unit tests”). In the Rails/Rspec world, these are called model tests, controller tests, mailer tests, etc… Click here to see the full list.
As we go along in this tutorial series, we’ll actually be writing many of our tests in advance of writing the code. This is called test-driven development (TDD), or behavior-driven development (BDD) in the case of system tests that test behavior. When done correctly, TDD/BDD can actually speed up development since you don’t have to manually check your code in a browser all the time.
For now, let’s go ahead and run prettier, ESLit, and Rubocop.
First, open up your .prettierignore and .eslintignore files and add to the bottom of each:
public/packs-test
Then we can go ahead and
- yarn prettier --write .
- yarn run eslint .
- rubocop -A
- rubocop
Rubocop was able to correct 29 of our 30 offenses, but the one thing it didn’t like was the length of our block in our spec file. We could refactor to make each part our our system test it’s own block, but it’s OK as is. So, on line 5 of our spec>system>example_app_spec.rb file let’s make it read:
RSpec.describe 'Example App' do # rubocop:disable Metrics/BlockLength
And then when you run rubocop again it will tell you there are no offenses detected.
Finally, let’s go ahead and run “bundle exec rspec” one more time to make sure our linters/formatter didn’t break anything (it shouldn’t).
Then we can review our code per usual, which should look like this commit, and then:
- git add .
- git commit -a -m "add Rspec to test our example app"
- git push
And that’s it!
This finishes off our 10-part series within the larger series teaching technical founder training.
Next, we’ll dive into building the actual app that we’ll walk through step-by-step together: a quotes app that lets us capture notable quotes from our friends/family and share with others. Subscribe here to stay posted when I get the next tutorial up.
As always, feel free to reach out to me at will@wclittle.com with any questions. Thanks!