5. How to Test?
The Problem with Testing
A very common subject in software development is testing.
However, it is not uncommon to see production-ready projects out there that either have a broken test suite that nobody takes care of or just have no tests whatsoever. In this episode I want to share with you my thoughts on tests, clarify some questions I had when I started testing and hopefully help you if you're working on a project that needs some help with tests.
How to NOT do it
When I started writing tests in good old Rspec I made the mistake of thinking I needed to test everything.
So I wrote tests for literally everything. Every edge case was covered by a test. If I had an
app/models/customer.rb file, I certainly had a
spec/models/customer_spec.rb testing all methods defined
Customer. In several cases, testing even the ActiveRecord validations I defined, for example:
# spec/models/customer_spec.rb describe Customer do it 'requires first_name and last_name' do customer = Customer.new expect(customer).to_not be_valid expect(customer.errors[:first_name]).to_not be_empty expect(customer.errors[:last_name]).to_not be_empty end describe '#name' do it 'joins first_name and last_name' do customer = Customer.new(first_name: 'Diego', last_name: 'Selzlein') expect(customer.name).to eq('Diego Selzlein') end end end
Don't do this. There are scenarios where unit tests like these are really important, like if you're working with
a gem, but for most web apps that I've seen, this is completely unnecessary. We don't need to test ActiveRecord
name method should be covered by other kind of tests that I'll show you soon.
Writing too many tiny detailed tests can easily turn up to be a burden to maintain. You will end up with a huge test suite that tests almost every method you have. You do not need to test everything.
If you do that, there will be a time you will need to move a method around, maybe into another class, and then certainly one (but probably several) tests will start failing. The same happens when you rename a method or a class. Since your highly detailed tests are tightly coupled with your implementation, changing the implementation will almost always mean having to change test code too.
Let me give you an example. Suppose the
show page uses the
name method like this:
<%# app/views/customers/show.html.erb %> <h1>Customer: <%= @customer.name %></h1>
Then you decide to rename
full_name. Besides the model and the ERB template that uses it,
you will have to change your test too! And this adds up as your test suite grows. The more test code you have
the slower it gets to change your code.
And then, after being frustrated by a test suite that takes more time to maintain than saves in development, developers tend to just ignore it and go back to the Go Horse methodology: if it compiles, it can be shipped.
I've been there, but fortunately I didn't give up on tests. Some time ago I learned how to create and maintain a powerful and really useful test suite. This is what I want to share with you today.
Automate your Development Process
Before I dive into that, I want you to think about your development process without tests. I bet it's something along these lines:
- You create a feature for your web app
- Then open your browser and use your feature to see if it's working as expected
- Then a change needs to happen to the code, being either you improving it, cleaning it up or just fixing a bug
- Now you go back to step 2 to test your feature again
By following this, you are manually testing it.
Now, don't you feel like doing that over and over again kinda silly? It's a repetitive procedure of pointing your browser to a particular page, doing something there and checking if it works. Couldn't you just write a piece of code that does that for you? We are in the automation business after all, why not automate part of our work as developers too?
I think many people don't write tests because they take time to be created, but they don't realize they spend way longer following this manual process than they would by just writing a simple test to do it for them.
High Level Feature Tests (BDD)
Now to the fun part.
There is one kind of test that does not tie up your implementation details: a feature test. A feature test is high level and tests your application as the user would, treating it like a black box, not caring about internal classes or methods. In Rspec you've probably written tests like this using Capybara.
With Capybara you can automate your browser to, for example, open up a page, fill in a form and submit it. Then
you check the database to make sure the proper changes have been persisted. Let's write a feature test for
Customer I mentioned earlier:
# spec/features/customer_spec.rb describe Customer do scenario 'Creating a new customer' do visit new_customer_path fill_in 'First Name', with: 'Diego' fill_in 'Last Name', with: 'Selzlein' click_on 'Save' expect(page).to have_content('Diego Selzlein') customer = Customer.last expect(customer.first_name).to eq('Diego') expect(customer.last_name).to eq('Selzlein') end scenario 'Trying to create an invalid customer' do visit new_customer_path click_on 'Save' expect(page).to have_content('First Name is required') expect(page).to have_content('Last Name is required') end end
The first scenario creates a customer with valid data, makes sure it got persisted and also checks if the full name is being displayed after saving. The second scenario makes sure that validations are working and the user can see the error messages being displayed.
To me this is the perfect kind of test. It does not know anything about the implementation details. You can
rename the controller, the action, move code around or what have you, that test is going to keep passing.
If submitting the form still results in a new
Customer record and the full name on the page, that first test will
be green. It is making sure your app is not breaking the contract without coupling too much with the
lower level code. What matters is what, not how.
Notice we are not testing the
name method anymore, at least not directly. As you can see, after creating
a new customer we check if the page we're redirected to shows the full name properly. There's no need to test that in isolation.
Now let's again pretend we want to rename the
name method to
full_name. We have to change the model, the ERB
template, aaaand... that's it! Our test code does not mention that method whatsoever, but we're still making sure
it's working because we're checking the contents of the page.
Also notice that this is really just automating the manual work of testing described earlier. It's simple, easy to write, and makes the development process a lot faster because you don't need to manually check this stuff anymore as you change your code.
I think this is the closest you get to Behavior Driven Development or BDD with Rspec, in which you describe the behavior of the system being built instead of how its inner classes and roles will respond to method calls. In the next episode I want to dive more into BDD in Rails the way I like to use it.
And before I go, what do you think about this? How do you test? Please leave a comment below.
Thank you for watching and I hope you enjoyed this snack!