Slide 1

Slide 1 text

TDD & SOLID Mike Nicholaides

Slide 2

Slide 2 text

http://promptworks.com! @promptworks

Slide 3

Slide 3 text

describe Notifier, "#notify" do let(:message_body) do "Hi @#{notifiable_user1.name} @#{notifiable_user2.name}\ @#{ unnotifiable_user.name }" end ! let(:message) { create(:message, body: message_body) } ! it "FB's the message's mentioned users who prefer to be FB'd" do notifiable_user1 = create(:facebook_connected_user, fb_me_on_mention: true) notifiable_user2 = create(:facebook_connected_user, fb_me_on_mention: true) unnotifiable_user = create(:facebook_connected_user, fb_me_on_mention: false) ! notifiable_user1.facebook_connection.stub(put_connections: true) notifiable_user2.facebook_connection.stub(put_connections: true) unnotifiable_user.facebook_connection.stub(put_connections: true) ! Notifier.notify(message) ! expected_message = "#{message.author.name} said #{message_body}" ! expect(notifiable_user1.facebook_connection).to have_received(:put_connections).with("me", "feed", message: expected_message) expect(notifiable_user2.facebook_connection).to have_received(:put_connections).with("me", "feed", message: expected_message) expect(unnotifiable_user.facebook_connection).not_to have_received(:put_connections) end ! it "emails the message's mentioned users who prefer to be emailed" do notifiable_user1 = create(:user, email_me_on_mention: true) notifiable_user2 = create(:user, email_me_on_mention: true) unnotifiable_user = create(:user, email_me_on_mention: false) ! MessageMailer.stub(:deliver_message) ! Notifier.notify(message) ! expected(MessageMailer).to have_received(:deliver_message).with(message, [notifiable_user1, notifiable_user2]) end ! it "SMS's the message's mentioned users who prefer to be SMS'd" do notifiable_user1 = create(:sms_user, sms_me_on_mention: true) notifiable_user2 = create(:sms_user, sms_me_on_mention: true) unnotifiable_user = create(:sms_user, sms_me_on_mention: false) ! SMSGateway.stub(:deliver) ! Notifier.notify(message) ! expected_message = "#{message.author.name} said #{message_body}" ! expect(SMSGateway).to have_received(:deliver).with(expected_message, notifiable_user1.sms_number) expect(SMSGateway).to have_received(:deliver).with(expected_message, notifiable_user2.sms_number) end end

Slide 4

Slide 4 text

class Notifier def self.notify(message) message.mentioned_users. select(&:fb_me_on_mention?). select(&:facebook_connection). each do |user| user.facebook_connection.put_connections( "me", "feed", message: "#{message.author.name} said: #{message.body}" ) end ! email_users = message.mentioned_users. select(&:email_me_on_mention?). select{|u| u.email.present? } MessageMailer.deliver_message(message, email_users) ! message.mentioned_users. select(&:sms_me_on_mention?). select{|u| u.sms_number.present? }. each { |user| SMSGateway.deliver(message, user.sms_number) } end end

Slide 5

Slide 5 text

Test first != good code

Slide 6

Slide 6 text

SOLID

Slide 7

Slide 7 text

Interface Segregation Principle “many client-specific interfaces are better than one general-purpose interface.” ! Robert C. Martin Principles of Object Oriented Design

Slide 8

Slide 8 text

Dependency Inversion Principle “Depend upon abstractions. Do not depend upon concretions.” ! ! Robert C. Martin, Design Principles and Design Patterns

Slide 9

Slide 9 text

Depending on a Concretion def save_my_game if game_changed? File.open('game.txt', 'w') do |file| file.print @game_data end end end ! save_my_game

Slide 10

Slide 10 text

Depending on an Abstraction def save_my_game persistent_store = FileStore.new persistent_store.save(@game_data) if game_changed? end ! save_my_game class FileStore def save(game_data) File.open('game.txt', 'w') do |file| file.print game_data end end end

Slide 11

Slide 11 text

Dependency Inversion Principle hide concretions behind abstractions

Slide 12

Slide 12 text

Open/Closed Principle “Software entities … should be open for extension, but closed for modification” ! Bertrand Meyer, Object Oriented Software Construction

Slide 13

Slide 13 text

Open/Closed Principle • dependencies • collaborators • logic Swappable pieces for

Slide 14

Slide 14 text

Open/Closed Principle • blocks • inheritance • dependency injection Techniques

Slide 15

Slide 15 text

Open/Closed Principle def save_my_game(persistent_store) persistent_store.save(@game_data) if game_changed? end ! save_my_game(FileStore.new)

Slide 16

Slide 16 text

Open/Closed Principle Make entities extendible

Slide 17

Slide 17 text

Liskov Substitution Principle • use duck typing

Slide 18

Slide 18 text

Liskov Substitution Principle def save_my_game(persistent_store) persistent_store.save(@game_data) if game_changed? end

Slide 19

Slide 19 text

Single Responsibility Principle “A class should have one, and only one, reason to change.” ! Robert C. Martin Principles of Object Oriented Design

Slide 20

Slide 20 text

Single Responsibility Principle • classes should be small • classes should do one thing • we should have lots of classes

Slide 21

Slide 21 text

SOLID • hide concretions behind abstractions • make entities extendible • use duck typing • classes should do one thing

Slide 22

Slide 22 text

message.author # => User @JesseKatsopolis ! message.body # => "@DannyTanner, @JoeyGladstone, Watch the hair!" ! message.mentioned_users # => [ User @DannyTanner, User @JoeyGladstone ]

Slide 23

Slide 23 text

# ... ! if @message.save ! Notifier.notify(@message) ! redirect_to @message else ! # ...

Slide 24

Slide 24 text

describe Notifier, ".notify" do ! it "emails the message's mentioned users who prefer to be emailed" it "SMS's the message's mentioned users who prefer to be SMS'd" it "FB's the message's mentioned users who prefer to be FB'd" ! end

Slide 25

Slide 25 text

Details • what it does • what is knows

Slide 26

Slide 26 text

Responsibilities • email users • SMS users • FB users ! Concretions • the message • the mentioned users • how to determine if someone wants to be emailed • how to determine if someone wants to be SMS'd • how to determine if someone wants to be FB'd


Slide 27

Slide 27 text

Responsibilities Described it "emails the message's mentioned users who prefer to be emailed" it "SMS's the message's mentioned users who prefer to be SMS'd" it "FB's the message's mentioned users who prefer to be FB'd"

Slide 28

Slide 28 text

Strained Language It emails the messages' mentioned users who prefer to be emailed.

Slide 29

Slide 29 text

it "FB's the message's mentioned users who prefer to be FB'd" do ! notifiable_user1 = create(:facebook_connected_user, fb_me_on_mention: true) notifiable_user2 = create(:facebook_connected_user, fb_me_on_mention: true) unnotifiable_user = create(:facebook_connected_user, fb_me_on_mention: false) ! message_body = "Hi @#{notifiable_user1.name} @#{notifiable_user2.name}\ @#{ unnotifiable_user.name }" ! message = create(:message, body: message_body) ! notifiable_user1.facebook_connection.stub(put_connections: true) notifiable_user2.facebook_connection.stub(put_connections: true) unnotifiable_user.facebook_connection.stub(put_connections: true) ! Notifier.notify(message) ! expected_message = "#{message.author.name} said #{message_body}" ! expect(notifiable_user1.facebook_connection). to have_received(:put_connections). with("me", "feed", message: expected_message) ! expect(notifiable_user2.facebook_connection). to have_received(:put_connections). with("me", "feed", message: expected_message) ! expect(unnotifiable_user.facebook_connection). not_to have_received(:put_connections) ! end

Slide 30

Slide 30 text

too many details == hard to test

Slide 31

Slide 31 text

describe Notifier, "#notify" do let(:message_body) do "Hi @#{notifiable_user1.name} @#{notifiable_user2.name}\ @#{ unnotifiable_user.name }" end ! let(:message) { create(:message, body: message_body) } ! it "FB's the message's mentioned users who prefer to be FB'd" do notifiable_user1 = create(:facebook_connected_user, fb_me_on_mention: true) notifiable_user2 = create(:facebook_connected_user, fb_me_on_mention: true) unnotifiable_user = create(:facebook_connected_user, fb_me_on_mention: false) ! notifiable_user1.facebook_connection.stub(put_connections: true) notifiable_user2.facebook_connection.stub(put_connections: true) unnotifiable_user.facebook_connection.stub(put_connections: true) ! Notifier.notify(message) ! expected_message = "#{message.author.name} said #{message_body}" ! expect(notifiable_user1.facebook_connection).to have_received(:put_connections).with("me", "feed", message: expected_message) expect(notifiable_user2.facebook_connection).to have_received(:put_connections).with("me", "feed", message: expected_message) expect(unnotifiable_user.facebook_connection).not_to have_received(:put_connections) end ! it "emails the message's mentioned users who prefer to be emailed" do notifiable_user1 = create(:user, email_me_on_mention: true) notifiable_user2 = create(:user, email_me_on_mention: true) unnotifiable_user = create(:user, email_me_on_mention: false) ! MessageMailer.stub(:deliver_message) ! Notifier.notify(message) ! expected(MessageMailer).to have_received(:deliver_message).with(message, [notifiable_user1, notifiable_user2]) end ! it "SMS's the message's mentioned users who prefer to be SMS'd" do notifiable_user1 = create(:sms_user, sms_me_on_mention: true) notifiable_user2 = create(:sms_user, sms_me_on_mention: true) unnotifiable_user = create(:sms_user, sms_me_on_mention: false) ! SMSGateway.stub(:deliver) ! Notifier.notify(message) ! expected_message = "#{message.author.name} said #{message_body}" ! expect(SMSGateway).to have_received(:deliver).with(expected_message, notifiable_user1.sms_number) expect(SMSGateway).to have_received(:deliver).with(expected_message, notifiable_user2.sms_number) end end

Slide 32

Slide 32 text

class Notifier def self.notify(message) message.mentioned_users. select(&:fb_me_on_mention?). select(&:facebook_connection). each do |user| user.facebook_connection.put_connections( "me", "feed", message: "#{message.author.name} said: #{message.body}" ) end ! email_users = message.mentioned_users. select(&:email_me_on_mention?). select{|u| u.email.present? } MessageMailer.deliver_message(message, email_users) ! message.mentioned_users. select(&:sms_me_on_mention?). select{|u| u.sms_number.present? }. each { |user| SMSGateway.deliver(message, user.sms_number) } end end

Slide 33

Slide 33 text

Guidelines for Pain-Free Testing ! • defer responsibilities to other classes • assume good abstractions

Slide 34

Slide 34 text

it "emails the message's mentioned users who prefer to be emailed" it "SMS's the message's mentioned users who prefer to be SMS'd" it "FB's the message's mentioned users who prefer to be FB'd"

Slide 35

Slide 35 text

it "______ the message's mentioned users who prefer to be ______" it "______ the message's mentioned users who prefer to be ______" it "______ the message's mentioned users who prefer to be ______"

Slide 36

Slide 36 text

it "______ the message's mentioned users who prefer to be ______"

Slide 37

Slide 37 text

it "notifies via SMS/FB/Email the message's mentioned users who prefer to be notified via SMS/FB/Email, respectively"

Slide 38

Slide 38 text

it "notifies the message's mentioned users via their preferred notification channels"

Slide 39

Slide 39 text

it "notifies the message's mentioned users via notification channels"

Slide 40

Slide 40 text

it "notifies mentioned users via notification channels"

Slide 41

Slide 41 text

it "notifies mentioned users via notification channels" do ! message = double('message') channels = [ double(notify_about_message: nil), double(notify_about_message: nil) ] notifier = Notifier.new(channels) ! notifier.notify(message) ! channel[0].should have_received(:notify_about_message).with message channel[1].should have_received(:notify_about_message).with message ! end

Slide 42

Slide 42 text

class Notifier ! def initialize(channels) @channels = channels end ! def notify(message) @channels.each do |channel| channel.notify_about_message(message) end end ! end ! notifier = Notifier.new(channels) notifier.notify(message)

Slide 43

Slide 43 text

channels = case Rails.env when 'production' [ EmailChannel.new, SMSChannel.new, NSAChannel.new ] when 'staging' [ TestEmailChannel.new, TestSMSChannel.new ] else [ LoggingChannel.new ] end ! config.notifier = Notifier.new( channels )

Slide 44

Slide 44 text

it "emails the message's mentioned users who prefer email" do ! message = build(:message, mentioned_users: [ user_who_likes_email1 = build(:user, email_on_mention: true), user_who_likes_email2 = build(:user, email_on_mention: true), user_who_dislikes_email = build(:user, email_on_mention: false) ]) MessageMailer.stub(:deliver_message) ! EmailChannel.new.notify(message) ! MessageMailer.should have_received(:deliver_message) .with(message, [user_who_likes_email1, user_who_likes_email2]) ! end

Slide 45

Slide 45 text

- notify via channels Notifier - how to notify by email - who prefers to be emailed Email Channel - how to notify by SMS - who prefers to be SMS'd SMS Channel - who prefers to be Facebooked - how to notify by Facebook Facebook Channel - which channels < Configuration > - which users are mentioned Message - email address - Facebook Credentials - SMS number User - notify by email - notify by SMS - notify by Facebook - how to notify by email - who prefers to be emailed - how to notify by SMS - who prefers to be SMS'd - who prefers to be Facebooked - how to notify by Facebook Notifier - which users are mentioned Message - email address - Facebook Credentials - SMS number User

Slide 46

Slide 46 text

@nicholaides mike@promptworks.com