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
Show archive.org snapshot
. 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.