Following on from my previous posts–Building Your First Rails Application: Models and Views and Controllers–I’m going to cover a simple test-driven approach to adding a new feature to our URL shortener application, Shorty.
To test out this process, we’re going to make the site function more like real life URL shorteners – that is, we’ll test and implement a way to generate a simple short code that represents the URL we’re shortening.
The functionality itself is relatively simple (In our case, we’ll convert the id back and forth between an alpha numeric representation) but it’s a good opportunity to cover a test-driven approach to implementation. To do this, we’ll be using Rails’s integrated testing tools and at the end I’ll also link to a few more options to explore in your own time.
Today, we’ll cover the model portion and in the next post we’ll integrate our shortened urls and write some controller tests.
What We Need
For this to work, we’re going to have to implement three separate things:
- A way to convert a given stored URL to a short code
- A way to convert from a short code to a stored URL
- A new, shorter route to fetch a URL from that.
Getting started, we’ll want to open up the existing test files. When we generated our URL model in the part one, Rails also generated a stubbed out test for us in
test/unit/url_test.rb. Opening it up and taking a look, you should see something similar to:
require 'test_helper' class UrlTest < ActiveSupport::TestCase # Replace this with your real tests. test "the truth" do assert true end end
This is the general structure of a unit test in Rails 3 – the
test method lets us declare a method (we also have
teardown to deal with maintaining a generalised environment for our tests) and we use asserts (e.g. the
assert method call in the code above). Rails (and
Test::Unit, the testing framework rails integrate) ship with several assertions out of the box that we can use – for a list, see the methods starting with
assert_ at the rails api docs and this older cheatsheet for some of the standard test unit assertions.
Next, we’ll add some test stubs—empty tests that we can fill out later. To do this, we need to work out exactly what we want to test in the most basic terms. Inside the URL test class, replace the existing test lines with the following:
test 'creating a url lets us fetch a short code' test 'existing urls have short codes' test 'converting a short code to an id' test 'finding a url from a known short code' test 'finding a url from a invalid short code raises an exception'
Next, from the command line, we can run these empty tests to verify they fail by running the following from our application directory:
Since we haven’t done anything other than write their names, we should get four failures.
Now, we’ll go through our tests 1 by and write them. To get started, fill them out one by one, replacing the test stub as you go:
test 'creating a url lets us fetch a short code' do my_url = Url.create(:url => 'http://google.com/') # The url should have a short code assert_present my_url.short_code end test 'existing urls have short codes' do my_url = Url.create(:url => 'http://google.com/') # Force a fetch from the datbase found_url = Url.find(my_url.id) assert_present found_url.short_code assert_equal my_url.short_code, found_url.short_code end test 'finding a url from a known short code' do my_url = Url.create(:url => 'http://google.com/') assert_equal my_url, Url.find_using_short_code!(my_url.short_code) end test 'finding a url from a invalid short code raises an exception' do assert_raises ActiveRecord::RecordNotFound do Url.find_using_short_code! 'non-existant-short-code' end end
In each of the tests, we test some facet of the expected model behaviour:
- In our first test we check that once we create a URL, that it has a short code by calling the
short_codemethod and invoking
assert_presentwith its value.
- In the second test, we create a URL, force-reload it from the database (to simulate fetching it at a later point in time) and then check that it also has a short code and more importantly that the found object has the same short code.
- In the third test, we create a URL, and test that when we fetch it from the database (using our currently non-existent method
find_using_short_code!) that it’ll return the same url.
- In our last test, we check that when we give it an invalid short code, it raises an exception as expected.
Switching back to the command line and running our tests again using
rake test:units, we should still see 4 failures. This is good—it means we have tests but haven’t actually implemented them yet.
Making Our Tests Pass
Now, we’re going to make our tests pass. In order to do this, we need to implement two methods. First, we the
short_code instance method on the
Url class and then
find_using_short_code! class method.
url.rb, we’ll add a method to generate a short code. For the moment, we’ll just use the base 36 value of id (e.g.
10 will be
class Url < ActiveRecord::Base validates :url, :presence => true def short_code id.to_s 36 end end
Re-running our tests, we’ll see 2 out of 4 of our tests now pass. Next, we’ll implement a method to find it from the id by doing the reverse conversion (taking a number from the base 36 value):
class Url < ActiveRecord::Base validates :url, :presence => true def short_code id.to_s 36 end def self.find_using_short_code!(short_code) find short_code.to_i(36) end end
Running our tests one last time, we’ll see that they all now pass – we can now get and generate the short codes.
In the next post, we’ll cover how to integrate our short codes into the controller and how to test it.
For the moment, see if you can write a few more tests your self – One case worth considering is what happens when you don’t have an id on the model? (e.g. it hasn’t been saved yet).
Eager readers may also want to read up about how to integrate RSpec into their application for an alternative syntax and approach for writing tests. For extra credit, you may also change how we convert back and forth between ids and short codes – e.g. instead of using base 36 (0 to 9 and a to b) you may include the difference between lower and uppercase letters as well.