#11 Refactoring User Name Part 2
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
- The RailsCast this episode is based on was based on Rails 1. The code above was written using Rails 2.2.
- http://rubyforge.org/projects/zentest/