Слайды к докладу на встрече "Донецкий кофе-и-код"
Дата: 18 сентября 2010
Подробности: http://cnc.dn.ua/meeting/september-2010-meeting-unit-tests-antipatterns-spravochnik.html
2. Вступление
О чем будем говорить:
• распространенные
• антипаттерны
• автоматического
• модульного
• тестирования
3. Вступление
Большинство практик написания чистого кода применимо к
тестам:
• содержательные имена
• компактные методы/функции
• принцип единственной ответственности
• …
Однако в этом выступлении речь пойдет о тест-специфических
паттернах и антипаттернах
5. Правило Шапокляк
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
6. Правило Шапокляк
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. Правило Шапокляк
Название: Indented Test Code
Ошибка: тестовый код содержит циклы и/или условные
конструкции
Мотивация:
• борьба с дублированием
• работы с неконтролируемыми аспектами системы (время,
дисковое пространство и т.д.)
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
13. Дублирование алгоритма
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
14. Дублирование алгоритма
Название: Test Logic in Prouction
Ошибка: тесты содержат алгоритм, который используется
функциональным кодом (часто это copy-paste)
Мотивация: получение актуального значения в тестовом
окружении
16. Дублирование алгоритма
it "should take into account the comment count" do
actual = subject.popular? post_with_comment_count(999)
actual.should be_true
end
18. Пионер, ты в ответе за всё!
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
19. Пионер, ты в ответе за всё!
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
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. Пионер, ты в ответе за всё!
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>)
22. Пионер, ты в ответе за всё!
Название: Too Many Expectations
Ошибка: моки используются вместо стабов
Причина: непонимание разницы между моками и стабами
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. Пионер, ты в ответе за всё!
Бенефиты: одна ошибка -- один падающий тест
27. “Реальная” фикстура
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
28. “Реальная” фикстура
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
29. “Реальная” фикстура
Ошибка: фикстура содержит данных больше, чем это
необходимо для конкретного теста
Мотивация:
• попытка воспроизвести "реальные" данные в тестовом
окружение
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
• сообщение о падении теста не перегружено лишними
данными
• профилактика "медленных" тестов
33. Ясный красный
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
34. Ясный красный
'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 ==)
35. Ясный красный
Название: Diagnostics Aren't a First-Class Feature
Ошибка: непонятное сообщение о падающем тесте
(многословное или малоинформативное)
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
• мокание методов тестируемого модуля
• мокание объектов-значений
42. Рекомендуемая литература
• Экстремальное программирование. Разработка через тестирование,
Кент Бек
• Growing Object-Oriented Software, Guided by Tests by Steve Freeman
and Nat Pryce