Sustainable Ruby on Rails Testing
A good test suite should be sustainable. TDD should remain possible as the service or application grows. We’ve all experienced the pain of clumsy, slow test suites.
The following are some recommendations to make a Ruby on Rails test suite works to support business changes while keeping the cost of maintenance low.
Features for Sustainable Ruby on Rails Test Suites
The run time of the test suite should be low enough to not leave “the zone.” it should not exceed 5 minutes.
It is possible to run a partial build on file modifications. The run time of partial build should be fast enough to give instant feedback on the class/method currently being changed. It should stay under 10 seconds.
The test suite should run, no matter the order of the individual tests.
Limit integration tests to core features. An excellent unit test suite is enough to cover most failure scenarios. The rationale here is that integration tests are, by nature, slower. Fast feedback from the unit test should be the goal.
Flaky test suites should be resolved as a priority, as any further development is compromised.
Stubbing in Ruby On Rails Tests
Stubbing out methods on the object being tested is a symptom of bad internal APIs or poor coupling.
Testing private methods is often a symptom of “god objects.” The private method can generally be isolated to its testable class or value object:
Using stub chains is a smell you should be using dependency injection (and injecting test doubles):
Use verified doubles] to prevent them from getting out of sync. Use dependency injection to inject the Test-Doubles.
Simplify The Testing Environment
Ruby on Rails comes with a harmful practice damaging both decoupling functionality and the speed of tests. It loads “all the things” every time the environment boots up. This means reading Ruby sources from disk and compiling them for even a medium-sized application, which can be 1000s of files.
A simpler testing environment starts at the `spec_helper.rb` level. For example, Routemaster’s `spec_helper` is very short: it configures RSpec. It doesn’t load any code or “environment,” nor should it.
When writing a [[Ruby]] application (e.g., in a gem or a Sinatra app), dependencies are required. Each file requires only what it needs. Top-level files will load the necessary tree (`config.ru` will load `app.rb`, which will load the various controllers, which will load the models).
The same rules should apply to tests. Each test file should require a minimal version of `spec_helper.rb`. Any files in `spec/support` need to function. The object under test and support classes, e.g., dependencies to be injected. `spec_helper.rb`, is loaded every time a partial test is run. The cost of adding unnecessary things is enormous.
Tooling
Always fail early, using RSpec’s ` — fail-fast,` for instance. The use of `guard` and `guard-rspec` is recommended for all Ruby gems and applications.
Run integration tests last. With `guard,` you can achieve this by having separate `RSpec` groups for integration/acceptance tests.
Using auto-loaders (spring, Zeus, spork) should be discouraged. They always create more problems than they solve (weird load dependency issues, test suite instability). The temptation to use them is usually a symptom of other problems (bad `spec_helper,` application too large).
RSpec should be configured to run in random order (the default). “Flakiness,” i.e., tests failing depending on run order, is a bug, and a flaky test suite gives no confidence that regressions are avoided.
Persistence and Business Logic
Make dependencies explicit to reduce coupling
Respecting SOLID goes a long way to making classes testable.
In particular, a layered/hexagonal/service design will abstract persistence, which is the key contributor to slow tests.
Use service objects, and avoid persistence in service objects. Service objects usually have a very compact concern and a single public method (called `call`).
If your service depends on stored data, avoid loading it in the service object; prefer to delegate that role to query objects (which can be mocked).
A typical pattern is to inject repositories:
In consumers of service objects, mock them out, and stub `save!` in any output objects, for instance. The query object can be mocked similarly to a repository class.
Avoid Business Logic In ActiveRecord Models
Keep “models” thin and never use business logic in models. A different angle on the previous recipe: if your ActiveRecord models are skinny, you can mock them out in any service object or other consumer.
Any addition of a method to models, even “sugar” methods (that test or combine attributes), is a smell.
Callbacks are the most robust smell of lousy coupling.
If you must react to persistence events, `after_save` is not your friend. Use a local event bus like the excellent wisper.
Wrap and Delegate Behavior
If you need an extension of the ActiveRecord model, write a presenter using SimpleDelegator.
Define Query Objects for Complex Persistence
Some queries are complex, and you cannot avoid testing them. In a fast test suite, query objects are _only_ run in their tests (and in integration tests).