+ All Categories
Home > Technology > Антипаттерны модульного тестирования (Донецкий...

Антипаттерны модульного тестирования (Донецкий...

Date post: 01-Jul-2015
Category:
Upload: mitinpavel
View: 837 times
Download: 4 times
Share this document with a friend
Description:
Слайды к докладу на встрече "Донецкий кофе-и-код" Дата: 18 сентября 2010 Подробности: http://cnc.dn.ua/meeting/september-2010-meeting-unit-tests-antipatterns-spravochnik.html
44
Обновленная версия доклада: http://www.slideshare.net/MitinPavel/rubyconfua-2010-unittestantipatterns Вступление О себе: Ruby on Rails разработчик 4 года практики в стиле test-driven development http://novembermeeting.blogspot.com
Transcript

Обновленная версия доклада: 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

Правило Шапокляк

THRESHOLD = 10

def popular?(post) THRESHOLD < post.comment_countend

Правило Шапокляк

Название: 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 ==)

Ясный красный

Было: "красный -> зеленый -> рефакторинг" Стало: "красный -> ясный красный -> зеленый -> рефакторинг"

Еще антипаттерны

• глобальные фикстуры

• функциональный код, используемый только в тестах

• нарушение изоляции тестов

• зависимости из других слоев приложения

• тестирование кода фреймворка

Антипаттерны в mocking TDD

• мокание методов тестируемого модуля

• мокание объектов-значений

И еще

• “медленные” тесты

• …

Рекомендуемая литература

• Экстремальное программирование. Разработка через тестирование, Кент Бек

• Growing Object-Oriented Software, Guided by Tests by Steve Freeman and Nat Pryce

Исходный код примеров

http://github.com/MitinPavel/test_antipatterns.git

Использованные изображения

• 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/


Recommended