Slide 1

Slide 1 text

SMELLS http://www.flickr.com/photos/kanaka/3480201136/ WHY OUR CODE

Slide 2

Slide 2 text

github.com/bkeepers @bkeepers

Slide 3

Slide 3 text

I AM TIRED OF WRITING BAD CODE.

Slide 4

Slide 4 text

I AM TIRED OF MAINTAINING BAD CODE.

Slide 5

Slide 5 text

Kent Beck Smalltalk Best Practice Patterns “Code doesn’t lie. If you’re not listening, you won’t hear the truths it tells.”

Slide 6

Slide 6 text

A CODE SMELL USUALLY INDICATES A DEEPER PROBLEM IN THE SYSTEM.

Slide 7

Slide 7 text

No content

Slide 8

Slide 8 text

CODE SMELLS ARE HEURISTICS TO SUGGEST WHEN TO REFACTOR AND WHAT TECHNIQUES TO USE.

Slide 9

Slide 9 text

DIVERGENT CHANGE

Slide 10

Slide 10 text

DIVERGENT CHANGE Occurs when one class is commonly changed in different ways for different reasons. Any change to handle a variation should change a single class.

Slide 11

Slide 11 text

DIVERGENT CHANGE Occurs when one class is commonly changed in different ways for different reasons. Any change to handle a variation should change a single class. Refactoring: Identify everything that changes for a particular cause and use Extract Class to put them all together.

Slide 12

Slide 12 text

OUR CODE SMELLS IN THE 21 WAYS THAT BECK AND FOWLER DESCRIBE, BUT…

Slide 13

Slide 13 text

our code smells when UNIT TESTS ARE COMPLEX & SLOW. IT IS TIGHTLY COUPLED TO A FRAMEWORK.

Slide 14

Slide 14 text

our code smells when UNIT TESTS ARE COMPLEX.

Slide 15

Slide 15 text

TDD IS A DESIGN PROCESS.

Slide 16

Slide 16 text

WHY DO WE WRITE TESTS?

Slide 17

Slide 17 text

WHY DO WE WRITE TESTS? 1.Guard against regressions

Slide 18

Slide 18 text

WHY DO WE WRITE TESTS? 1.Guard against regressions 2.Gain confidence to change

Slide 19

Slide 19 text

WHY DO WE WRITE TESTS? 1.Guard against regressions 2.Gain confidence to change 3.Discover better designs

Slide 20

Slide 20 text

WARNING Excessive use of quotations ahead.

Slide 21

Slide 21 text

Steve Freeman, Nat Pryce Growing Object-Oriented Software, Guided by Tests “We find that the effort of writing a test first also gives us rapid feedback about the quality of our design ideas—that making code accessible for testing often drives it towards being cleaner and more modular.”

Slide 22

Slide 22 text

unit tests can be complex when OBJECTS ARE TOO TIGHTLY COUPLED.

Slide 23

Slide 23 text

Exhibit A: GitHub Notifications context Notifications::Emails::Message do test "#to uses global email" do @settings.email :global, '[email protected]' assert_equal '[email protected]', @message.to end test "#body includes comment body" do assert_match @comment.body, @message.body end end

Slide 24

Slide 24 text

Exhibit A: GitHub Notifications context Notifications::Emails::Message do setup do @comment = Issue.make @summary = Notifications::Summary.from(@comment) @handlers = [Notifications::EmailHandler.new] @delivery = Notifications::Delivery.new( @summary, @comment, @handlers) @settings = Notifications::Settings.new(-1) @message = Notifications::Emails::Message.new( @delivery, @settings) end test "#to uses global email" do @settings.email :global, '[email protected]' assert_equal '[email protected]', @message.to end

Slide 25

Slide 25 text

Steve Freeman, Nat Pryce Growing Object-Oriented Software, Guided by Tests “When we find a feature that’s difficult to test, we don’t just ask ourselves how to test it, but also why is it difficult to test.”

Slide 26

Slide 26 text

Exhibit A: GitHub Notifications context Notifications::Emails::Message do setup do @comment = Issue.make @summary = Notifications::Summary.from(@comment) @handlers = [Notifications::EmailHandler.new] @delivery = Notifications::Delivery.new( @summary, @comment, @handlers) @settings = Notifications::Settings.new(-1) @message = Notifications::Emails::Message.new( @delivery, @settings) end test "#to uses global email" do @settings.email :global, '[email protected]' assert_equal '[email protected]', @message.to end end

Slide 27

Slide 27 text

Exhibit A: GitHub Notifications context Notifications::Emails::Message do setup do @comment = Issue.make @settings = Notifications::Settings.new(-1) @message = Notifications::Emails::Message.new( @comment, @settings) end test "#to uses global email" do @settings.email :global, '[email protected]' assert_equal '[email protected]', @message.to end end

Slide 28

Slide 28 text

Exhibit A: GitHub Notifications context Notifications::Emails::Message do setup do @comment = Issue.make @settings = Notifications::Settings.new(-1) @message = Notifications::Emails::Message.new( @comment, @settings) end test "#to uses global email" do @settings.email :global, '[email protected]' assert_equal '[email protected]', @message.to end test "#body includes comment body" do assert_match @comment.body, @message.body end end

Slide 29

Slide 29 text

unit tests can be complex when OBJECTS ARE DOING TOO MUCH.

Slide 30

Slide 30 text

Steve Freeman, Nat Pryce Growing Object-Oriented Software, Guided by Tests “An element’s cohesion is a measure of whether its responsibilities form a meaningful unit…Think of a machine that washes both clothes and dishes —it’s unlikely to do both well.”

Slide 31

Slide 31 text

Every class should have a single responsibility, and that responsibility should be entirely encapsulated by the class. SINGLE RESPONSIBILITY PRINCIPLE

Slide 32

Slide 32 text

Steve Freeman, Nat Pryce Growing Object-Oriented Software, Guided by Tests “Our heuristic is that we should be able to describe what an object does without using any conjunctions (‘and,’ ‘or’).”

Slide 33

Slide 33 text

jQuery(function($) { $('#new-status').on('submit', function() { $.ajax({ url: '/statuses', type: 'POST', dataType: 'json', data: {text: $(this).find('textarea').val()}, success: function(data) { $('#statuses').append('
  • ' + data.text + '
  • '); } }); return false; }); }); example lovingly stolen from @searls

    Slide 34

    Slide 34 text

    The test tells me the code is complex. describe("Updating my status", function() { var $form, $statuses; beforeEach(function(){ $form = affix('form#new-status'); $form.affix('textarea').val('sniffing code'); $statuses = affix('#statuses'); spyOn($, "ajax"); $form.trigger('submit'); }); it("posts status to the server", function() { expect($.ajax).toHaveBeenCalledWith({ url: '/statuses', data: {text: 'sniffing code'}, success: jasmine.any(Function) }); });

    Slide 35

    Slide 35 text

    The test tells me the code is complex. }); it("posts status to the server", function() { expect($.ajax).toHaveBeenCalledWith({ url: '/statuses', data: {text: 'sniffing code'}, success: jasmine.any(Function) }); }); describe("with a successful response", function() { beforeEach(function() { $.ajax.mostRecentCall.args[0].success({ text: "This is starting stink!" }); }); it("appends text", function() { expect($statuses).toHaveHtml( '
    This is starting stink!
    '); }); }); });

    Slide 36

    Slide 36 text

    Why is this hard to test? jQuery(function($) { $('#new-status').on('submit', function() { $.ajax({ url: '/statuses', type: 'POST', dataType: 'json', data: {text: $(this).find('textarea').val()}, success: function(data) { $('#statuses').append('
  • ' + data.text + '
  • '); } }); return false; }); }); example lovingly stolen from @searls

    Slide 37

    Slide 37 text

    Why is this hard to test? jQuery(function($) { $('#new-status').on('submit', function() { $.ajax({ url: '/statuses', type: 'POST', dataType: 'json', data: {text: $(this).find('textarea').val()}, success: function(data) { $('#statuses').append('
  • ' + data.text + '
  • '); } }); return false; }); }); 1. page event example lovingly stolen from @searls

    Slide 38

    Slide 38 text

    Why is this hard to test? jQuery(function($) { $('#new-status').on('submit', function() { $.ajax({ url: '/statuses', type: 'POST', dataType: 'json', data: {text: $(this).find('textarea').val()}, success: function(data) { $('#statuses').append('
  • ' + data.text + '
  • '); } }); return false; }); }); 1. page event 2. user event example lovingly stolen from @searls

    Slide 39

    Slide 39 text

    Why is this hard to test? jQuery(function($) { $('#new-status').on('submit', function() { $.ajax({ url: '/statuses', type: 'POST', dataType: 'json', data: {text: $(this).find('textarea').val()}, success: function(data) { $('#statuses').append('
  • ' + data.text + '
  • '); } }); return false; }); }); 1. page event 2. user event 3. network IO example lovingly stolen from @searls

    Slide 40

    Slide 40 text

    Why is this hard to test? jQuery(function($) { $('#new-status').on('submit', function() { $.ajax({ url: '/statuses', type: 'POST', dataType: 'json', data: {text: $(this).find('textarea').val()}, success: function(data) { $('#statuses').append('
  • ' + data.text + '
  • '); } }); return false; }); }); 1. page event 2. user event 3. network IO 4. user input example lovingly stolen from @searls

    Slide 41

    Slide 41 text

    Why is this hard to test? jQuery(function($) { $('#new-status').on('submit', function() { $.ajax({ url: '/statuses', type: 'POST', dataType: 'json', data: {text: $(this).find('textarea').val()}, success: function(data) { $('#statuses').append('
  • ' + data.text + '
  • '); } }); return false; }); }); 1. page event 2. user event 3. network IO 4. user input 5. network event example lovingly stolen from @searls

    Slide 42

    Slide 42 text

    Why is this hard to test? jQuery(function($) { $('#new-status').on('submit', function() { $.ajax({ url: '/statuses', type: 'POST', dataType: 'json', data: {text: $(this).find('textarea').val()}, success: function(data) { $('#statuses').append('
  • ' + data.text + '
  • '); } }); return false; }); }); 1. page event 2. user event 3. network IO 4. user input 5. network event 6. HTML templating example lovingly stolen from @searls

    Slide 43

    Slide 43 text

    jQuery(function($) { $('#new-status').on('submit', function() { $.ajax({ url: '/statuses', type: 'POST', dataType: 'json', data: {text: $(this).find('textarea').val()}, success: function(data) { $('#statuses').append('
  • ' + data.text + '
  • '); } }); return false; }); }); So we start to refactor…

    Slide 44

    Slide 44 text

    jQuery(function($) { $('#new-status').on('submit', function() { $.ajax({ url: '/statuses', type: 'POST', dataType: 'json', data: {text: $(this).find('textarea').val()}, success: function(data) { $('#statuses').append('
  • ' + data.text + '
  • '); } }); return false; }); }); Refactor to use a model

    Slide 45

    Slide 45 text

    Refactor to use a model jQuery(function($) { var statuses = new Collection.Statuses(); $('#new-status').on('submit', function() { statuses.create({text: $(this).find('textarea').val()}); return false; }); statuses.on('add', function(status) { $('#statuses').append( '
  • ' + status.get('text') + '
  • '); }); });

    Slide 46

    Slide 46 text

    Refactor to use a model jQuery(function($) { var statuses = new Collection.Statuses(); $('#new-status').on('submit', function() { statuses.create({text: $(this).find('textarea').val()}); return false; }); statuses.on('add', function(status) { $('#statuses').append( '
  • ' + status.get('text') + '
  • '); }); }); RESPONSIBILITY: Sync state with server

    Slide 47

    Slide 47 text

    Refactor handling of user input jQuery(function($) { var statuses = new Collection.Statuses(); $('#new-status').on('submit', function() { statuses.create({text: $(this).find('textarea').val()}); return false; }); statuses.on('add', function(status) { $('#statuses').append( '
  • ' + status.get('text') + '
  • '); }); });

    Slide 48

    Slide 48 text

    Refactor handling of user input jQuery(function($) { var statuses = new Collection.Statuses(); new View.PostStatus({collection: statuses}); statuses.on('add', function(status) { $('#statuses').append( '
  • ' + status.get('text') + '
  • '); }); });

    Slide 49

    Slide 49 text

    Refactor handling of user input jQuery(function($) { var statuses = new Collection.Statuses(); new View.PostStatus({collection: statuses}); statuses.on('add', function(status) { $('#statuses').append( '
  • ' + status.get('text') + '
  • '); }); }); RESPONSIBILITY: Create statuses from user input

    Slide 50

    Slide 50 text

    jQuery(function($) { var statuses = new Collection.Statuses(); new View.PostStatus({collection: statuses}); statuses.on('add', function(status) { $('#statuses').append( '
  • ' + status.get('text') + '
  • '); }); }); Refactor templating

    Slide 51

    Slide 51 text

    Refactor templating jQuery(function($) { var statuses = new Collection.Statuses(); new View.PostStatus({collection: statuses}); new View.StatusList({collection: statuses}); });

    Slide 52

    Slide 52 text

    Refactor templating jQuery(function($) { var statuses = new Collection.Statuses(); new View.PostStatus({collection: statuses}); new View.StatusList({collection: statuses}); }); RESPONSIBILITY: Render statuses to the page

    Slide 53

    Slide 53 text

    jQuery(function($) { var statuses = new Collection.Statuses(); new View.PostStatus({collection: statuses}); new View.StatusList({collection: statuses}); }); RESPONSIBILITY: Initialize application on page load

    Slide 54

    Slide 54 text

    Our tests only have one concern describe("View.StatusList", function() { beforeEach(function() { $el = $('
      '); collection = new Backbone.Collection(); view = new View.StatusList({ el: $el, collection: collection }); }); it("appends newly added items", function() { collection.add({text: 'this is fun!'}); expect($el.find('li').length).toBe(1); expect($el.find('li').text()).toEqual('this is fun!'); }); });

      Slide 55

      Slide 55 text

      PAY ATTENTION TO TESTING PAINS AND ADJUST THE DESIGN ACCORDINGLY.

      Slide 56

      Slide 56 text

      Steve Freeman, Nat Pryce Growing Object-Oriented Software, Guided by Tests Poor quality tests can slow development to a crawl, and poor internal quality of the system being tested will result in poor quality tests.

      Slide 57

      Slide 57 text

      Christian Johansen Test-Driven JavaScript Development “If you write bad unit tests, you might find that you gain none of the benefits, and instead are stuck with a bunch of tests that are time-consuming and hard to maintain.”

      Slide 58

      Slide 58 text

      our code smells when UNIT TESTS ARE SLOW.

      Slide 59

      Slide 59 text

      $ bx rake test:units ............................................................................................... ............................................................................................... ............................................................................................... ............................................................................................... ............................................................................................... ............................................................................................... ............................................................................................... ............................................................................................... ............................................................................................... ............................................................................................... ............................................................................................... ............................................................................................... ............................................................................................... ............................................................................................... ............................................................................................... ............................................................................................... ............................................................................................... ............................................................................................... ............................................................................................... ............................................................................................... ............................................................................................... ............................................................................................... ............................................................................................... ........................................................................................ Finished in 274.623286 seconds. 2273 tests, 6765 assertions, 0 failures, 0 errors

      Slide 60

      Slide 60 text

      WHAT’S WRONG WITH SLOW TESTS?

      Slide 61

      Slide 61 text

      You don't run them often WHAT’S WRONG WITH SLOW TESTS?

      Slide 62

      Slide 62 text

      You don't run them often You waste time waiting for tests WHAT’S WRONG WITH SLOW TESTS?

      Slide 63

      Slide 63 text

      You don't run them often You waste time waiting for tests You distracted others while waiting WHAT’S WRONG WITH SLOW TESTS?

      Slide 64

      Slide 64 text

      You don't run them often You waste time waiting for tests You distracted others while waiting WHAT’S WRONG WITH SLOW TESTS? http://xkcd.com/303/

      Slide 65

      Slide 65 text

      You don't run them often You waste time waiting for tests You distracted others while waiting You commit failing changes WHAT’S WRONG WITH SLOW TESTS?

      Slide 66

      Slide 66 text

      You don't run them often You waste time waiting for tests You distracted others while waiting You commit failing changes You lose the rapid feedback cycle WHAT’S WRONG WITH SLOW TESTS?

      Slide 67

      Slide 67 text

      unit tests can be slow when they INTERACT WITH SLOW COMPONENTS.

      Slide 68

      Slide 68 text

      context Notifications::Emails::Message do setup do @comment = Issue.make! # create record in database @settings = Notifications::Settings.new(-1) @message = Notifications::Emails::Message.new( @comment, @settings) end test "#to uses global email" do @settings.email :global, '[email protected]' assert_equal '[email protected]', @message.to end test "#body includes comment body" do assert_match @comment.body, @message.body end end

      Slide 69

      Slide 69 text

      $ ruby test/unit/notifications/emails/message_test.rb ................... Finished in 3.517926 seconds. 19 tests, 24 assertions, 0 failures, 0 errors

      Slide 70

      Slide 70 text

      context Notifications::Emails::Message do setup do @comment = Issue.make # create in memory @comment.id = -1 # make it appear persisted @settings = Notifications::Settings.new(-1) @message = Notifications::Emails::Message.new( @comment, @settings) end test "#to uses global email" do @settings.email :global, '[email protected]' assert_equal '[email protected]', @message.to end test "#body includes comment body" do assert_match @comment.body, @message.body end

      Slide 71

      Slide 71 text

      $ ruby test/unit/notifications/emails/message_test.rb ................... Finished in 0.073752 seconds. 19 tests, 24 assertions, 0 failures, 0 errors

      Slide 72

      Slide 72 text

      3.517926 ÷ 0.073752 ~50 X FASTER

      Slide 73

      Slide 73 text

      unit tests can be slow when they DON’T TEST OBJECTS IN ISOLATION.

      Slide 74

      Slide 74 text

      context Notifications::Emails::CommitMention do setup do @repo = Repository.make! readonly_example_repo :notification_mentions, @repo @commit = @repo.commit('a62c6b20') @comment = CommitMention.new(:commit_id => @commit.sha) @message = Emails::CommitMention.new(@comment) end test 'subject' do expected = "[testy] hello world (#{@comment.short_id})" assert_equal expected, @message.subject end end

      Slide 75

      Slide 75 text

      context Notifications::Emails::CommitMention do setup do @repo = Repository.make! readonly_example_repo :notification_mentions, @repo @commit = @repo.commit('a62c6b20') @comment = CommitMention.new(:commit_id => @commit.sha) @message = Emails::CommitMention.new(@comment) end test 'subject' do expected = "[testy] hello world (#{@comment.short_id})" assert_equal expected, @message.subject end end

      Slide 76

      Slide 76 text

      context Notifications::Emails::CommitMention do setup do @commit = stub( :sha => Sham.sha, :short_id => '12345678', :short_message => 'hello world', :message => 'goodbye world' ) @comment = CommitMention.new(:commit_id => @commit.sha) @comment.stubs(:commit => @commit) @message = Emails::CommitMention.new(@comment) end test 'subject' do expected = "[testy] hello world (#{@comment.short_id})" assert_equal expected, message.subject end

      Slide 77

      Slide 77 text

      BEFORE $ ruby test/unit/notifications/emails/commit_mention_test.rb .... Finished in 0.576135 seconds. AFTER $ ruby test/unit/notifications/emails/commit_mention_test.rb .... Finished in 0.052412 seconds.

      Slide 78

      Slide 78 text

      0.576135 ÷ 0.052412 ~10 X FASTER

      Slide 79

      Slide 79 text

      unit tests can be slow when they BOOTSTRAP HEAVY FRAMEWORKS.

      Slide 80

      Slide 80 text

      No content

      Slide 81

      Slide 81 text

      $ time ruby test/unit/notifications/email_handler_test.rb

      Slide 82

      Slide 82 text

      $ time ruby test/unit/notifications/email_handler_test.rb ........ Finished in 0.084729 seconds. 8 tests, 10 assertions, 0 failures, 0 errors

      Slide 83

      Slide 83 text

      $ time ruby test/unit/notifications/email_handler_test.rb ........ Finished in 0.084729 seconds. 8 tests, 10 assertions, 0 failures, 0 errors real 0m7.065s user 0m4.948s sys 0m1.961s

      Slide 84

      Slide 84 text

      test/fast/notifications/web_handler.rb require 'notifications/summary_store' require 'notifications/memory_indexer' require 'notifications/web_handler' context Notifications::WebHandler do def web @web ||= WebHandler.new( :indexer => MemoryIndexer.new, :store => SummaryStore.new ) end def test_insert_increments_count assert_equal 0, web.count(1) web.add build_summary, 1 assert_equal 1, web.count(1) end

      Slide 85

      Slide 85 text

      $ time ruby test/fast/notifications/web_handler_test.rb .... Finished in 0.001577 seconds. 4 tests, 22 assertions, 0 failures, 0 errors real 0m0.139s user 0m0.068s sys 0m0.063s

      Slide 86

      Slide 86 text

      7.065 ÷ 0.139 ~50 X FASTER

      Slide 87

      Slide 87 text

      SLOW TEST MYTHS

      Slide 88

      Slide 88 text

      SLOW TEST MYTHS “Our tests are slow because we have too many of them.”

      Slide 89

      Slide 89 text

      SLOW TEST MYTHS “Our tests are slow because we have too many of them.” “To speed up our tests, we just need to parallelize them.”

      Slide 90

      Slide 90 text

      SLOW TEST MYTHS “Our tests are slow because we have too many of them.” “To speed up our tests, we just need to parallelize them.” “We can’t use test doubles because we’ll loose confidence that it still works.”

      Slide 91

      Slide 91 text

      PAY ATTENTION TO THE SPEED OF YOUR TESTS AS YOU WRITE THEM.

      Slide 92

      Slide 92 text

      our code smells when IT IS TIGHTLY COUPLED TO A FRAMEWORK.

      Slide 93

      Slide 93 text

      FRAMEWORKS ENCOURAGE YOU TO PUT ALL OF YOUR APPLICATION INSIDE THEIR SANDBOX.

      Slide 94

      Slide 94 text

      …WHICH MAKES CODE DIFFICULT TO TEST, CHANGE AND REUSE.

      Slide 95

      Slide 95 text

      No content

      Slide 96

      Slide 96 text

      God Objects

      Slide 97

      Slide 97 text

      God Objects class Issue < ActiveRecord::Base

      Slide 98

      Slide 98 text

      God Objects class Issue < ActiveRecord::Base # validations validates_presence_of :title, :user_id, :repository_id

      Slide 99

      Slide 99 text

      God Objects class Issue < ActiveRecord::Base # validations validates_presence_of :title, :user_id, :repository_id # associations belongs_to :user

      Slide 100

      Slide 100 text

      God Objects class Issue < ActiveRecord::Base # validations validates_presence_of :title, :user_id, :repository_id # associations belongs_to :user # data integrity before_validation :set_state

      Slide 101

      Slide 101 text

      God Objects class Issue < ActiveRecord::Base # validations validates_presence_of :title, :user_id, :repository_id # associations belongs_to :user # data integrity before_validation :set_state # misc concerns before_save :audit_if_changed

      Slide 102

      Slide 102 text

      God Objects class Issue < ActiveRecord::Base # validations validates_presence_of :title, :user_id, :repository_id # associations belongs_to :user # data integrity before_validation :set_state # misc concerns before_save :audit_if_changed # querying named_scope :watched_by, lambda {|user| ... }

      Slide 103

      Slide 103 text

      God Objects class Issue < ActiveRecord::Base # validations validates_presence_of :title, :user_id, :repository_id # associations belongs_to :user # data integrity before_validation :set_state # misc concerns before_save :audit_if_changed # querying named_scope :watched_by, lambda {|user| ... } # who knows what these do? include Mentionable, Subscribable, Summarizable

      Slide 104

      Slide 104 text

      God Objects class Issue < ActiveRecord::Base # validations validates_presence_of :title, :user_id, :repository_id # associations belongs_to :user # data integrity before_validation :set_state # misc concerns before_save :audit_if_changed # querying named_scope :watched_by, lambda {|user| ... } # who knows what these do? include Mentionable, Subscribable, Summarizable # domain logic def active_participants [self.user] + watchers + commentors end end

      Slide 105

      Slide 105 text

      MAKE THE FRAMEWORK DEPEND ON YOUR APPLICATION, INSTEAD OF MAKING YOUR APPLICATION DEPEND ON THE FRAMEWORK.

      Slide 106

      Slide 106 text

      GOOS

      Slide 107

      Slide 107 text

      coupled to a framework GOOS

      Slide 108

      Slide 108 text

      coupled to a framework the rest of the application GOOS

      Slide 109

      Slide 109 text

      A typical Rails controller… class SessionsController < ApplicationController def create user = User.authenticate(params[:username], params[:password]) if user self.current_user = user redirect_to root_path, success: 'You are signed in!' else render :new, warning: 'Wrong username or password.' end end end

      Slide 110

      Slide 110 text

      …and model class User < ActiveRecord::Base def self.authenticate(username, password) user = find_by_username(username) user if user && user.authenticated?(password) end def authenticated?(password) encrypt(password, self.salt) == self.encrypted_password end def encrypt(password, salt) Digest::SHA1.hexdigest(password+salt) end end

      Slide 111

      Slide 111 text

      Why does this depend on Active Record? class User < ActiveRecord::Base def self.authenticate(username, password) user = find_by_username(username) user if user && user.authenticated?(password) end def authenticated?(password) encrypt(password, self.salt) == self.encrypted_password end def encrypt(password, salt) Digest::SHA1.hexdigest(password+salt) end end

      Slide 112

      Slide 112 text

      The spec is already complex. describe User do # … a couple hundred lines of specs … describe ".authenticate" do let!(:user) do create :user, :email => "bkeepers", :password => "testing" end it "returns user with case insensitive username" do User.authenticate('BKeepers', 'testing').should == @user end it "returns nil with incorrect password" do User.authenticate("bkeepers", "wrong").should be_nil end it "returns nil with unknown username" do User.authenticate('[email protected]', 'testing').should be_nil end end # … a couple hundred more lines of specs …

      Slide 113

      Slide 113 text

      Create objects to model the domain class SessionsController < ApplicationController def create user = PasswordAuthentication.new(params[:username], params[:password]).user if user self.current_user = user redirect_to root_path, success: 'You are signed in!' else render :new, warning: 'Wrong username or password.' end end end

      Slide 114

      Slide 114 text

      Plain ol’ Ruby class class PasswordAuthentication def initialize(username, password) @username = username @password = password end def user end end

      Slide 115

      Slide 115 text

      require 'spec_helper' describe PasswordAuthentication do describe 'user' do context 'with a valid username & password' context 'with an unknown username' context 'with an incorrect password' end end

      Slide 116

      Slide 116 text

      describe PasswordAuthentication do describe 'user' do let!(:user) do create :user, :username => 'bkeepers', :password => 'testing' end context 'with a valid username & password' do subject do PasswordAuthentication.new(user.username, 'testing') end it 'returns the user' do subject.user.should == user end end end

      Slide 117

      Slide 117 text

      class PasswordAuthentication def initialize(username, password) @username = username @password = password end def user User.find_by_username(@username) end end

      Slide 118

      Slide 118 text

      context 'with an unknown username' do subject do PasswordAuthentication.new('unknown', 'testing') end it 'returns nil' do subject.user.should be_nil end end

      Slide 119

      Slide 119 text

      No changes necessary class PasswordAuthentication def initialize(username, password) @username = username @password = password end def user User.find_by_username(@username) end end

      Slide 120

      Slide 120 text

      describe PasswordAuthentication do describe 'user' do context 'with a valid username & password' do # … context 'with an unknown username' do # … context 'with an incorrect password' do subject do PasswordAuthentication.new(user.username, 'wrong') end it 'returns nil' do subject.user.should be_nil end end end end

      Slide 121

      Slide 121 text

      class PasswordAuthentication # … def user user = User.find_by_username(@username) user if user && authenticated?(user) end private def authenticated?(user) encrypt(@password, user.password_salt) == user.encrypted_password end def encrypt(password, salt) Digest::SHA1.hexdigest(password+salt) end end

      Slide 122

      Slide 122 text

      describe PasswordAuthentication do describe 'user' do let!(:user) do create :user, :username => 'bkeepers', :password => 'testing' # hits the DB :( end # … end end

      Slide 123

      Slide 123 text

      describe PasswordAuthentication do describe 'user' do let!(:user) do double :user, :username => 'bkeepers', :encrypted_password => '…', :password_salt => '…' end before do User.stub(:find_by_username). with(user.username). and_return(user) end end end

      Slide 124

      Slide 124 text

      context 'with an unknown username' do before do User.should_receive(:find_by_username). with('unknown'). and_return(nil) end subject do PasswordAuthentication.new('unknown', 'testing') end it 'returns nil' do subject.user.should be_nil end end

      Slide 125

      Slide 125 text

      POSITIVE FEEDBACK LOOP Unit tests help us isolate our code and reduce coupling to frameworks, which makes our tests faster.

      Slide 126

      Slide 126 text

      Before we part ways EPILOGUE

      Slide 127

      Slide 127 text

      Robert C. Martin Clean Code “Writing clean code requires the disciplined use of a myriad little techniques applied through a painstakingly acquired sense of ‘cleanliness.’”

      Slide 128

      Slide 128 text

      REFERENCES

      Slide 129

      Slide 129 text

      REFERENCES Gary Bernhardt Fast test, Slow Test http://pyvideo.org/video/631/fast-test-slow-test Corey Haines Fast Rails Tests http://confreaks.com/videos/641-gogaruco2011-fast-rails-tests

      Slide 130

      Slide 130 text

      @bkeepers http://bit.ly/smells-slides QUESTIONS?