Menu Navigation Menu

Spend any time in the Ruby and Rails world and you’ve probably at least heard of test-driven development (TDD), and testing in general. Testing is a ubiquitous concept. Popular tutorials (like Michael Hartl’s Ruby on Rails tutorial) have TDD baked in, and newcomers are encouraged to adopt the practice.

But, testing Rails apps can be challenging.

  • You might feel like you're testing too much, or that you're not testing enough.
  • You might feel like you're spending more time testing than writing functionality.
  • You might be stuck on TDDing because you're unsure what your object's interface should look like.
  • Worst case, you might feel like you're a bad developer if you don't TDD.

Fear not! It’s more than possible to have both well-tested code and an enjoyable workflow that leaves you feeling confident with your code.

I’ll show you the roadmap I use regularly as a guide when I’m writing Rails apps. It overviews what parts of your Rails app ought to be covered with tests. I’ll also share the techniques I use in my tests to ensure a good level of coverage while at the same time feeling the joy of programming as often as possible.

Disclaimer: Depending on your needs, your mileage may vary with the following suggestions. I hope you learn something either way.

Note: I use the terms “test” and “spec” interchangeably.

What is Test-Driven Development (TDD)?

First, we must make the distinction between TDD and automated tests in general.

Tests, in general, can be derived from TDD or written after-the-fact. Either way, a test suite that covers the majority of code paths within your app has a couple of huge benefits:

  1. You can change code with a high degree of confidence that you won’t introduce bugs.
  2. It documents your code.

TDD, as you probably know, is a workflow where tests are written first and drive codebase development and design. The main benefit of TDD is that it enables a tight feedback loop when you’re developing (the red-green-refactor cycle). In addition to being enjoyable, this helps to keep the cost of changing your code constant.

Though TDD isn’t a must, I find it useful because:

  1. It pushes me to achieve a higher level of test coverage than I would’ve had otherwise. This is important because on the whole, a higher test coverage means a lower incidence of bugs.
  2. I enjoy the process once I’m in the groove.

That being said, I don’t always start with TDD.

When to Use TDD

  1. If I’m changing the behavior of an existing feature, then I TDD right away (assuming the code has tests to begin with)
  2. If the code doesn’t have tests, then I introduce test coverage before changing the code. This can be a chicken & egg problem sometimes, because you might need to change the code to introduce tests.
  3. When I’m fixing a bug, I write a test to prove the failure before I go in and fix it.
  4. If I’m unsure how to implement something, I first develop working code without tests. Then I throw this away, and restart development with TDD. This way I know what has to be done, but can work out my design with tests leading the way.

Roadmap for Testing Rails Apps

Install & Setup simplecov

You need a way to assess test coverage in your system. If you don’t have one already, I recommend installing the simplecov gem. I’d also recommend running a coverage tool as part of your CI process.

Set up a key command to quickly run the test you’re working on

You will likely find it beneficial as you’re writing tests to get immediate feedback. A key command like \q or Enter works nicely for this purpose.

Acceptance Tests

Tests involving the browser are typically slow. At the same time, without acceptance tests, you can’t be 100% sure that all the pieces of your system work as expected. So a happy medium is this:

  • Write an acceptance test for the “happy path”
  • Save testing the edge cases for the controller (or other objects) tests
  • Rely on CSS classes & ids for your test assertions. This ensures your tests don’t have to change too often. For example, favor something like expect(page).to have_css('#greeting') over expect(page).to have_content('hello')
  • Use a headless browser

Controllers

I prefer to keep my controller specs small and integrated (for the most part).

At a minimum, here’s what I test for in my controllers:

  • Check that the right view template is rendered
  • Check redirects
  • Ensure flash messages are set
  • Ensure instance variables are set as expected
  • If applicable (and simple enough to do so), check that emails are sent and/or the database is updated

Ideally, I try to stay clear of putting business logic in my controllers. If my controller spec has tests for more than the above, I start to consider extracting the relevant functionality into service objects.

I also get two extra benefits with my controller tests:

  1. The tests won’t pass if the route is not defined.
  2. By using render_views, I ensure that any view errors are caught.

Because of this, I don’t write view and route tests.

Models

In my model tests, I often make use of the build_stubbed helper that FactoryGirl provides. It reduces your test’s dependence on the database and makes for a faster test.

My model specs typically only contain tests for the following:

  • Columns (something like it { is_expected.to have_db_column(:name).of_type(:string) })
  • Scopes
  • Validations
  • Associations
  • Delegations

If your spec contains tests for more than the above, you might want to consider if extracting this functionality would make your code easier to deal with.

POROs and Other Custom Rails Objects

Most business logic in a decent sized Rails app will (or should) reside outside the MVC structure. Subsequently, the lion’s share of your tests will run on these objects:

  • Validators
  • Serializers
  • Workers
  • Helpers
  • Mailers
  • POROs (things like Form Objects, Value Objects etc)

As far as you can, aim to isolate your tests for these objects from other parts of the system. Though it won’t always be possible because of the way Rails is designed, sustained effort in this area is bound to pay off in more ways than one:

  1. Your tests will run faster. This will make your development process more enjoyable.
  2. Difficulty isolating an object under test (for example, with deeply nested stubs) can be a sign that your object is doing too much, or knows too much about other objects in the system. When this happens to me, I often reconsider my design and think of ways I can reduce my object’s responsibilities.

Javascript

I use jasmine to write my Javascript specs. I do think Javascript specs are a necessary component of a well-tested and stable Rails app. There’s not much more to say here other than the fact that Javascript code is like any other code - it is subject to change over time. The higher your test coverage, the more confidence you will have in changing your code when the time comes to do so.

External Services

You can run into a number of issues when dealing with external services in your tests:

  • Slow tests, because you need to wait for the 3rd party API call to return.
  • Intermittent test failures.
  • Hitting API rate limits.
  • Service doesn’t have a “test” mode.

To get around these issues, I use the VCR and WebMock gems. These gems allow you to effectively stub the calls made to third party services, and ensure that your tests are deterministic and fast. A word of caution: If you decide to use gems like VCR and WebMock, you should always keep an eye out for changes to the API. When your calls to the API are stubbed and the API changes, your tests can continue to pass even though your production app might break.

Test Speed: Rules of Thumb

Martin Fowler says:

…your test suites should run fast enough that you’re not discouraged from running them frequently enough. And frequently enough is so that when they detect a bug there’s a sufficiently small amount of work to look through that you can find it quickly. (emphasis mine)

Kent Beck’s rule of thumb: Your test suite as a whole should take no longer than 10 minutes to run.

Conclusion

How do you test your Rails apps? Has TDD helped or hindered you? Compare your own test strategy to the one described above and consider how they differ, and if either can be improved. More importantly, figure out what your goals are with your tests, and evaluate if they are giving you the results you want.

I’ve condensed the information in this article into an easy-to-digest PDF cheatsheet - my goal to provide you with a quick reference on how to approach testing in your own Rails app.

Download your free cheatsheet on Rails Testing here!


Sid has architected and developed a cyber risk survey app for Fortune 500 firms, and built features for an IoT messaging device startup, mortgage financing, and clean energy applications.


Got a question about ruby dependency management?

Or need a second pair of eyes on your Rails app?

Contact us today for a complimentary 30 minute consultation.

get in touch