Skip to Content
Course content

134: Testing Rails Serializers

Click on the "Edit" button in the top corner of the screen to edit your slide content.

When you first start adding serializers to your Rails app—whether you're using active_model_serializers, jsonapi-serializer, or even just custom as_json overrides—it's tempting to think of them as simple configuration files. You're just mapping an attribute from the database to a key in a JSON object, right? Because of that, most developers I've mentored tend to skip dedicated serializer tests entirely, relying instead on their request specs to "catch" any issues.

Testing the API endpoint (The heavy lift)

The naive way to test a serializer is to write a request spec that hits the controller action and checks the response body. Imagine we have a UserSerializer that doesn't just mirror the database, but computes a full_name by combining first_name and last_name, and explicitly filters out the password_digest. Your test might look like this:

it "returns the user's full name" do
  user = create(:user, first_name: "Jane", last_name: "Doe")
  get "/users/#{user.id}"
  
  json = JSON.parse(response.body)
  expect(json["full_name"]).to eq("Jane Doe")
  expect(json).not_to have_key("password_digest")
end

Now, this works. It technically proves the JSON coming out of your app is correct. But here is the problem: you've just spun up the entire Rails routing stack, hit a controller, queried the database, and executed the serialization logic just to verify a string concatenation. When you have fifty different serializers and hundreds of fields, this approach turns your test suite into a slog. If the full_name logic breaks, you're spending seconds waiting for the infrastructure to boot just to find out a simple Ruby method failed.

Isolating the transformation (The lightweight approach)

The better way is to treat the serializer as a plain Ruby object. A serializer's only job is to take an object and turn it into a hash. Why not just test that transformation in isolation? By instantiating the serializer directly in a unit test, you bypass the network, the controller, and often even the database if you use a build_stubbed object or a double.

I prefer writing these as focused unit tests. It looks more like this:

it "serializes the full name correctly" do
  user = build_stubbed(:user, first_name: "Jane", last_name: "Doe")
  serializer = UserSerializer.new(user)
  serialization = serializer.serializable_hash # or .as_json depending on your gem
  
  expect(serialization[:full_name]).to eq("Jane Doe")
  expect(serialization).not_to have_key(:password_digest)
end

The difference in speed is night and day. You're testing the logic of the transformation without the overhead of the delivery mechanism. If you add a conditional attribute—say, only showing the user's email if the current user is an admin—you can easily pass a mock "scope" or "context" into the serializer and test both permutations in milliseconds.

Balancing coverage and speed

I'll be honest: you shouldn't delete all your request specs. You still need a few "smoke tests" to ensure the controller is actually using the correct serializer. If you forget to add render json: @user, serializer: UserSerializer in your controller, your unit tests will pass, but your API will return the wrong data.

The sweet spot is a 90/10 split. Use unit tests to exhaustively check every edge case of your data transformation—null values, formatted dates, conditional fields—and use a single, simple request spec to verify that the plumbing is connected. You get the confidence of full coverage without the penalty of a slow CI pipeline.




📋 Practical Task

Verify User Profile Privacy Logic in UserSerializer

You have a UserSerializer that should only expose the phone_number attribute if the user is marked as public_profile: true. Currently, the serializer is leaking phone numbers for all users.

Your task:

  • Create a unit test for UserSerializer.
  • Write one test case ensuring that when public_profile is true, the phone_number is present in the resulting hash.
  • Write a second test case ensuring that when public_profile is false, the phone_number key is completely absent from the output.
  • Modify the UserSerializer logic to make these tests pass.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.