Updating Deeply Nested Attributes with RSpec

The problem: I had to find a way to test the update action that was nested four layers deep.

The solution: Create RSpecs params in a way I had never done before..

I had been tasked with overhauling the relationship between a Club and Promo (for a gym app), and because of the way a form needed to be setup, I ended up with a one-off controller bulk_promo_controller.rb that had nested attributes four levels deep. There are ways to cleanly test nested attributes, but all examples and documentation stops at one level deep, I was in uncharted territory to say the least.

To ensure you are on the same page, here’s an example of my standard RSpec update setup:

describe 'PUT #update' do
  before do
    @promo = create(:promo)

  context 'with valid attributes' do
    it 'should update the promo and redirect to the index' do
      process :update, method: :put, params: { id: @promo.id, promo: attributes_for(:promo, customer_promo_code: 'Updated Name') }

      expect(assigns(:promo)).to eq(@promo)
      expect(response).to redirect_to(admin_promos_path)


Pretty standard, heres whats happening:

process :update -> which controller action is being called

method: :put -> which type of route is to be used

params: -> params from the controller, in this case for promo

attributes_for(:promo) -> using the Promo model’s FactoryGirl configuration to update the object

customer_promo_code: -> overwriting the default FactoryGirl setting for customer_promo_code to a constant, to be able to test it.

Now, this is the bulk_promos_controller.rb . As you can see, the params include the promo , at the highest level, then the club_promos_attributes , which inherit the club_promo_plans_attributes , and that controller inherits the mms_prices_attributes, which is ultimately what I’m trying to get at.

# frozen_string_literal: true
class Admin::BulkPromosController < Admin::ApplicationController
  before_action :set_promo, only: :update

  def update
    if @bulk_promo.update(bulk_promo_params)
      redirect_to admin_promo_club_promos_path(@bulk_promo), notice: 'Promo was successfully updated.'
      flash[:error] = 'Promo was not updated.'
      render :edit

  def set_promo
    @bulk_promo = Promo.find(params[:id])

  def bulk_promo_params
      :customer_promo_code, club_ids:[],
      club_promos_attributes: [:id, :club_id, :promo_id,
      club_promo_plans_attributes: [:id, :club_promo_id, :plan_id,
      mms_prices_attributes: [:id, :payment_type, :mms_promo_code, :mms_installment_id, :mms_plan_id, :_destroy]]]

I tried to setup the bulk_promo_controller spec in a similar way to the promo controller (or any other updating action for that matter), using the attributes_for method, but the trouble was that I couldn’t burrow down through all the necessary associations to get at the proper mms_price. I don’t have examples of all the attempts, but I tried more iterations than I could count. I threw a binding.pry into the mix, to try and step through the process in the terminal, but it wasn’t happening.

Finally, I acknowledged that this was a bit over my head and requested a pair session with a senior dev. The first thing he asked me was ‘whats the readout from the server logs when you hit the controller from the web interface?’ I hadn’t even thought to check there, but it made total sense, especially because I knew the action was functioning correctly, I was essentially trying to duplicate that process in the test. (Thats definitely not the best practice in writing tests, but this has been an RSpec edge case from the get go). So, this is how I got the RSpec params for the test:

In the terminal running the local server, I ran command + k to clear the server log.

I went to my locally run browser page that had the bulk promo update action, and updated a record. Now, back in the server log, it will display all the parameters that were passed to update that mms_record.

Started PATCH "/admin/bulk_promos/394" for ::1 at 2017-10-05 11:29:16 -0600
Processing by Admin::BulkPromosController#update as HTML
  Parameters: {"utf8"=>"✓", "authenticity_token"=>"1l+8oy6dANoziB/PZ5HpRQAAnBU8Pp7j/BIdB2NxgQcyGqpglX7tV+T/EBY5yfPGFz1bKsJPT3hWyiOqpyh5dw==", "promo"=>{"club_promos_attributes"=>{"0"=>{"club_promo_plans_attributes"=>{"0"=>{"mms_prices_attributes"=>{"0"=>{"mms_plan_id"=>"thisistestmmscontent", "mms_installment_id"=>"", "mms_promo_code"=>"", "id"=>"2643"}, "1"=>{"mms_plan_id"=>"", "mms_installment_id"=>"", "mms_promo_code"=>"", "id"=>"2642"}}, "id"=>"1217"}, "1"=>{"mms_prices_attributes"=>{"0"=>{"mms_plan_id"=>"", "mms_installment_id"=>"", "mms_promo_code"=>"", "id"=>"2645"}, "1"=>{"mms_plan_id"=>"", "mms_installment_id"=>"", "mms_promo_code"=>"", "id"=>"2644"}}, "id"=>"1218"}, "2"=>{"mms_prices_attributes"=>{"0"=>{"mms_plan_id"=>"", "mms_installment_id"=>"", "mms_promo_code"=>"", "id"=>"2647"}, "1"=>{"mms_plan_id"=>"", "mms_installment_id"=>"", "mms_promo_code"=>"", "id"=>"2646"}}, "id"=>"1219"}, "3"=>{"mms_prices_attributes"=>{"0"=>{"mms_plan_id"=>"", "mms_installment_id"=>"", "mms_promo_code"=>"", "id"=>"2649"}, "1"=>{"mms_plan_id"=>"", "mms_installment_id"=>"", "mms_promo_code"=>"", "id"=>"2648"}}, "id"=>"1220"}}, "id"=>"481"}}}, "commit"=>"Update Promo Plans", "id"=>"394"}

I don’t know about anyone else, but I have a terrible time trying to read this output, so I copy/pasted it into my text editor, starting after the authenticity token . Now in the editor, I found where it started repeating, and broke out those lines. The indicators are the "id" labels, because thats what I was trying to identify, the id of the club_promoclub_promo_plan, and the mms_pricing.

I ended up with a pretty long hash (to help you read it, I replaced the random integers with the model id). The output was something like this:

{"club_promos_attributes" => {"0" => {"club_promo_plans_attributes" => {"0" => {"mms_prices_attributes" => {"0" => {"mms_promo_code" => "teststring","mms_installment_id" => "","mms_plan_id" => "","id" => "mms_price_id"}},"id" => "club_promo_plan_id"}},"id" => "club_promo_id"}}}

Using that pattern, I was able to assign the dynamic ids from the RSpec tests and place that hash (as a variable) into the params. With some formatting of that hash my working RSpec test looks like this:

describe 'PUT #update' do
  context 'with valid attributes' do
    it 'should update the promo with mms attribute and redirect to the index' do
      promo = create(:promo)
      @club_promo_id = promo.club_promos.first.id
      @club_promo_plan_id = promo.club_promos.first.club_promo_plans.first.id
      @mms_price = promo.club_promos.first.club_promo_plans.first.mms_prices.first

      promo_attributes =  {
                            "club_promos_attributes" => {
                              "0" => {
                                "club_promo_plans_attributes" => {
                                  "0" => {
                                    "mms_prices_attributes" => {
                                      "0" => {
                                        "mms_promo_code" => "teststring",
                                        "mms_installment_id" => "",
                                        "mms_plan_id" => "",
                                        "id" => "#{@mms_price.id}"
                                    "id" => "#{@club_promo_plan_id}"
                                "id" => "#{@club_promo_id}"

      process :update, method: :put, params: { id: promo.id, promo: promo_attributes }


      expect(@mms_price.mms_promo_code).to eq("teststring")
      expect(response).to redirect_to(admin_promo_club_promos_path(promo))

It’s not pretty, probably the ugliest RSpec I’ve written to date, but its functional and not brittle. I also feel like another dev could look at this test and have a pretty good understanding of whats going on, which is important. I’m open to feedback if anyone has a better strategy, let me know!