Testing shared traits or modules without repeating yourself

Updated . Posted . Visible to the public. Repeats.

When two classes implement the same behavior (methods, callbacks, etc.), you should extract that behavior into a trait Show archive.org snapshot or module. This card describes how to test that extracted behavior without repeating yourself.

Note that the examples below use Modularity traits Show archive.org snapshot to extract shared behavior. This is simply because we like to do it that way at makandra. The same techniques apply for modules and overriding self.included.

Example

Say you have two classes Page and Template. Both contain the same behavior: They have a string field #html which needs to be sanitized Show archive.org snapshot (stripped of malicious HTML) before validation:

# app/models/page.rb

class Page < ApplicationRecord
  
  before_validation :sanitize_html

  private
  
  def sanitize_html
    self.html = Sanitize.clean(html)
  end
  
end

# app/models/template.rb

class Template < ApplicationRecord
  
  before_validation :sanitize_html

  private
  
  def sanitize_html
    self.html = Sanitize.clean(html)
  end
  
end

You should extract this behavior into a trait Show archive.org snapshot or module, so you can reduce the code to this:

# app/models/shared/sanitize_html_trait.rb

module DoesSanitizeHtml
  as_trait do
  
    before_validation :sanitize_html

    private
    
    def sanitize_html
      self.html = Sanitize.clean(html)
    end
          
  end
end

# app/models/page.rb

class Page < ApplicationRecord
  include DoesSanitizeHtml
end

# app/models/template.rb

class Template < ApplicationRecord
  include DoesSanitizeHtml
end

Testing test trait usage with a shared example group

When two classes share behavior through a common trait or module, the tests for those two classes should share a shared example group:

# spec/support/shared_examples/does_sanitize_html.rb

shared_examples_for DoesSanitizeHtml do

  it 'should strip malicious HTML during validation' do
    subject.html = 'before <script>alert("hacked!")</script> after'
    subject.validate
    expect(subject.html).to eq('before  after')
  end

end

# spec/models/page_spec.rb

describe Page do
  it_behaves_like DoesSanitizeHtml
end

# spec/models/template_spec.rb

describe Template do
  it_behaves_like DoesSanitizeHtml
end

If your trait takes arguments, simply remember that shared_examples_for can have parameters.

Other approaches

There is another approach to test traits by instantiating an anonymous class that uses the trait under test. I dislike this approach because it always ends up in a stubfest that doesn't really test anything.

I recommend testing traits with shared example groups as described above.

Henning Koch
Last edit
Daniel Straßner
Keywords
rspec, shared, example, groups
License
Source code in this card is licensed under the MIT License.
Posted by Henning Koch to makandra dev (2013-08-20 09:47)