Антипаттерны модульного тестирования
DESCRIPTION
RubyConfUa2010 speechTRANSCRIPT
Антипаттерны
модульного тестирования
Митин Павел, RubyConfUa 2010
О себе
• Ruby on Rails разработчик
• 4 года практики в стиле test-driven development
• http://novembermeeting.blogspot.com
Indented test code vs Правило Шапокляк
describe PopularityCalculator, '#popular?' do it 'should take into account \ the comment count' do subject.popular?(post).should be_false end
# ...end
Indented test code vs Правило Шапокляк
class PopularityCalculator def popular?(post) endend
Indented test code vs Правило Шапокляк
it 'should take into account the comment count' do posts = (0..19).map do |n| post_with_comment_count n end
posts.each do |post| if 10 < post.comment_count subject.popular?(post).should be_true else subject.popular?(post).should be_false end endend
Indented test code vs Правило Шапокляк
class PopularityCalculator THRESHOLD = 10
def popular?(post) THRESHOLD < post.comment_count endend
Indented test code vs Правило Шапокляк
it 'should take into account the comment count' do posts = (0..19).map do |n| post_with_comment_count n end
posts.each do |post| if 10 < post.comment_count subject.popular?(post).should be_true else subject.popular?(post).should be_false end endend
Indented test code vs Правило Шапокляк
Мотивация:
• борьба с дублированием
• работы с неконтролируемыми аспектами системы(время, дисковое пространство и т.д.)
Indented test code vs Правило Шапокляк
Правило Шапокляк: Это хорошо ... хорошо, что Вы зеленый и плоский
Indented test code vs Правило Шапокляк
it "should return true if the comment count / is more then the popularity threshold" do
post = post_with_comment_count THRESHOLD + 1 subject.popular?(post).should be_true
post = post_with_comment_count THRESHOLD + 100 subject.popular?(post).should be_true end
Indented test code vs Правило Шапокляк
Бенефиты:
• тесты проще понять
• тесты содержат меньше ошибок
Production Logic in Test
class PopularityCalculator THRESHOLD = 10
def popular?(post) THRESHOLD < post.comment_count end end
Production Logic in Test
it "should take into account the comment count" do post = post_with_comment_count 11 expected = THRESHOLD < post.comment_count actual = subject.popular? post actual.should == expectedend
Production Logic in Test
it "should take into account the comment count" do post = post_with_comment_count 11 expected = THRESHOLD < post.comment_count actual = subject.popular? post actual.should == expectedend
Production Logic in Test
Мотивация: упростить получение ожидаемого значения
Production Logic in Test
Production Logic in Test
it "should take into account the comment count" do
actual = subject.popular? post_with_comment_count(11)
actual.should be_true
end
Production Logic in Test
Бенефиты: мы действительно тестируем, а не только улучшаем тестовое покрытие :)
Too Many Expectations
describe NotificationService, "#notify_about" do it "should notify the post author by email" do notification_service.notify_about @comment end
it "should notify the post author by sms" end
Too Many Expectations
class NotificationService < Struct.new(:email_service, :sms_service)
def notify_about(comment) end end
Too Many Expectations
before do @email_service, @sms_service = mock, mock @comment = Comment.new 'some text', 'dummy post' @notification_service = NotificationService. new @email_service, @sms_serviceend
Too Many Expectations
it "should notify the post author by email" do
@email_service.expects(:deliver_new_comment_email).
with @comment
@sms_service.expects :deliver_new_comment_sms
@notification_service.notify_about @comment
end Too Many Expectations
it "should notify the post author by sms" do @email_service.expects :deliver_new_comment_email @sms_service.expects(:deliver_new_comment_sms). with @comment @notification_service.notify_about @comment end
Too Many Expectations
def notify_about(comment) email_service.deliver_new_comment_email comment sms_service.deliver_new_comment_sms comment end
Too Many Expectations
it "should notify the post author by email" do
@email_service.expects(:deliver_new_comment_email).
with @comment
@sms_service.expects :deliver_new_comment_sms
@notification_service.notify_about @comment
end
it "should notify the post author by sms" do
@email_service.expects :deliver_new_comment_email
@sms_service.expects(:deliver_new_comment_sms).
with @comment
@notification_service.notify_about @comment
end
Too Many Expectations
def notify_about(comment) # email_service.deliver_new_comment_email comment sms_service.deliver_new_comment_sms comment end
Too Many Expectations
1)
Mocha::ExpectationError in 'NotificationService#notify_about should notify the post author by email'
not all expectations were satisfied
unsatisfied expectations:
- expected exactly once, not yet invoked: #<Mock:0xb74cdd64>.deliver_new_comment_email(#<Comment:0xb74cdb70>,
#<Mock:0xb74cdf08>)
satisfied expectations:
- expected exactly once, already invoked once: #<Mock:0xb74cde2c>.get(any_parameters)
- expected exactly once, already invoked once: #<Mock:0xb74cdc9c>.author_id(any_parameters)
- expected exactly once, already invoked once: nil.deliver_new_comment_sms(any_parameters)
2)
Mocha::ExpectationError in 'NotificationService#notify_about should notify the post author by sms'
not all expectations were satisfied
unsatisfied expectations:
- expected exactly once, not yet invoked: #<Mock:0xb74c937c>.deliver_new_comment_email(any_parameters)
satisfied expectations:
- expected exactly once, already invoked once: #<Mock:0xb74c9444>.get(any_parameters)
- expected exactly once, already invoked once: #<Mock:0xb74c92b4>.author_id(any_parameters)
- expected exactly once, already invoked once: nil.deliver_new_comment_sms(#<Comment:0xb74c9188>,
#<Mock:0xb74c9520>)
Too Many Expectations
Too Many Expectations
Причины воспроизведения паттерна: отсутствие знаний о стаб-объектах
Too Many Expectations
before do
@sms_service = stub_everything
@email_service = stub_everything
@notification_service = NotificationService. new @email_service, @sms_serviceend
Too Many Expectations
it "should notify the post author by email" do
@email_service.expects(:deliver_new_comment_email).
with @comment, @author
@notification_service.notify_about @comment
end
it "should notify the post author by sms" do
@sms_service.expects(:deliver_new_comment_sms).
with @comment, @author
@notification_service.notify_about @comment
end
Too Many Expectations
Бенефиты: одна ошибка - один падающий тест
Redundant Fixture
describe PostRepository, "#popular" do it "should return all popular posts" do repository.popular.should include(popular_post) end end
Redundant Fixture
class PostRepository def popular [] end end
Redundant Fixture
it "should return all popular posts" do popular_posts = (1..2).map { build_popular_post } unpopular_posts = (1..3). map { build_unpopular_post } posts = (popular_posts + unpopular_posts).shuffle repository = PostRepository.new posts
actual = repository.popular
actual.should have(2).posts actual.should include(popular_posts.first, popular_posts.last) end
Redundant Fixture
it "should return all popular posts" do popular_posts = (1..2).map { build_popular_post } unpopular_posts = (1..3). map { build_unpopular_post } posts = (popular_posts + unpopular_posts).shuffle repository = PostRepository.new posts
actual = repository.popular
actual.should have(2).posts actual.should include(popular_posts.first, popular_posts.last) end
Redundant Fixture
Мотивация: желание получить в тестовом окружении “реальные” данные
Redundant Fixture
VS
Redundant Fixture
before do @popular_post = build_popular_post @unpopular_post = build_unpopular_post @repository = PostRepository.new( [@popular_post, @unpopular_post] )end it "should return a popular post" do @repository.popular.should include(@popular_post) end it "shouldn't return an unpopular post" do @repository.popular. should_not include(@unpopular_post) end
Redundant Fixture
Бенефиты:
• простой setup
• сообщение о падении теста не перегружено лишними данными
• профилактика "медленных" тестов
Neglected Diagnostic vs Ясный красный
describe BullshitProfitCalculator, "#calculate" do it "should return the projected profit" do actual = subject.calculate 'dummy author' actual.should == '$123'.to_money end end
class BullshitProfitCalculator def calculate(author) '$1'.to_money end end
Neglected Diagnostic vs Ясный красный
'BullshitProfitCalculator#calculate should return the projected profit' FAILED
expected: #<Money:0xb7447ebc @currency=#<Money::Currency id: usd
priority: 1, iso_code: USD, name: United States Dollar, symbol: $,
subunit: Cent, subunit_to_unit: 100, separator: ., delimiter: ,>,
@cents=12300, @bank=#<Money::VariableExchangeBank:0xb74dabb8
@rates={}, @mutex=#<Mutex:0xb74dab7c>, @rounding_method=nil>>,
got: #<Money:0xb7448038 @currency=#<Money::Currency id: usd
priority: 1, iso_code: USD, name: United States Dollar, symbol: $,
subunit: Cent, subunit_to_unit: 100, separator: ., delimiter: ,>,
@cents=100, @bank=#<Money::VariableExchangeBank:0xb74dabb8 @rates={},
@mutex=#<Mutex:0xb74dab7c>, @rounding_method=nil>> (using ==)
Neglected Diagnostic vs Ясный красный
Neglected Diagnostic vs Ясный красный
module TestMoneyFormatter def inspect format end end
class Money include TestMoneyFormatter end
Neglected Diagnostic vs Ясный красный
'BullshitProfitCalculator#calculate should return the projected profit' FAILED expected: $123.00, got: $1.00 (using ==)
Neglected Diagnostic vs Ясный красный
Было: Стало:
красный красный
ясный красный
зеленый зеленый
рефакторинг рефакторинг
Neglected Diagnostic vs Ясный красный
Бенефиты: ясное диагностическое сообщение упрощает дальнейшую поддержку теста
Еще антипаттерны
• глобальные фикстуры
• функциональный код, используемый только в тестах
• нарушение изоляции тестов
• зависимости из других слоев приложения
• тестирование кода фреймворка
Антипаттерны в mocking TDD
• мокание методов тестируемого модуля
• мокание объектов-значений
Еще антипаттерны
• “медленные” тесты
• …
Рекомендуемая литература
• Экстремальное программирование. Разработка через тестирование, Кент Бек
• Growing Object-Oriented Software, Guided by Tests by Steve Freeman and Nat Pryce
Исходный код примеров
http://github.com/MitinPavel/test_antipatterns.git
Использованные изображения
• http://dreamworlds.ru/kartinki/27030-otricatelnye-personazhi-v-multfilmakh.html
• http://www.inquisitr.com/39089/former-police-officer-sues-for-discrimination-over-his-alcoholism-disability/
• http://teachpro.ru/source/obz11/Html/der11163.htm
• http://bigpicture.ru/?p=4302
• http://giga.satel.com.ua/index.php?newsid=25654
Спасибо за внимание
RubyConfUa 2010