Model Tests and Request Tests
Fast recall
Quick revision
Core answer
Whether you know model tests check validations/scopes/methods and request tests check the HTTP layer (get, assert_response, response.body).
Testing the framework (that .save works) instead of YOUR logic, or coupling tests to internals.
Model tests: validations/scopes/methods in isolation. Request tests: get '/x'; assert_response; response.body. Fixtures/factories seed data; rails test runs them.
Interview-ready guide
Interview answer
A model test should verify behaviour owned by the model or domain object: calculations, validations, state changes, and rejected transitions. It calls that object directly, so cases such as order.ship! before payment fail close to the rule. It should not spend assertions proving that Active Record itself can save an ordinary row.
A request test sends an HTTP request through routing and the Rails stack. It can verify status, headers, parsed JSON or rendered HTML, redirects, authentication, and persisted effects. For POST /orders, that means checking what a client receives and whether the correct user owns the new record—not a private controller method.
For an order flow, focused tests can cover every invalid state transition. A request test then submits one representative invalid order and expects 422 Unprocessable Entity plus the public error body. The executable example keeps the business rule direct and crosses the HTTP-shaped boundary only once.
I keep many fast cases around the object that makes a decision and fewer request tests around valuable success, invalid-input, permission, and not-found paths. Limited overlap is useful when it proves a rule becomes the correct HTTP result. Thin controllers make this split clearer: focused objects own decisions, while request tests verify coordination.
Model Tests and Request Tests
Direct Answer
Model tests should verify business rules owned by a model or domain object, such as calculations, validations, and state changes. Request tests should exercise the HTTP boundary through routing and the Rails stack, then verify status, headers, response body, redirects, authentication, and persisted effects. Keep controller actions thin enough that request tests cover coordination while focused model or service tests cover decision logic. Test observable results, not private methods or framework internals.
Deep Dive & Architecture
A validation can be observed at two levels. A model test can construct an invalid record and inspect its errors, which isolates the business rule. A request test can submit invalid input and inspect the HTTP response, which proves the rule is translated into the public API. The two tests answer different questions, so limited overlap can be valuable.
Request tests include routing, parameter parsing, controller filters, authentication, rendering, and persistence. That wider path catches integration mistakes a direct controller method call can miss. It also introduces more possible causes of failure, so request examples should focus on representative success, invalid input, permission, and not-found paths instead of every arithmetic boundary.
A thin controller coordinates the boundary: accept input, call application behaviour, and render or redirect. If a controller contains branching business policy, tests become tied to the transport layer. Moving the policy to a named object lets it receive focused tests while the request test checks only the mapping to HTTP.
Assertions should match what a consumer can rely on. For JSON, check the selected schema fields and status rather than a fragile full string. For redirects, check the destination and durable state. Avoid asserting assignments or exact template internals unless those are genuinely part of the product contract.
Code Example
# Model test: focused business rule
class OrderTest < ActiveSupport::TestCase
test 'cannot ship before payment' do
order = Order.new(status: :pending)
assert_raises(Order::InvalidTransition) { order.ship! }
end
end
# Request test: public endpoint contract
class OrdersFlowTest < ActionDispatch::IntegrationTest
test 'invalid order returns an error response' do
post orders_path, params: { order: { quantity: 0 } }, as: :json
assert_response :unprocessable_entity
assert_includes response.parsed_body['errors'], 'Quantity must be greater than 0'
end
end
Common Mistakes & Pitfalls
Testing the framework (that .save works) instead of YOUR logic, or coupling tests to internals.
Check your understanding
Answer these without looking back at the article.
- Why not repeat every model edge case through a request test?
- What should an API request test assert besides the status code?
- When can a small amount of overlap between model and request tests be useful?
Looking for a structured roadmap?
Every module on this page is also part of the full Java backend interview track.