Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Why Our Code Smells

Why Our Code Smells

Odors are communication devices. They exist for a reason and are usually trying to tell us something.

Our code smells and it is trying to tell us what is wrong. Does a test case require an abundance of setup? Maybe the code being tested is doing too much, or it is not isolated enough for the test? Does an object have an abundance of instance variables? Maybe it should be split into multiple objects? Is a view brittle? Maybe it is too tightly coupled to a model, or maybe the logic needs abstracted into an object that can be tested?

In this talk, I will walk through code from projects that I work on every day, looking for smells that indicate problems, understanding why it smells, what the smell is trying to tell us, and how to refactor it.

Brandon Keepers
PRO

May 23, 2012
Tweet

More Decks by Brandon Keepers

Other Decks in Programming

Transcript

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

    View Slide

  2. github.com/bkeepers
    @bkeepers

    View Slide

  3. I AM TIRED OF
    WRITING
    BAD CODE.

    View Slide

  4. I AM TIRED OF
    MAINTAINING
    BAD CODE.

    View Slide

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

    View Slide

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

    View Slide

  7. View Slide

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

    View Slide

  9. DIVERGENT CHANGE

    View Slide

  10. 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.

    View Slide

  11. 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.

    View Slide

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

    View Slide

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

    View Slide

  14. our code smells when
    UNIT TESTS
    ARE COMPLEX.

    View Slide

  15. TDD IS A
    DESIGN
    PROCESS.

    View Slide

  16. WHY DO WE WRITE TESTS?

    View Slide

  17. WHY DO WE WRITE TESTS?
    1.Guard against regressions

    View Slide

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

    View Slide

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

    View Slide

  20. WARNING
    Excessive use of
    quotations ahead.

    View Slide

  21. 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.”

    View Slide

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

    View Slide

  23. 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

    View Slide

  24. 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

    View Slide

  25. 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.”

    View Slide

  26. 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

    View Slide

  27. 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

    View Slide

  28. 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

    View Slide

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

    View Slide

  30. 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.”

    View Slide

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

    View Slide

  32. 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’).”

    View Slide

  33. 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

    View Slide

  34. 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)
    });
    });

    View Slide

  35. 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!');
    });
    });
    });

    View Slide

  36. 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

    View Slide

  37. 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

    View Slide

  38. 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

    View Slide

  39. 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

    View Slide

  40. 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

    View Slide

  41. 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

    View Slide

  42. 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

    View Slide

  43. 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…

    View Slide

  44. 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

    View Slide

  45. 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') + '');
    });
    });

    View Slide

  46. 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

    View Slide

  47. 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') + '');
    });
    });

    View Slide

  48. 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') + '');
    });
    });

    View Slide

  49. 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

    View Slide

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

    View Slide

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

    View Slide

  52. 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

    View Slide

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

    View Slide

  54. 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!');
    });
    });

    View Slide

  55. PAY ATTENTION TO
    TESTING PAINS AND
    ADJUST THE DESIGN
    ACCORDINGLY.

    View Slide

  56. 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.

    View Slide

  57. 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.”

    View Slide

  58. our code smells when
    UNIT TESTS
    ARE SLOW.

    View Slide

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

    View Slide

  60. WHAT’S WRONG WITH SLOW TESTS?

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  64. 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/

    View Slide

  65. 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?

    View Slide

  66. 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?

    View Slide

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

    View Slide

  68. 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

    View Slide

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

    View Slide

  70. 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

    View Slide

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

    View Slide

  72. 3.517926
    ÷ 0.073752
    ~50 X FASTER

    View Slide

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

    View Slide

  74. 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

    View Slide

  75. 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

    View Slide

  76. 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

    View Slide

  77. 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.

    View Slide

  78. 0.576135
    ÷ 0.052412
    ~10 X FASTER

    View Slide

  79. unit tests can be slow when they
    BOOTSTRAP
    HEAVY
    FRAMEWORKS.

    View Slide

  80. View Slide

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

    View Slide

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

    View Slide

  83. $ 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

    View Slide

  84. 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

    View Slide

  85. $ 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

    View Slide

  86. 7.065
    ÷ 0.139
    ~50 X FASTER

    View Slide

  87. SLOW TEST MYTHS

    View Slide

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

    View Slide

  89. 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.”

    View Slide

  90. 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.”

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  95. View Slide

  96. God Objects

    View Slide

  97. God Objects
    class Issue < ActiveRecord::Base

    View Slide

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

    View Slide

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

    View Slide

  100. 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

    View Slide

  101. 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

    View Slide

  102. 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| ... }

    View Slide

  103. 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

    View Slide

  104. 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

    View Slide

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

    View Slide

  106. GOOS

    View Slide

  107. coupled to a
    framework
    GOOS

    View Slide

  108. coupled to a
    framework
    the rest of the
    application
    GOOS

    View Slide

  109. 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

    View Slide

  110. …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

    View Slide

  111. 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

    View Slide

  112. 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 …

    View Slide

  113. 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

    View Slide

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

    View Slide

  115. 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

    View Slide

  116. 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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  120. 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

    View Slide

  121. 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

    View Slide

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

    View Slide

  123. 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

    View Slide

  124. 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

    View Slide

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

    View Slide

  126. Before we part ways
    EPILOGUE

    View Slide

  127. 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.’”

    View Slide

  128. REFERENCES

    View Slide

  129. 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

    View Slide

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

    View Slide