Антипаттерны модульного тестирования (Донецкий кофе-и-код Сентябрь 2010)

  • 743 views
Uploaded on

Слайды к докладу на встрече "Донецкий кофе-и-код" …

Слайды к докладу на встрече "Донецкий кофе-и-код"

Дата: 18 сентября 2010
Подробности: http://cnc.dn.ua/meeting/september-2010-meeting-unit-tests-antipatterns-spravochnik.html

More in: Technology
  • Full Name Full Name Comment goes here.
    Are you sure you want to
    Your message goes here
    Be the first to comment
No Downloads

Views

Total Views
743
On Slideshare
0
From Embeds
0
Number of Embeds
1

Actions

Shares
Downloads
2
Comments
0
Likes
1

Embeds 0

No embeds

Report content

Flagged as inappropriate Flag as inappropriate
Flag as inappropriate

Select your reason for flagging this presentation as inappropriate.

Cancel
    No notes for slide

Transcript

  • 1. Вступление О себе: • Ruby on Rails разработчик • 4 года практики в стиле test-driven development • http://novembermeeting.blogspot.com
  • 2. Вступление О чем будем говорить: • распространенные • антипаттерны • автоматического • модульного • тестирования
  • 3. Вступление Большинство практик написания чистого кода применимо к тестам: • содержательные имена • компактные методы/функции • принцип единственной ответственности • … Однако в этом выступлении речь пойдет о тест-специфических паттернах и антипаттернах
  • 4. Правило Шапокляк Название: Indented Test Code Описание: тестовый код содержит циклы и/или условные конструкции Мотивация: • борьба с дублированием • работы с неконтролируемыми аспектами системы (время, дисковое пространство и т.д.)
  • 5. Правило Шапокляк Начинаем с тестового кода describe PopularityCalculator, "#popular?" do it "should take into account the comment count" do subject.popular?(post).should be_true end end
  • 6. Правило Шапокляк Начинаем с тестового кода 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) true end end
  • 7. Правило Шапокляк И через минуту имеем тест 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 Обратите внимание на цикл и условную конструкцию в теле тестового метода (красный шрифт)
  • 8. Правило Шапокляк Добавляем функциональный код для теста class PopularityCalculator THRESHOLD = 10 def popular?(post) THRESHOLD < post.comment_count end end
  • 9. Правило Шапокляк Это хорошо ... хорошо, что Вы зеленый и плоский
  • 10. Правило Шапокляк Правильно написанный тестовый метод 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
  • 11. Правило Шапокляк Бенефиты: • тесты проще понять • тесты содержат меньше ошибок
  • 12. Дублирование алгоритма Название: Prouction Logic in Test Описание: тесты содержат алгоритм, который используется функциональным кодом (часто это copy-paste) Мотивация: получение актуального значения в тестовом окружении
  • 13. Дублирование алгоритма Используем функциональный код предыдущего примера class PopularityCalculator THRESHOLD = 10 def popular?(post) THRESHOLD < post.comment_count end end
  • 14. Дублирование алгоритма Вторая строка теста содержит алгоритм функционального кода 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
  • 15. Дублирование алгоритма
  • 16. Дублирование алгоритма Правильная реализация теста it "should take into account the comment count" do actual = subject.popular? post_with_comment_count(11) actual.should be_true end
  • 17. Дублирование алгоритма Бенефиты: все выгоды тестирования методом черного ящика
  • 18. Пионер, ты в ответе за всё! Название: Too Many Expectations Описание: моки используются вместо стабов Причина: непонимание разницы между моками и стабами
  • 19. Пионер, ты в ответе за всё! Начинаем писать тест describe NotificationService, "#notify_about" do it "should notify the post author by email" do service.notify_about 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
  • 20. Пионер, ты в ответе за всё! before do @author, @author_repository, @email_service = mock, mock, mock end 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
  • 21. Пионер, ты в ответе за всё! Закомментируем вызов одного из сервисов в функциональном коде def notify_about(comment) author = author_repository.get comment.post.author_id # email_service.deliver_new_comment_email( # comment, author) sms_service.deliver_new_comment_sms comment, author end
  • 22. Пионер, ты в ответе за всё! Был нарушен только один аспект поведения функционального кода, а упали оба теста 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>)
  • 23. Пионер, ты в ответе за всё!
  • 24. Пионер, ты в ответе за всё! Правильная реализация 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
  • 25. Пионер, ты в ответе за всё! Бенефиты: одна ошибка -- один падающий тест
  • 26. “Реальная” фикстура Описание: фикстура содержит данных больше, чем это необходимо для конкретного теста Мотивация: • попытка воспроизвести "реальные" данные в тестовом окружение
  • 27. “Реальная” фикстура Опять начинаем с тестового кода describe PostRepository, "#popular" do it "should return all popular posts" do repository.popular.should include(popular_posts) end end
  • 28. “Реальная” фикстура Опять начинаем с тестового кода 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
  • 29. “Реальная” фикстура Воспроизводим антипаттерн 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 posts end it "should return all popular posts" do actual = @repository.popular actual.should include(@popular_posts.first) actual.should include(@popular_posts.last) end
  • 30. “Реальная” фикстура Правильно написанный тест 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
  • 31. “Реальная” фикстура Бенефиты: • простой setup • сообщение о падении теста не перегружено лишними данными • профилактика "медленных" тестов
  • 32. Ясный красный Название: Diagnostics Aren't a First-Class Feature Ошибка: непонятное сообщение о падающем тесте (многословное или малоинформативное)
  • 33. Ясный красный Тест describe BullshitProfitCalculator, "#calculate" do it "should return the projected profit" do actual = subject.calculate 'dummy author' actual.should == '$123'.to_money end end
  • 34. Ясный красный Тест 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
  • 35. Ясный красный Сообщение об ошибке '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 ==)
  • 36. Ясный красный Возможное решение module TestMoneyFormatter def inspect format end end class Money include TestMoneyFormatter end
  • 37. Ясный красный Было: '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 ==)
  • 38. Ясный красный Было: "красный -> зеленый -> рефакторинг" Стало: "красный -> ясный красный -> зеленый -> рефакторинг"
  • 39. Еще антипаттерны • глобальные фикстуры • функциональный код, используемый только в тестах • нарушение изоляции тестов • зависимости из других слоев приложения • тестирование кода фреймворка
  • 40. Антипаттерны в mocking TDD • мокание методов тестируемого модуля • мокание объектов-значений
  • 41. И еще • “медленные” тесты • …
  • 42. Рекомендуемая литература • Экстремальное программирование. Разработка через тестирование, Кент Бек • Growing Object-Oriented Software, Guided by Tests by Steve Freeman and Nat Pryce
  • 43. Исходный код примеров http://github.com/MitinPavel/test_antipatterns.git
  • 44. Использованные изображения • http://www.content.su/?p=318 • http://www.inquisitr.com/39089/former-police-officer-sues-for- discrimination-over-his-alcoholism-disability/ • http://www.skaskin.com/