SlideShare a Scribd company logo
1 of 130
Download to read offline
SMELLS
http://www.flickr.com/photos/kanaka/3480201136/
WHY OUR CODE
github.com/bkeepers
@bkeepers
I AM TIRED OF
WRITING
BAD CODE.
I AM TIRED OF
MAINTAINING
BAD CODE.
Kent Beck
Smalltalk Best Practice Patterns
“Code doesn’t lie. If
you’re not listening,
you won’t hear the
truths it tells.”
A CODE SMELL
USUALLY INDICATES
A DEEPER PROBLEM
IN THE SYSTEM.
CODE SMELLS ARE
HEURISTICS TO
SUGGEST WHEN TO
REFACTOR AND WHAT
TECHNIQUES TO USE.
DIVERGENT CHANGE
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.
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.
OUR CODE SMELLS IN
THE 21 WAYS THAT
BECK AND FOWLER
DESCRIBE, BUT…
our code smells when
UNIT TESTS ARE
COMPLEX & SLOW.
IT IS TIGHTLY COUPLED
TO A FRAMEWORK.
our code smells when
UNIT TESTS
ARE COMPLEX.
TDD IS A
DESIGN
PROCESS.
WHY DO WE WRITE TESTS?
WHY DO WE WRITE TESTS?
1.Guard against regressions
WHY DO WE WRITE TESTS?
1.Guard against regressions
2.Gain confidence to change
WHY DO WE WRITE TESTS?
1.Guard against regressions
2.Gain confidence to change
3.Discover better designs
WARNING
Excessive use of
quotations ahead.
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.”
unit tests can be complex when
OBJECTS ARE
TOO TIGHTLY
COUPLED.
Exhibit A: GitHub Notifications
context Notifications::Emails::Message do
test "#to uses global email" do
@settings.email :global, 'bkeepers@github.com'
assert_equal 'bkeepers@github.com', @message.to
end
test "#body includes comment body" do
assert_match @comment.body, @message.body
end
end
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, 'bkeepers@github.com'
assert_equal 'bkeepers@github.com', @message.to
end
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.”
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, 'bkeepers@github.com'
assert_equal 'bkeepers@github.com', @message.to
end
end
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, 'bkeepers@github.com'
assert_equal 'bkeepers@github.com', @message.to
end
end
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, 'bkeepers@github.com'
assert_equal 'bkeepers@github.com', @message.to
end
test "#body includes comment body" do
assert_match @comment.body, @message.body
end
end
unit tests can be complex when
OBJECTS ARE
DOING TOO
MUCH.
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.”
Every class should have a single responsibility,
and that responsibility should be entirely
encapsulated by the class.
SINGLE
RESPONSIBILITY
PRINCIPLE
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’).”
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('<li>' + data.text + '</li>');
}
});
return false;
});
});
example lovingly stolen from @searls
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)
});
});
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(
'<div>This is starting stink!</div>');
});
});
});
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('<li>' + data.text + '</li>');
}
});
return false;
});
});
example lovingly stolen from @searls
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('<li>' + data.text + '</li>');
}
});
return false;
});
});
1. page event
example lovingly stolen from @searls
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('<li>' + data.text + '</li>');
}
});
return false;
});
});
1. page event
2. user event
example lovingly stolen from @searls
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('<li>' + data.text + '</li>');
}
});
return false;
});
});
1. page event
2. user event
3. network IO
example lovingly stolen from @searls
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('<li>' + data.text + '</li>');
}
});
return false;
});
});
1. page event
2. user event
3. network IO
4. user input
example lovingly stolen from @searls
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('<li>' + data.text + '</li>');
}
});
return false;
});
});
1. page event
2. user event
3. network IO
4. user input
5. network event
example lovingly stolen from @searls
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('<li>' + data.text + '</li>');
}
});
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
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('<li>' + data.text + '</li>');
}
});
return false;
});
});
So we start to refactor…
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('<li>' + data.text + '</li>');
}
});
return false;
});
});
Refactor to use a model
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(
'<li>' + status.get('text') + '</li>');
});
});
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(
'<li>' + status.get('text') + '</li>');
});
});
RESPONSIBILITY:
Sync state with server
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(
'<li>' + status.get('text') + '</li>');
});
});
Refactor handling of user input
jQuery(function($) {
var statuses = new Collection.Statuses();
new View.PostStatus({collection: statuses});
statuses.on('add', function(status) {
$('#statuses').append(
'<li>' + status.get('text') + '</li>');
});
});
Refactor handling of user input
jQuery(function($) {
var statuses = new Collection.Statuses();
new View.PostStatus({collection: statuses});
statuses.on('add', function(status) {
$('#statuses').append(
'<li>' + status.get('text') + '</li>');
});
});
RESPONSIBILITY:
Create statuses from user input
jQuery(function($) {
var statuses = new Collection.Statuses();
new View.PostStatus({collection: statuses});
statuses.on('add', function(status) {
$('#statuses').append(
'<li>' + status.get('text') + '</li>');
});
});
Refactor templating
Refactor templating
jQuery(function($) {
var statuses = new Collection.Statuses();
new View.PostStatus({collection: statuses});
new View.StatusList({collection: statuses});
});
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
jQuery(function($) {
var statuses = new Collection.Statuses();
new View.PostStatus({collection: statuses});
new View.StatusList({collection: statuses});
});
RESPONSIBILITY:
Initialize application on page load
Our tests only have one concern
describe("View.StatusList", function() {
beforeEach(function() {
$el = $('<ul></ul>');
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!');
});
});
PAY ATTENTION TO
TESTING PAINS AND
ADJUST THE DESIGN
ACCORDINGLY.
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.
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.”
our code smells when
UNIT TESTS
ARE SLOW.
$ bx rake test:units
...............................................................................................
...............................................................................................
...............................................................................................
...............................................................................................
...............................................................................................
...............................................................................................
...............................................................................................
...............................................................................................
...............................................................................................
...............................................................................................
...............................................................................................
...............................................................................................
...............................................................................................
...............................................................................................
...............................................................................................
...............................................................................................
...............................................................................................
...............................................................................................
...............................................................................................
...............................................................................................
...............................................................................................
...............................................................................................
...............................................................................................
........................................................................................
Finished in 274.623286 seconds.
2273 tests, 6765 assertions, 0 failures, 0 errors
WHAT’S WRONG WITH SLOW TESTS?
You don't run them often
WHAT’S WRONG WITH SLOW TESTS?
You don't run them often
You waste time waiting for tests
WHAT’S WRONG WITH SLOW TESTS?
You don't run them often
You waste time waiting for tests
You distracted others while waiting
WHAT’S WRONG WITH SLOW TESTS?
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/
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?
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?
unit tests can be slow when they
INTERACT
WITH SLOW
COMPONENTS.
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, 'bkeepers@github.com'
assert_equal 'bkeepers@github.com', @message.to
end
test "#body includes comment body" do
assert_match @comment.body, @message.body
end
end
$ ruby test/unit/notifications/emails/message_test.rb
...................
Finished in 3.517926 seconds.
19 tests, 24 assertions, 0 failures, 0 errors
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, 'bkeepers@github.com'
assert_equal 'bkeepers@github.com', @message.to
end
test "#body includes comment body" do
assert_match @comment.body, @message.body
end
$ ruby test/unit/notifications/emails/message_test.rb
...................
Finished in 0.073752 seconds.
19 tests, 24 assertions, 0 failures, 0 errors
3.517926
÷ 0.073752
~50 X FASTER
unit tests can be slow when they
DON’T TEST
OBJECTS IN
ISOLATION.
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
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
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
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.
0.576135
÷ 0.052412
~10 X FASTER
unit tests can be slow when they
BOOTSTRAP
HEAVY
FRAMEWORKS.
$ time ruby test/unit/notifications/email_handler_test.rb
$ time ruby test/unit/notifications/email_handler_test.rb
........
Finished in 0.084729 seconds.
8 tests, 10 assertions, 0 failures, 0 errors
$ 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
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
$ 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
7.065
÷ 0.139
~50 X FASTER
SLOW TEST MYTHS
SLOW TEST MYTHS
“Our tests are slow because we have too
many of them.”
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.”
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.”
PAY ATTENTION
TO THE SPEED OF
YOUR TESTS AS
YOU WRITE THEM.
our code smells when
IT IS TIGHTLY
COUPLED TO A
FRAMEWORK.
FRAMEWORKS
ENCOURAGE YOU TO
PUT ALL OF YOUR
APPLICATION INSIDE
THEIR SANDBOX.
…WHICH MAKES
CODE DIFFICULT TO
TEST, CHANGE AND
REUSE.
God Objects
God Objects
class Issue < ActiveRecord::Base
God Objects
class Issue < ActiveRecord::Base
# validations
validates_presence_of :title, :user_id, :repository_id
God Objects
class Issue < ActiveRecord::Base
# validations
validates_presence_of :title, :user_id, :repository_id
# associations
belongs_to :user
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
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
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| ... }
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
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
MAKE THE FRAMEWORK
DEPEND ON YOUR
APPLICATION, INSTEAD
OF MAKING YOUR
APPLICATION DEPEND
ON THE FRAMEWORK.
GOOS
coupled to a
framework
GOOS
coupled to a
framework
the rest of the
application
GOOS
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
…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
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
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('foobar@foobar.com', 'testing').should be_nil
end
end
# … a couple hundred more lines of specs …
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
Plain ol’ Ruby class
class PasswordAuthentication
def initialize(username, password)
@username = username
@password = password
end
def user
end
end
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
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
class PasswordAuthentication
def initialize(username, password)
@username = username
@password = password
end
def user
User.find_by_username(@username)
end
end
context 'with an unknown username' do
subject do
PasswordAuthentication.new('unknown', 'testing')
end
it 'returns nil' do
subject.user.should be_nil
end
end
No changes necessary
class PasswordAuthentication
def initialize(username, password)
@username = username
@password = password
end
def user
User.find_by_username(@username)
end
end
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
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
describe PasswordAuthentication do
describe 'user' do
let!(:user) do
create :user, :username => 'bkeepers',
:password => 'testing'
# hits the DB :(
end
# …
end
end
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
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
POSITIVE FEEDBACK LOOP
Unit tests help us isolate our
code and reduce coupling to
frameworks, which makes
our tests faster.
Before we part ways
EPILOGUE
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.’”
REFERENCES
REFERENCES
Gary Bernhardt
Fast test, SlowTest
http://pyvideo.org/video/631/fast-test-slow-test
Corey Haines
Fast RailsTests
http://confreaks.com/videos/641-gogaruco2011-fast-rails-tests
@bkeepers
http://bit.ly/smells-slides
QUESTIONS?

More Related Content

Similar to Why Our Code Smells

Two Trains and Other Refactoring Analogies
Two Trains and Other Refactoring AnalogiesTwo Trains and Other Refactoring Analogies
Two Trains and Other Refactoring AnalogiesKevin London
 
Adding a modern twist to legacy web applications
Adding a modern twist to legacy web applicationsAdding a modern twist to legacy web applications
Adding a modern twist to legacy web applicationsJeff Durta
 
Why is crud a bad idea - focus on real scenarios
Why is crud a bad idea - focus on real scenariosWhy is crud a bad idea - focus on real scenarios
Why is crud a bad idea - focus on real scenariosDivante
 
Using Task Queues and D3.js to build an analytics product on App Engine
Using Task Queues and D3.js to build an analytics product on App EngineUsing Task Queues and D3.js to build an analytics product on App Engine
Using Task Queues and D3.js to build an analytics product on App EngineRiver of Talent
 
Paying off technical debt with PHPSpec
Paying off technical debt with PHPSpecPaying off technical debt with PHPSpec
Paying off technical debt with PHPSpecLewis Wright
 
Building Large jQuery Applications
Building Large jQuery ApplicationsBuilding Large jQuery Applications
Building Large jQuery ApplicationsRebecca Murphey
 
前端MVC 豆瓣说
前端MVC 豆瓣说前端MVC 豆瓣说
前端MVC 豆瓣说Ting Lv
 
Ruby/Rails
Ruby/RailsRuby/Rails
Ruby/Railsrstankov
 
How To Test Everything
How To Test EverythingHow To Test Everything
How To Test Everythingnoelrap
 
Real life-coffeescript
Real life-coffeescriptReal life-coffeescript
Real life-coffeescriptDavid Furber
 
Lithium: The Framework for People Who Hate Frameworks
Lithium: The Framework for People Who Hate FrameworksLithium: The Framework for People Who Hate Frameworks
Lithium: The Framework for People Who Hate FrameworksNate Abele
 
SummaryHW6 Account ManagementIn HW4, you kept track of multiple.pdf
SummaryHW6 Account ManagementIn HW4, you kept track of multiple.pdfSummaryHW6 Account ManagementIn HW4, you kept track of multiple.pdf
SummaryHW6 Account ManagementIn HW4, you kept track of multiple.pdfARORACOCKERY2111
 
Building Better Applications with Data::Manager
Building Better Applications with Data::ManagerBuilding Better Applications with Data::Manager
Building Better Applications with Data::ManagerJay Shirley
 
Pyconie 2012
Pyconie 2012Pyconie 2012
Pyconie 2012Yaqi Zhao
 
Advanced Perl Techniques
Advanced Perl TechniquesAdvanced Perl Techniques
Advanced Perl TechniquesDave Cross
 
SproutCore and the Future of Web Apps
SproutCore and the Future of Web AppsSproutCore and the Future of Web Apps
SproutCore and the Future of Web AppsMike Subelsky
 
Understanding backbonejs
Understanding backbonejsUnderstanding backbonejs
Understanding backbonejsNick Lee
 

Similar to Why Our Code Smells (20)

Two Trains and Other Refactoring Analogies
Two Trains and Other Refactoring AnalogiesTwo Trains and Other Refactoring Analogies
Two Trains and Other Refactoring Analogies
 
Adding a modern twist to legacy web applications
Adding a modern twist to legacy web applicationsAdding a modern twist to legacy web applications
Adding a modern twist to legacy web applications
 
Why is crud a bad idea - focus on real scenarios
Why is crud a bad idea - focus on real scenariosWhy is crud a bad idea - focus on real scenarios
Why is crud a bad idea - focus on real scenarios
 
Using Task Queues and D3.js to build an analytics product on App Engine
Using Task Queues and D3.js to build an analytics product on App EngineUsing Task Queues and D3.js to build an analytics product on App Engine
Using Task Queues and D3.js to build an analytics product on App Engine
 
Paying off technical debt with PHPSpec
Paying off technical debt with PHPSpecPaying off technical debt with PHPSpec
Paying off technical debt with PHPSpec
 
Having Fun with Play
Having Fun with PlayHaving Fun with Play
Having Fun with Play
 
Test driven development_for_php
Test driven development_for_phpTest driven development_for_php
Test driven development_for_php
 
Building Large jQuery Applications
Building Large jQuery ApplicationsBuilding Large jQuery Applications
Building Large jQuery Applications
 
前端MVC 豆瓣说
前端MVC 豆瓣说前端MVC 豆瓣说
前端MVC 豆瓣说
 
Ruby/Rails
Ruby/RailsRuby/Rails
Ruby/Rails
 
How To Test Everything
How To Test EverythingHow To Test Everything
How To Test Everything
 
Real life-coffeescript
Real life-coffeescriptReal life-coffeescript
Real life-coffeescript
 
Lithium: The Framework for People Who Hate Frameworks
Lithium: The Framework for People Who Hate FrameworksLithium: The Framework for People Who Hate Frameworks
Lithium: The Framework for People Who Hate Frameworks
 
SummaryHW6 Account ManagementIn HW4, you kept track of multiple.pdf
SummaryHW6 Account ManagementIn HW4, you kept track of multiple.pdfSummaryHW6 Account ManagementIn HW4, you kept track of multiple.pdf
SummaryHW6 Account ManagementIn HW4, you kept track of multiple.pdf
 
Building Better Applications with Data::Manager
Building Better Applications with Data::ManagerBuilding Better Applications with Data::Manager
Building Better Applications with Data::Manager
 
Pyconie 2012
Pyconie 2012Pyconie 2012
Pyconie 2012
 
Advanced Perl Techniques
Advanced Perl TechniquesAdvanced Perl Techniques
Advanced Perl Techniques
 
PHP Unit Testing
PHP Unit TestingPHP Unit Testing
PHP Unit Testing
 
SproutCore and the Future of Web Apps
SproutCore and the Future of Web AppsSproutCore and the Future of Web Apps
SproutCore and the Future of Web Apps
 
Understanding backbonejs
Understanding backbonejsUnderstanding backbonejs
Understanding backbonejs
 

Recently uploaded

From Event to Action: Accelerate Your Decision Making with Real-Time Automation
From Event to Action: Accelerate Your Decision Making with Real-Time AutomationFrom Event to Action: Accelerate Your Decision Making with Real-Time Automation
From Event to Action: Accelerate Your Decision Making with Real-Time AutomationSafe Software
 
Finology Group – Insurtech Innovation Award 2024
Finology Group – Insurtech Innovation Award 2024Finology Group – Insurtech Innovation Award 2024
Finology Group – Insurtech Innovation Award 2024The Digital Insurer
 
Salesforce Community Group Quito, Salesforce 101
Salesforce Community Group Quito, Salesforce 101Salesforce Community Group Quito, Salesforce 101
Salesforce Community Group Quito, Salesforce 101Paola De la Torre
 
Handwritten Text Recognition for manuscripts and early printed texts
Handwritten Text Recognition for manuscripts and early printed textsHandwritten Text Recognition for manuscripts and early printed texts
Handwritten Text Recognition for manuscripts and early printed textsMaria Levchenko
 
Raspberry Pi 5: Challenges and Solutions in Bringing up an OpenGL/Vulkan Driv...
Raspberry Pi 5: Challenges and Solutions in Bringing up an OpenGL/Vulkan Driv...Raspberry Pi 5: Challenges and Solutions in Bringing up an OpenGL/Vulkan Driv...
Raspberry Pi 5: Challenges and Solutions in Bringing up an OpenGL/Vulkan Driv...Igalia
 
Scaling API-first – The story of a global engineering organization
Scaling API-first – The story of a global engineering organizationScaling API-first – The story of a global engineering organization
Scaling API-first – The story of a global engineering organizationRadu Cotescu
 
FULL ENJOY 🔝 8264348440 🔝 Call Girls in Diplomatic Enclave | Delhi
FULL ENJOY 🔝 8264348440 🔝 Call Girls in Diplomatic Enclave | DelhiFULL ENJOY 🔝 8264348440 🔝 Call Girls in Diplomatic Enclave | Delhi
FULL ENJOY 🔝 8264348440 🔝 Call Girls in Diplomatic Enclave | Delhisoniya singh
 
The Role of Taxonomy and Ontology in Semantic Layers - Heather Hedden.pdf
The Role of Taxonomy and Ontology in Semantic Layers - Heather Hedden.pdfThe Role of Taxonomy and Ontology in Semantic Layers - Heather Hedden.pdf
The Role of Taxonomy and Ontology in Semantic Layers - Heather Hedden.pdfEnterprise Knowledge
 
Kalyanpur ) Call Girls in Lucknow Finest Escorts Service 🍸 8923113531 🎰 Avail...
Kalyanpur ) Call Girls in Lucknow Finest Escorts Service 🍸 8923113531 🎰 Avail...Kalyanpur ) Call Girls in Lucknow Finest Escorts Service 🍸 8923113531 🎰 Avail...
Kalyanpur ) Call Girls in Lucknow Finest Escorts Service 🍸 8923113531 🎰 Avail...gurkirankumar98700
 
[2024]Digital Global Overview Report 2024 Meltwater.pdf
[2024]Digital Global Overview Report 2024 Meltwater.pdf[2024]Digital Global Overview Report 2024 Meltwater.pdf
[2024]Digital Global Overview Report 2024 Meltwater.pdfhans926745
 
The 7 Things I Know About Cyber Security After 25 Years | April 2024
The 7 Things I Know About Cyber Security After 25 Years | April 2024The 7 Things I Know About Cyber Security After 25 Years | April 2024
The 7 Things I Know About Cyber Security After 25 Years | April 2024Rafal Los
 
GenCyber Cyber Security Day Presentation
GenCyber Cyber Security Day PresentationGenCyber Cyber Security Day Presentation
GenCyber Cyber Security Day PresentationMichael W. Hawkins
 
Histor y of HAM Radio presentation slide
Histor y of HAM Radio presentation slideHistor y of HAM Radio presentation slide
Histor y of HAM Radio presentation slidevu2urc
 
#StandardsGoals for 2024: What’s new for BISAC - Tech Forum 2024
#StandardsGoals for 2024: What’s new for BISAC - Tech Forum 2024#StandardsGoals for 2024: What’s new for BISAC - Tech Forum 2024
#StandardsGoals for 2024: What’s new for BISAC - Tech Forum 2024BookNet Canada
 
Transforming Data Streams with Kafka Connect: An Introduction to Single Messa...
Transforming Data Streams with Kafka Connect: An Introduction to Single Messa...Transforming Data Streams with Kafka Connect: An Introduction to Single Messa...
Transforming Data Streams with Kafka Connect: An Introduction to Single Messa...HostedbyConfluent
 
My Hashitalk Indonesia April 2024 Presentation
My Hashitalk Indonesia April 2024 PresentationMy Hashitalk Indonesia April 2024 Presentation
My Hashitalk Indonesia April 2024 PresentationRidwan Fadjar
 
Maximizing Board Effectiveness 2024 Webinar.pptx
Maximizing Board Effectiveness 2024 Webinar.pptxMaximizing Board Effectiveness 2024 Webinar.pptx
Maximizing Board Effectiveness 2024 Webinar.pptxOnBoard
 
Tech-Forward - Achieving Business Readiness For Copilot in Microsoft 365
Tech-Forward - Achieving Business Readiness For Copilot in Microsoft 365Tech-Forward - Achieving Business Readiness For Copilot in Microsoft 365
Tech-Forward - Achieving Business Readiness For Copilot in Microsoft 3652toLead Limited
 
Breaking the Kubernetes Kill Chain: Host Path Mount
Breaking the Kubernetes Kill Chain: Host Path MountBreaking the Kubernetes Kill Chain: Host Path Mount
Breaking the Kubernetes Kill Chain: Host Path MountPuma Security, LLC
 
CNv6 Instructor Chapter 6 Quality of Service
CNv6 Instructor Chapter 6 Quality of ServiceCNv6 Instructor Chapter 6 Quality of Service
CNv6 Instructor Chapter 6 Quality of Servicegiselly40
 

Recently uploaded (20)

From Event to Action: Accelerate Your Decision Making with Real-Time Automation
From Event to Action: Accelerate Your Decision Making with Real-Time AutomationFrom Event to Action: Accelerate Your Decision Making with Real-Time Automation
From Event to Action: Accelerate Your Decision Making with Real-Time Automation
 
Finology Group – Insurtech Innovation Award 2024
Finology Group – Insurtech Innovation Award 2024Finology Group – Insurtech Innovation Award 2024
Finology Group – Insurtech Innovation Award 2024
 
Salesforce Community Group Quito, Salesforce 101
Salesforce Community Group Quito, Salesforce 101Salesforce Community Group Quito, Salesforce 101
Salesforce Community Group Quito, Salesforce 101
 
Handwritten Text Recognition for manuscripts and early printed texts
Handwritten Text Recognition for manuscripts and early printed textsHandwritten Text Recognition for manuscripts and early printed texts
Handwritten Text Recognition for manuscripts and early printed texts
 
Raspberry Pi 5: Challenges and Solutions in Bringing up an OpenGL/Vulkan Driv...
Raspberry Pi 5: Challenges and Solutions in Bringing up an OpenGL/Vulkan Driv...Raspberry Pi 5: Challenges and Solutions in Bringing up an OpenGL/Vulkan Driv...
Raspberry Pi 5: Challenges and Solutions in Bringing up an OpenGL/Vulkan Driv...
 
Scaling API-first – The story of a global engineering organization
Scaling API-first – The story of a global engineering organizationScaling API-first – The story of a global engineering organization
Scaling API-first – The story of a global engineering organization
 
FULL ENJOY 🔝 8264348440 🔝 Call Girls in Diplomatic Enclave | Delhi
FULL ENJOY 🔝 8264348440 🔝 Call Girls in Diplomatic Enclave | DelhiFULL ENJOY 🔝 8264348440 🔝 Call Girls in Diplomatic Enclave | Delhi
FULL ENJOY 🔝 8264348440 🔝 Call Girls in Diplomatic Enclave | Delhi
 
The Role of Taxonomy and Ontology in Semantic Layers - Heather Hedden.pdf
The Role of Taxonomy and Ontology in Semantic Layers - Heather Hedden.pdfThe Role of Taxonomy and Ontology in Semantic Layers - Heather Hedden.pdf
The Role of Taxonomy and Ontology in Semantic Layers - Heather Hedden.pdf
 
Kalyanpur ) Call Girls in Lucknow Finest Escorts Service 🍸 8923113531 🎰 Avail...
Kalyanpur ) Call Girls in Lucknow Finest Escorts Service 🍸 8923113531 🎰 Avail...Kalyanpur ) Call Girls in Lucknow Finest Escorts Service 🍸 8923113531 🎰 Avail...
Kalyanpur ) Call Girls in Lucknow Finest Escorts Service 🍸 8923113531 🎰 Avail...
 
[2024]Digital Global Overview Report 2024 Meltwater.pdf
[2024]Digital Global Overview Report 2024 Meltwater.pdf[2024]Digital Global Overview Report 2024 Meltwater.pdf
[2024]Digital Global Overview Report 2024 Meltwater.pdf
 
The 7 Things I Know About Cyber Security After 25 Years | April 2024
The 7 Things I Know About Cyber Security After 25 Years | April 2024The 7 Things I Know About Cyber Security After 25 Years | April 2024
The 7 Things I Know About Cyber Security After 25 Years | April 2024
 
GenCyber Cyber Security Day Presentation
GenCyber Cyber Security Day PresentationGenCyber Cyber Security Day Presentation
GenCyber Cyber Security Day Presentation
 
Histor y of HAM Radio presentation slide
Histor y of HAM Radio presentation slideHistor y of HAM Radio presentation slide
Histor y of HAM Radio presentation slide
 
#StandardsGoals for 2024: What’s new for BISAC - Tech Forum 2024
#StandardsGoals for 2024: What’s new for BISAC - Tech Forum 2024#StandardsGoals for 2024: What’s new for BISAC - Tech Forum 2024
#StandardsGoals for 2024: What’s new for BISAC - Tech Forum 2024
 
Transforming Data Streams with Kafka Connect: An Introduction to Single Messa...
Transforming Data Streams with Kafka Connect: An Introduction to Single Messa...Transforming Data Streams with Kafka Connect: An Introduction to Single Messa...
Transforming Data Streams with Kafka Connect: An Introduction to Single Messa...
 
My Hashitalk Indonesia April 2024 Presentation
My Hashitalk Indonesia April 2024 PresentationMy Hashitalk Indonesia April 2024 Presentation
My Hashitalk Indonesia April 2024 Presentation
 
Maximizing Board Effectiveness 2024 Webinar.pptx
Maximizing Board Effectiveness 2024 Webinar.pptxMaximizing Board Effectiveness 2024 Webinar.pptx
Maximizing Board Effectiveness 2024 Webinar.pptx
 
Tech-Forward - Achieving Business Readiness For Copilot in Microsoft 365
Tech-Forward - Achieving Business Readiness For Copilot in Microsoft 365Tech-Forward - Achieving Business Readiness For Copilot in Microsoft 365
Tech-Forward - Achieving Business Readiness For Copilot in Microsoft 365
 
Breaking the Kubernetes Kill Chain: Host Path Mount
Breaking the Kubernetes Kill Chain: Host Path MountBreaking the Kubernetes Kill Chain: Host Path Mount
Breaking the Kubernetes Kill Chain: Host Path Mount
 
CNv6 Instructor Chapter 6 Quality of Service
CNv6 Instructor Chapter 6 Quality of ServiceCNv6 Instructor Chapter 6 Quality of Service
CNv6 Instructor Chapter 6 Quality of Service
 

Why Our Code Smells

  • 3. I AM TIRED OF WRITING BAD CODE.
  • 4. I AM TIRED OF MAINTAINING BAD CODE.
  • 5. Kent Beck Smalltalk Best Practice Patterns “Code doesn’t lie. If you’re not listening, you won’t hear the truths it tells.”
  • 6. A CODE SMELL USUALLY INDICATES A DEEPER PROBLEM IN THE SYSTEM.
  • 7.
  • 8. CODE SMELLS ARE HEURISTICS TO SUGGEST WHEN TO REFACTOR AND WHAT TECHNIQUES TO USE.
  • 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.
  • 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.
  • 12. OUR CODE SMELLS IN THE 21 WAYS THAT BECK AND FOWLER DESCRIBE, BUT…
  • 13. our code smells when UNIT TESTS ARE COMPLEX & SLOW. IT IS TIGHTLY COUPLED TO A FRAMEWORK.
  • 14. our code smells when UNIT TESTS ARE COMPLEX.
  • 16. WHY DO WE WRITE TESTS?
  • 17. WHY DO WE WRITE TESTS? 1.Guard against regressions
  • 18. WHY DO WE WRITE TESTS? 1.Guard against regressions 2.Gain confidence to change
  • 19. WHY DO WE WRITE TESTS? 1.Guard against regressions 2.Gain confidence to change 3.Discover better designs
  • 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.”
  • 22. unit tests can be complex when OBJECTS ARE TOO TIGHTLY COUPLED.
  • 23. Exhibit A: GitHub Notifications context Notifications::Emails::Message do test "#to uses global email" do @settings.email :global, 'bkeepers@github.com' assert_equal 'bkeepers@github.com', @message.to end test "#body includes comment body" do assert_match @comment.body, @message.body end end
  • 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, 'bkeepers@github.com' assert_equal 'bkeepers@github.com', @message.to end
  • 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.”
  • 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, 'bkeepers@github.com' assert_equal 'bkeepers@github.com', @message.to end end
  • 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, 'bkeepers@github.com' assert_equal 'bkeepers@github.com', @message.to end end
  • 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, 'bkeepers@github.com' assert_equal 'bkeepers@github.com', @message.to end test "#body includes comment body" do assert_match @comment.body, @message.body end end
  • 29. unit tests can be complex when OBJECTS ARE DOING TOO MUCH.
  • 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.”
  • 31. Every class should have a single responsibility, and that responsibility should be entirely encapsulated by the class. SINGLE RESPONSIBILITY PRINCIPLE
  • 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’).”
  • 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('<li>' + data.text + '</li>'); } }); return false; }); }); example lovingly stolen from @searls
  • 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) }); });
  • 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( '<div>This is starting stink!</div>'); }); }); });
  • 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('<li>' + data.text + '</li>'); } }); return false; }); }); example lovingly stolen from @searls
  • 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('<li>' + data.text + '</li>'); } }); return false; }); }); 1. page event example lovingly stolen from @searls
  • 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('<li>' + data.text + '</li>'); } }); return false; }); }); 1. page event 2. user event example lovingly stolen from @searls
  • 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('<li>' + data.text + '</li>'); } }); return false; }); }); 1. page event 2. user event 3. network IO example lovingly stolen from @searls
  • 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('<li>' + data.text + '</li>'); } }); return false; }); }); 1. page event 2. user event 3. network IO 4. user input example lovingly stolen from @searls
  • 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('<li>' + data.text + '</li>'); } }); return false; }); }); 1. page event 2. user event 3. network IO 4. user input 5. network event example lovingly stolen from @searls
  • 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('<li>' + data.text + '</li>'); } }); 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
  • 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('<li>' + data.text + '</li>'); } }); return false; }); }); So we start to refactor…
  • 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('<li>' + data.text + '</li>'); } }); return false; }); }); Refactor to use a model
  • 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( '<li>' + status.get('text') + '</li>'); }); });
  • 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( '<li>' + status.get('text') + '</li>'); }); }); RESPONSIBILITY: Sync state with server
  • 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( '<li>' + status.get('text') + '</li>'); }); });
  • 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( '<li>' + status.get('text') + '</li>'); }); });
  • 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( '<li>' + status.get('text') + '</li>'); }); }); RESPONSIBILITY: Create statuses from user input
  • 50. jQuery(function($) { var statuses = new Collection.Statuses(); new View.PostStatus({collection: statuses}); statuses.on('add', function(status) { $('#statuses').append( '<li>' + status.get('text') + '</li>'); }); }); Refactor templating
  • 51. Refactor templating jQuery(function($) { var statuses = new Collection.Statuses(); new View.PostStatus({collection: statuses}); new View.StatusList({collection: statuses}); });
  • 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
  • 53. jQuery(function($) { var statuses = new Collection.Statuses(); new View.PostStatus({collection: statuses}); new View.StatusList({collection: statuses}); }); RESPONSIBILITY: Initialize application on page load
  • 54. Our tests only have one concern describe("View.StatusList", function() { beforeEach(function() { $el = $('<ul></ul>'); 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!'); }); });
  • 55. PAY ATTENTION TO TESTING PAINS AND ADJUST THE DESIGN ACCORDINGLY.
  • 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.
  • 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.”
  • 58. our code smells when UNIT TESTS ARE SLOW.
  • 59. $ bx rake test:units ............................................................................................... ............................................................................................... ............................................................................................... ............................................................................................... ............................................................................................... ............................................................................................... ............................................................................................... ............................................................................................... ............................................................................................... ............................................................................................... ............................................................................................... ............................................................................................... ............................................................................................... ............................................................................................... ............................................................................................... ............................................................................................... ............................................................................................... ............................................................................................... ............................................................................................... ............................................................................................... ............................................................................................... ............................................................................................... ............................................................................................... ........................................................................................ Finished in 274.623286 seconds. 2273 tests, 6765 assertions, 0 failures, 0 errors
  • 60. WHAT’S WRONG WITH SLOW TESTS?
  • 61. You don't run them often WHAT’S WRONG WITH SLOW TESTS?
  • 62. You don't run them often You waste time waiting for tests WHAT’S WRONG WITH SLOW TESTS?
  • 63. You don't run them often You waste time waiting for tests You distracted others while waiting WHAT’S WRONG WITH SLOW TESTS?
  • 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/
  • 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?
  • 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?
  • 67. unit tests can be slow when they INTERACT WITH SLOW COMPONENTS.
  • 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, 'bkeepers@github.com' assert_equal 'bkeepers@github.com', @message.to end test "#body includes comment body" do assert_match @comment.body, @message.body end end
  • 69. $ ruby test/unit/notifications/emails/message_test.rb ................... Finished in 3.517926 seconds. 19 tests, 24 assertions, 0 failures, 0 errors
  • 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, 'bkeepers@github.com' assert_equal 'bkeepers@github.com', @message.to end test "#body includes comment body" do assert_match @comment.body, @message.body end
  • 71. $ ruby test/unit/notifications/emails/message_test.rb ................... Finished in 0.073752 seconds. 19 tests, 24 assertions, 0 failures, 0 errors
  • 73. unit tests can be slow when they DON’T TEST OBJECTS IN ISOLATION.
  • 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
  • 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
  • 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
  • 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.
  • 79. unit tests can be slow when they BOOTSTRAP HEAVY FRAMEWORKS.
  • 80.
  • 81. $ time ruby test/unit/notifications/email_handler_test.rb
  • 82. $ time ruby test/unit/notifications/email_handler_test.rb ........ Finished in 0.084729 seconds. 8 tests, 10 assertions, 0 failures, 0 errors
  • 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
  • 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
  • 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
  • 88. SLOW TEST MYTHS “Our tests are slow because we have too many of them.”
  • 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.”
  • 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.”
  • 91. PAY ATTENTION TO THE SPEED OF YOUR TESTS AS YOU WRITE THEM.
  • 92. our code smells when IT IS TIGHTLY COUPLED TO A FRAMEWORK.
  • 93. FRAMEWORKS ENCOURAGE YOU TO PUT ALL OF YOUR APPLICATION INSIDE THEIR SANDBOX.
  • 94. …WHICH MAKES CODE DIFFICULT TO TEST, CHANGE AND REUSE.
  • 95.
  • 97. God Objects class Issue < ActiveRecord::Base
  • 98. God Objects class Issue < ActiveRecord::Base # validations validates_presence_of :title, :user_id, :repository_id
  • 99. God Objects class Issue < ActiveRecord::Base # validations validates_presence_of :title, :user_id, :repository_id # associations belongs_to :user
  • 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
  • 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
  • 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| ... }
  • 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
  • 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
  • 105. MAKE THE FRAMEWORK DEPEND ON YOUR APPLICATION, INSTEAD OF MAKING YOUR APPLICATION DEPEND ON THE FRAMEWORK.
  • 106. GOOS
  • 108. coupled to a framework the rest of the application GOOS
  • 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
  • 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
  • 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
  • 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('foobar@foobar.com', 'testing').should be_nil end end # … a couple hundred more lines of specs …
  • 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
  • 114. Plain ol’ Ruby class class PasswordAuthentication def initialize(username, password) @username = username @password = password end def user end end
  • 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
  • 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
  • 117. class PasswordAuthentication def initialize(username, password) @username = username @password = password end def user User.find_by_username(@username) end end
  • 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
  • 119. No changes necessary class PasswordAuthentication def initialize(username, password) @username = username @password = password end def user User.find_by_username(@username) end end
  • 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
  • 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
  • 122. describe PasswordAuthentication do describe 'user' do let!(:user) do create :user, :username => 'bkeepers', :password => 'testing' # hits the DB :( end # … end end
  • 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
  • 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
  • 125. POSITIVE FEEDBACK LOOP Unit tests help us isolate our code and reduce coupling to frameworks, which makes our tests faster.
  • 126. Before we part ways EPILOGUE
  • 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.’”
  • 129. REFERENCES Gary Bernhardt Fast test, SlowTest http://pyvideo.org/video/631/fast-test-slow-test Corey Haines Fast RailsTests http://confreaks.com/videos/641-gogaruco2011-fast-rails-tests