homeASCIIcasts

11: Refactoring User Name Part 2 

(view original Railscast)

Other translations: It Es Fr

In the previous episode we talked about refactoring and how we can move code from the view into a method in the model in order to eliminate code duplication. The code in our User model could still be improved though.

class User < ActiveRecord::Base
  def full_name
    name = first_name + ' '
    name += "#{middle_initial}. " unless middle_initial.nil?
    name += last_name
  end
end

Our user model as it was at the end of the last episode.

Testing

Before we start refactoring our User model, let’s look at testing. Testing and refactoring go hand-in-hand, as the aim of refactoring is to improve your code without changing its behaviour. Good test coverage ensures that our refactoring doesn’t affect what the code does. When a model is created in a Rails app, a test file for that model is automatically created. Let’s look at the user_test.rb file in the /test/unit folder1.

require 'test_helper'
class UserTest < ActiveSupport::TestCase
  # Replace this with your real tests.
  test "the truth" do
    assert true
  end
end

The default test supplied by Rails.

There is a default test supplied in the code above which simply asserts that true == true. We’ll replace it with the first test of our own, creating a new user without a middle initial and checking that the full name we expect is returned.

test "full name without middle initial" do
  user = User.new(:first_name => "John", :last_name => "Smith")
  assert_equal 'John Smith', user.full_name
end

Our first test checks a name without a middle initial.

To run our tests we’d could run rake test from the command line while in our application’s folder, but we’re going to use AutoTest2. This can be installed by installing the ZenTest gem (sudo gem install ZenTest). AutoTest has the advantage that it will continually run our tests so it’s easy to see when we’ve broken something. We’ll run autotest and see what results we get.

Laa-Laa:ep11 eifion$ autotest
loading autotest/rails
/usr/local/bin/ruby -I.:lib:test -rtest/unit -e "%w[test/functional/users_controller_test.rb test/unit/user_test.rb].each { |f| require f }" | unit_diff -u
Loaded suite -e
Started
.
Finished in 0.046945 seconds.
1 tests, 1 assertions, 0 failures, 0 errors

Output from AutoTest

Our first test passes! Now we’ll create another test for a name with a middle initial by adding the method below to our UserTest class.

test "full name with middle initial" do
  user = User.new(:first_name => "Paul", :middle_initial => "P", :last_name => "Hughes")
  assert_equal 'Paul P. Hughes', user.full_name
end

When you save the test file, AutoTest should automatically rerun the tests. Both tests should now pass.

Refactoring

Now that we know that our full_name method behaves as we expect it to we can start to refactor it. The use of the local variable in the method and the string concatenation seem unnecessary so we’ll remove those. We could instead put each part of the user’s name in to an array and join them with a space.

class User < ActiveRecord::Base
  def full_name
    [first_name, middle_initial, last_name].join(' ')
  end
end

First refactor of the User class.

When we save the file, the tests should run automatically and we can see that they both fail.

  1) Failure:
  test_full_name_with_middle_initial(UserTest)
  [./test/unit/user_test.rb:11:in `test_full_name_with_middle_initial'
  /usr/local/lib/ruby/gems/1.8/gems/activesupport-2.2.2/lib/active_support/testing/setup_and_teardown.rb:60:in `run']:
  --- /var/folders/yD/yDkhXjIsHAqkCTKsBbUlC++++TI/-Tmp-/expect31666.0    2009-01-04 11:23:35.000000000 +0000
  +++ /var/folders/yD/yDkhXjIsHAqkCTKsBbUlC++++TI/-Tmp-/butwas31666.0    2009-01-04 11:23:35.000000000 +0000
  @@ -1 +1 @@
  -Paul P. Hughes
  +Paul P Hughes
  2) Failure:
  test_full_name_without_middle_initial(UserTest)
  [./test/unit/user_test.rb:6:in `test_full_name_without_middle_initial'
  /usr/local/lib/ruby/gems/1.8/gems/activesupport-2.2.2/lib/active_support/testing/setup_and_teardown.rb:60:in `run']:
  --- /var/folders/yD/yDkhXjIsHAqkCTKsBbUlC++++TI/-Tmp-/expect31666.1    2009-01-04 11:23:35.000000000 +0000
  +++ /var/folders/yD/yDkhXjIsHAqkCTKsBbUlC++++TI/-Tmp-/butwas31666.1    2009-01-04 11:23:35.000000000 +0000
  @@ -1 +1 @@
  -John Smith
  +John  Smith
  2 tests, 2 assertions, 2 failures, 0 errors

AutoTest output showing two failing unit tests.

The first test fails as there’s no full stop after the middle initial; the second because there are two spaces between the first and last names. We’ll try to fix the second test first. There are two spaces because the middle initial is nil and we’re joining each part of the array with a space. We can fix this by calling compact on the array before we join each part. (compact removes all nil values from an array.)

def full_name
  [first_name, middle_initial, last_name].compact.join(' ')
end

Now, only our first test fails: we need the full stop after the middle initial. We’ll fix this by creating a new method in the User class called middle_initial_with_full_stop.

def full_name
  [first_name, middle_initial_with_full_stop, last_name].compact.join(' ')
end  
def middle_initial_with_full_stop
  "#{middle_initial}." unless middle_initial.blank?
end

Our tests now pass, but there’s one condition we’ve not tested. What if the middle initial is an empty string? We can write a test for that and see if it passes.

test "full name with empty middle initial" do
  user = User.new(:first_name => "John", :middle_initial => "", :last_name => "Jones")
  assert_equal 'John Jones', user.full_name
end

This passes too. The blank? method for a string will return true for either a nil value or an empty string, so our name with an empty middle initial still returns the expected result.

Our User model code now looks much better than it did before. There is still some duplication in the testing code, though. We’ll look at that in the next episode.

Final code

class User < ActiveRecord::Base
  def full_name
    [first_name, middle_initial_with_full_stop, last_name].compact.join(' ')
  end  
  def middle_initial_with_full_stop
    "#{middle_initial}." unless middle_initial.blank?
  end
end

The User class after refactoring.

  require 'test_helper'
  class UserTest < ActiveSupport::TestCase
    test "full name without middle initial" do
      user = User.new(:first_name => "John", :last_name => "Smith")
      assert_equal 'John Smith', user.full_name
    end
    test "full name with middle initial" do
      user = User.new(:first_name => "Paul", :middle_initial => "P", :last_name => "Hughes")
      assert_equal 'Paul P. Hughes', user.full_name
    end
    test "full name with empty middle initial" do
      user = User.new(:first_name => "John", :middle_initial => "", :last_name => "Jones")
      assert_equal 'John Jones', user.full_name
    end
  end

The tests for the User class.

Notes

  1. The RailsCast this episode is based on was based on Rails 1. The code above was written using Rails 2.2.
  2. http://rubyforge.org/projects/zentest/