Антипаттерны модульного тестирования (Донецкий...
-
Upload
mitinpavel -
Category
Technology
-
view
840 -
download
4
description
Transcript of Антипаттерны модульного тестирования (Донецкий...
Обновленная версия доклада: http://www.slideshare.net/MitinPavel/rubyconfua-2010-unittestantipatterns
Вступление
О себе:
• Ruby on Rails разработчик
• 4 года практики в стиле test-driven development
• http://novembermeeting.blogspot.com
Вступление
О чем будем говорить:
• распространенные
• антипаттерны
• автоматического
• модульного
• тестирования
Вступление
Большинство практик написания чистого кода применимо к тестам:
• содержательные имена
• компактные методы/функции
• принцип единственной ответственности
• …
Однако в этом выступлении речь пойдет о тест-специфических паттернах и антипаттернах
Правило Шапокляк
describe PopularityCalculator, "#popular?" do it "should take into account the comment count" do subject.popular?(post).should be_true end end
Правило Шапокляк
describe PopularityCalculator, "#popular?" do it "should take into account the comment count" do subject.popular?(post).should be_true end end
class PopularityCalculator def popular?(post) end end
Правило Шапокляк
it "should take into account the comment count" do posts = (0..20).map { |i| post_with_comment_count i }
posts.each do |post| if 10 < post.comment_count subject.popular?(post).should be_true else subject.popular?(post).should be_false end end end
Правило Шапокляк
Название: Indented Test Code
Ошибка: тестовый код содержит циклы и/или условные конструкции
Мотивация:
• борьба с дублированием
• работы с неконтролируемыми аспектами системы (время, дисковое пространство и т.д.)
Правило Шапокляк
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
Дублирование алгоритма
Используем функциональный код предыдущего примера
class PopularityCalculator THRESHOLD = 10
def popular?(post) THRESHOLD < post.comment_count end end
Дублирование алгоритма
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 == expected
end
Дублирование алгоритма
Название: Test Logic in Prouction
Ошибка: тесты содержат алгоритм, который используется функциональным кодом (часто это copy-paste)
Мотивация: получение актуального значения в тестовом окружении
Дублирование алгоритма
it "should take into account the comment count" do actual = subject.popular? post_with_comment_count(999) actual.should be_true end
Пионер, ты в ответе за всё!
describe NotificationService, "#notify_about" do it "should notify the post author by email" do service.comment_was_added comment end it "should notify the post author by sms" end
Пионер, ты в ответе за всё!
describe NotificationService, "#notify_about" do it "should notify the post author by email" do service.comment_was_added comment end it "should notify the post author by sms" end
class NotificationService < Struct.new(:email_service, :sms_service, :author_repository) def notify_about(comment) end end
Пионер, ты в ответе за всё!
before do @author, @author_repository, @email_service = mock, mock, mockend
it "should notify the post author by email" do @author_repository.expects(:get).returns @author @email_service.expects(:deliver_new_comment_email) .with @comment, @author @sms_service.expects :deliver_new_comment_sms
notification_service.notify_about @comment end it "should notify the post author by sms" do @author_repository.expects(:get).returns @author @email_service.expects :deliver_new_comment_email @sms_service.expects(:deliver_new_comment_sms) .with @comment, @author
notification_service.notify_about @comment end
Пионер, ты в ответе за всё!
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
Ошибка: моки используются вместо стабов
Причина: непонимание разницы между моками и стабами
Пионер, ты в ответе за всё!
before do @author_repository = stub ... @sms_service = stub ... @email_service = stub ... end
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
“Реальная” фикстура
describe PostRepository, "#popular" do it "should return all popular posts" do repository.popular.should include(popular_posts) end end
“Реальная” фикстура
describe PostRepository, "#popular" do it "should return all popular posts" do repository.popular.should include(popular_posts) end end
class PostRepository def popular all_posts.select { true } end end
“Реальная” фикстура
before 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 postsend it "should return all popular posts" do actual = @repository.popular actual.should include(@popular_posts.first) actual.should include(@popular_posts.last) end
“Реальная” фикстура
Ошибка: фикстура содержит данных больше, чем это необходимо для конкретного теста
Мотивация:
• попытка воспроизвести "реальные" данные в тестовом окружение
“Реальная” фикстура
it "should return a popular post" do post = build_popular_post repository = PostRepository.new [post] repository.popular.should include(post) end
it "shouldn't return an unpopular post" do post = build_unpopular_post repository = PostRepository.new [post] repository.popular.should_not include(post) end
“Реальная” фикстура
Бенефиты:
• простой setup
• сообщение о падении теста не перегружено лишними данными
• профилактика "медленных" тестов
Ясный красный
describe BullshitProfitCalculator, "#calculate" do it "should return the projected profit" do actual = subject.calculate 'dummy author' actual.should == '$123'.to_money end end
Ясный красный
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
Ясный красный
'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 ==)
Ясный красный
Название: Diagnostics Aren't a First-Class Feature
Ошибка: непонятное сообщение о падающем тесте (многословное или малоинформативное)
Ясный красный
Возможное решение:
module TestMoneyFormatter def inspect format end end
class Money include TestMoneyFormatter end
Ясный красный
Было:
'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 ==)
Стало:
'BullshitProfitCalculator#calculate should return the projected profit' FAILED expected: $123.00, got: $1.00 (using ==)
Ясный красный
Было: "красный -> зеленый -> рефакторинг" Стало: "красный -> ясный красный -> зеленый -> рефакторинг"
Еще антипаттерны
• глобальные фикстуры
• функциональный код, используемый только в тестах
• нарушение изоляции тестов
• зависимости из других слоев приложения
• тестирование кода фреймворка
Рекомендуемая литература
• Экстремальное программирование. Разработка через тестирование, Кент Бек
• Growing Object-Oriented Software, Guided by Tests by Steve Freeman and Nat Pryce