Testing Rails Apps
🧪 Testing Rails Apps
ruby2-6) covered describe/it/expect for testing plain Ruby classes — this chapter applies the same tool to an entire Rails app: models, HTTP requests, and the Pundit policies from Chapter 2.
💎 RSpec in a Rails App
This creates a spec/ directory (Rails' convention over the plain test/ directory Minitest would use) and a rails_helper.rb that loads the Rails environment before every test run. Everything from ruby2-6 — describe, it, expect(...).to matchers, before(:each) — carries over unchanged. What's new is what gets described: not a plain class, but models, requests, and policies wired into a real Rails app.
🗄️ Model Specs
Model specs verify exactly the things Course 1 taught you to declare — validates :body, presence: true from rails1-7, belongs_to :post from rails1-6 — actually behave the way they claim to. be_valid is RSpec's automatic matcher for any valid?-ending predicate method, the same auto-generated-matcher trick ruby2-6 covered for plain Ruby objects.
🏭 FactoryBot — Test Data Without Fixtures
Rails ships with fixtures — static YAML files loaded wholesale before each test — but they get unwieldy fast: every association has to be hand-wired by ID across separate files, and every test sees the same shared data whether it needs it or not. FactoryBot builds records in code instead, one factory per model, with only the attributes a given test actually cares about overridden inline. create saves to the (test) database; build instantiates without saving, for tests that don't need persistence.
trait :from_admin { association :user, factory: :admin_user } lets a single factory produce meaningful variations (create(:comment, :from_admin)) without a combinatorial explosion of near-duplicate factories — the same instinct behind default arguments in ruby1-4: define the common case once, override only what differs.
🌐 Request Specs
Request specs send a real HTTP request through the full stack — routing, middleware, controller, view — and assert on the response, the same end-to-end shape as Express's supertest tests from express2-5. They replaced Rails' older "controller specs," which called controller actions directly and skipped routing entirely; that shortcut turned out to hide real bugs (like a route that never matched in the first place), so request specs are now the recommended default.
🔐 Testing the Authorization Chapter
The second request spec above is really testing Chapter 2's CommentPolicy#update? indirectly, through the full HTTP round-trip. Pundit also ships a matcher for testing a policy directly, without going through a controller at all:
Policy specs are fast (no HTTP layer, no full request cycle) and precise (they test only the permission logic), while request specs confirm that logic is actually wired up correctly end to end. Good Rails test suites use both — narrow, fast specs for logic, broader request specs for "does the whole thing actually work."
📜 Rails Testing vs Express Testing
RSpec + FactoryBot (Rails) vs Jest + supertest (Express)
| Concern | Express (express2-5) | Rails |
|---|---|---|
| Test framework | Jest | RSpec |
| HTTP-level tests | supertest against the Express app | Request specs (type: :request) |
| Test data | Hand-built objects or mocked DB layer | FactoryBot factories, real test database |
| Isolated unit tests | Jest unit tests + manual mocks | Model specs, policy specs |
| Database in tests | Often mocked to avoid a real DB | Real test DB, wrapped in a rollback transaction per test |
The database row is worth calling out: express2-5 leaned toward mocking the database layer to keep tests fast and isolated. Rails takes the opposite default — tests run against a real (separate, disposable) test database, with each test wrapped in a transaction that's rolled back afterward, so tests stay isolated from each other without giving up the ability to catch real SQL-level bugs a mock would miss.
💻 Coding Challenges
Challenge 1: A Model Spec
Given a Post model with validates :title, presence: true and belongs_to :user, write a model spec with two examples: one confirming a Post without a title is invalid, and one confirming the user association exists.
Goal: Practice turning a model's declared rules into concrete assertions.
Challenge 2: A FactoryBot Factory
Write a FactoryBot factory for a Post model (attributes: title, body, belongs to a user). Then show how a spec would use it to create a post with a custom title while leaving the other attributes at their factory defaults.
Goal: Practice defining and overriding factory attributes.
Challenge 3: A Request Spec for Authorization
Using the PostPolicy from this course (only an admin or the post's author can destroy it), write a request spec for DELETE /posts/:id with two examples: one where the author successfully deletes their post, and one where a different, non-admin user is redirected instead.
Goal: Test an authorization rule through the full HTTP stack, not just the policy in isolation.
Every spec in this chapter follows the same shape: one example proving the rule allows what it should, one proving it denies what it should. A model spec that only checks "a valid post passes" without also checking "an invalid post fails" hasn't actually verified the validation is doing anything — it would still pass even if the validates line were deleted entirely.
🎯 What's Next
Next chapter: Background Jobs — Active Job and Sidekiq, for work (sending an email, resizing an image) that shouldn't block the request/response cycle this chapter just finished testing.