March 14, 2018

Shared Components, Shared Integration Testing

At my job, we’re in the process of writing a library of shared React components. (This blog post deals with React, Ruby, and Selenium, but the concepts are applicable to anyone writing integration tests for a shared set of reusable components.) This is awesome, and we’re seeing high rates of adoption and modification among devs. However, when writing our integration specs, we end up writing the same selectors and behavior over and over. I came up with a solution this week for how to help improve some of the pain we’re feeling, and I wanted to share it with you:

Current Pain

  • Our integration tests are tightly coupled to the classnames and markup of our components.
  • If we want to do a green-green refactor of a core component (like, say, swapping out a third-party library for a home-rolled one, or vice versa), we need to refactor the integration tests as well.
  • There’s no one place to document the expected behavior of our core components.

Proposed Solution

  • Create spec helper objects that expose methods for manipulating and asserting things about the DOM that our components render.
  • Keep the knowledge of the actual shape of our HTML hidden inside these methods.
  • Put the spec helpers in the same folder as the components themselves.

An Example:

# core-components/paginator/paginator-helper.rb
module CoreComponents
  class Paginator
    include Capybara::DSL
    BASE_SELECTOR = ".core-paginator"

    def initialize(parent_selector = nil)
      @selector = parent_selector ? "#{parent_selector} #{BASE_SELECTOR}" : BASE_SELECTOR
    end

    def go_to_next_page
      within(@selector) do
        click_button('.next')
      end
    end

    def has_number_of_pages(number)
      within(@selector) do
        find(".page", count: number)
      end
    end

    def is_on_page?(page_number)
      within(@selector) do
        find(".page.current")
      end
    end
  end
end

# spec_helpers/pages/library.rb
module Pages
  class Library
    def book_paginator
      ::CoreComponents::Paginator.new('.book-paginator')
    end
  end
end

# library_spec.rb
let(:library_page) { Pages::Library.new }

it 'can page between books' do
  log_in
  library_page.navigate

  expect(library_page.book_paginator).to be_on_page(1)
  expect(library_page.book_paginator).to have_number_of_pages(23)
  library_page.book_paginator.go_to_next_page
  expect(library_page.book_paginator).to be_on_page(2)
end

Benefits

  • Now, only one file knows about the shape of our core components, so that we’ll only have to change one file when we refactor the component.
  • As we move more toward the glorious world of building features out of mostly shared components, our testing will become easier and easier, as we’ll already have pre-built helpers that know what the component should be able to do. People will do what is easy, so let’s make TDDing easy.
  • While we could have a folder of core component helpers in our spec folder, who’s going to know to look there? Discoverability is key for driving adoption, and it’s much more likely that the next developer who goes to use a core component will see and use the spec helper when it’s in the same folder than if they don’t even know it exists.
  • If you have multiple instances of a core component on the same page (e.g. a datepicker), you’ll be able to pass the helper a selector for a parent div and have different instances of the helper. This should help reduce duplication in the main integration spec.