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.