I'm working on a Ruby on Rails project and practicing Test-Driven Development (TDD) using Minitest.
For user authentication (using Devise), I wrote a test to check that a user cannot sign up without an email. However, I also have other required attributes like first_name, last_name, username, and password.
This got me thinking:
Another developer suggested that if I have presence validation for multiple attributes, I should also write a test for each one. Their reasoning was that every implemented functionality should be tested. They also suggested using a looping approach to avoid redundant code instead of writing multiple nearly identical tests.
I understand that from a TDD perspective, where I write the test first and then write the code to make it pass, a looping test for missing attributes would force me to implement presence validation for each field. This ensures I don’t accidentally forget one. However, from a more pragmatic approach, I still wonder if this level of testing is necessary when Rails already enforces presence validation at the model level.
Would love to hear the best practice here—how do experienced Rails developers approach this?
Here is the test that I currently have:
test "user cannot sign up without email" do
post user_registration_path, params: {
user: {
first_name: "Test",
last_name: "User",
username: "testuser",
email: "",
password: "password123",
password_confirmation: "password123",
}
}
assert_response :unprocessable_entity
end
I agree with what engineersmnky said in the comment. If your model has presence validations for those attributes, you're expected to write unit tests for each one of the validations in the model tests.
If you have a User
model, I'd expect you also have a UserTest
like the one below:
# test/models/user_test.rb
require 'test_helper'
class UserTest < ActiveSupport::TestCase
def setup
@user = User.new(
first_name: 'Test',
email: 'User',
username: "testuser",
email: "user@example.com",
password: "password123",
password_confirmation: "password123",
)
end
test 'invalid without email' do
@user.email = nil
refute @user.valid?
assert_not_nil @user.errors[:email]
end
# And other tests like the one above should be added for
# any other validations
end
Then, when testing the request flow, you'll likely want to write a test for the happy path scenario and a generic test for an unhappy path.
Something like:
# Happy path
test "user can sign up as long as the data is valid" do
post user_registration_path, params: {
user: {
first_name: "Test",
last_name: "User",
username: "testuser",
email: "user@example.com",
password: "password123",
password_confirmation: "password123",
}
}
assert_response :success
end
# Unhappy path
test "user cannot sign up with invalid data" do
post user_registration_path, params: {
user: {
first_name: "Test",
last_name: "User",
username: "testuser",
email: "",
password: "password123",
password_confirmation: "password123",
}
}
assert_response :unprocessable_entity
end
Note the "Unhappy path" test here is almost identical to the one in your question (the only difference being the test description). But on this approach, one unhappy test should be enough. You do not have to test that email should be present for every user, what you should test here is that your controller is properly handling the case where the user might be invalid, regardless of the reason.