Testing with Capybara
Capybara Workflow
Capybara is a great tool, but what do you actually do with it?
Acceptance & Feature Tests
We use the terms "acceptance tests" and "feature tests" to mean almost the same thing – with a nuanced difference.
Acceptance Tests are for End Users
Acceptance tests exercise your application just like a real user. They therefore depend on the full stack from your models up through your controllers, helpers, view templates, web server, database, and middleware.
Acceptance tests are typically a translation of a user story:
1 2 3 4 5 |
|
Proper acceptance tests use your application as a black box. They know nothing about what happens under the hood, they just interact with the interface and observe the results.
Acceptance tests are the 30,000-foot view of your application. They’re focused on the happy-path, the business value driving the creation of the application.
When your user stories have been translated to acceptance tests and those tests pass, then your application is finished! Until they write more stories…
Feature Tests are for Clients & Developers
But there are often a lot of details a little bit under the surface. What about when things go wrong? What happens when a user tries to save an article without a title? Or a title that’s too short?
While we might write just a small number of acceptance tests or even just one for a user story, we can use several feature tests to drive more of the development.
Breaking Down an Example
Let’s talk about creating an article on our blog.
Acceptance Test
The acceptance test might go like this:
1 2 3 4 5 6 7 8 |
|
That’s the happy path. Translated to Minitest and Capybara, that might go like this:
1 2 3 4 5 6 7 8 9 10 |
|
Feature Tests
The feature tests drill down just a bit deeper:
Create and Redirect
The concept:
1 2 3 4 5 6 |
|
Implemented in Capybara:
1 2 3 4 5 6 7 8 9 |
|
Body is Required
Conceptually…
1 2 3 4 5 6 7 |
|
Implemented with Capybara:
1 2 3 4 5 6 7 8 9 |
|
Titles Are Unique
Conceptually:
1 2 3 4 5 6 7 8 |
|
Implemented with Capybara:
1 2 3 4 5 6 7 8 9 10 |
|
Maintaining the Veil
The goal of both acceptance and feature tests is to know very little about your application. If they need some data to be in the app, the test needs to create that data first.
But that can be a real pain, not to mention slow. Imagine you’re writing a suite of tests focused on the admin functionality in your appliction. If you keep your suite conceptually pure, then every test needs to:
- Register a new user
- Confirm a user
- Upgrade the user to an administrator (how does this even happen?)
Each of those happens before you get to the meat of the test. You could easily wait a long time for all that to complete, end up with a slow test suite, and either (A) waste development time or (B) stop running the tests. Both are bad.
Making Compromises
When you do these kinds of tests you’ll have to make compromises. But you want to encapsulate those cheats so you can pull them out later.
In the examples above, we used this method:
1
|
|
You can assume what that method does, but how does it do it? From the test, you don’t care. By abstracting the steps for that method, the test can maintain a consistent level of abstraction from the application’s implementation.
Inside the method, it very well might do something like this:
1 2 3 4 5 6 7 8 9 10 11 |
|
But that’s slow, right? You could also implement it in a way that reaches directly into the application:
1 2 3 4 |
|
Then you’d setup the controller who responds to login_path
to allow the username and password to be passed in as URL parameters. You should only allow this in non-production environments because it’s a potential security hole.
Do you really know that user registration is working properly? You should have tests elsewhere that examine just that functionality. But you have the continuous integration server who’s time doesn’t matter. Implement the method to use the slow approach on CI:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
|
Outside-In Testing
Let’s continue the example from above where you’ve implemented an acceptance test about posting an article. You run it and it fails.
A Feature Test
You drop down a level to the feature tests and write this test (from above):
1 2 3 4 5 6 7 8 9 |
|
You run it and it fails, too. To focus, you go back to the acceptance test and mark it as skip
, then re-run your suite and see just this feature test failing.
Dive Inside the App
Now you change gears and step inside the application. How do you pass the test? You need:
- a
new_article_path
that displays a form - that form to have
title
andbody
text fields and asave
button - a redirect to the
show
action for that article
What don’t you need? This feature test doesn’t actually compel you to store the data from the form!
Let’s dive down to the controller tests.
Testing the Controller
We don’t believe in controller tests specifying much about content, but to focus instead on functionality.
You could, for instance, write a controller test that specifies the new
action renders a form
partial. But why? What does it really help you learn about the design of the application? Nothing.
Your feature test is failing because the controller doesn’t exist or doesn’t have a new
action. You go ahead and implement it:
1 2 3 4 5 |
|
Then the test fails because the view template is missing. Go ahead and implement that template with a form:
1 2 3 4 5 |
|
And now, finally, the feature test fails because the create
action doesn’t exist to receive the form data.
It’s time for a controller test. In test/controller/articles_controller_test.rb
:
1 2 3 4 5 6 |
|
Check out the Rails Testing Documentation for more details on what you can do in a controller test. This one submits a post request with some parameters, then checks that it gets redirected. The assigns(:article)
checks that the action defines a variable article
and fetches it in order to build the URL.
Write a Unit/Model Test
Repeat and Bubble-Up
Write the Next Feature Test
Repeat.