Tuesday, February 12, 2008

Testing: Expect literals

When writing a state based test you must specify an expected value and an actual value. Whether you are using RSpec, Test::Unit, or any other framework, there is always an expected and an actual value.

# RSpec
presenter.hotel.should == :the_hotel

# Test::Unit
assert_equal :the_hotel, presenter.hotel

Expected values (:the_hotel in our example) can be any object; however, using literals (strings, integers, symbols) for expected values is advantageous for readability and traceability. To be clear, the expected value should be the literal itself, not a variable holding a literal that was created earlier in the test.

Tests that use literals are easier to read because they allow you to focus on the actual value, since the expected value is simply the literal. The following 3 tests verify the same thing, but the 3rd example uses a literal for the expected value. Since the 3rd example uses a literal, it's easy to see what the value of the actual should be without scanning the body of the test.

require 'test/unit'
require 'rubygems'
require 'expectations'

class SuiteTests < Test::Unit::TestCase
EXPECTED = :expected

def test_expectations_for_should_only_run_the_test_on_the_specified_line_with_local_var
expected = :expected
suite = Expectations::Suite.new
suite.expect(1) { 2 }
suite.expect(expected) { 2 }
assert_equal expected, suite.expectations_for(__LINE__ - 1).first.expected
end

def test_expectations_for_should_only_run_the_test_on_the_specified_line_with_constant
suite = Expectations::Suite.new
suite.expect(1) { 2 }
suite.expect(EXPECTED) { 2 }
assert_equal EXPECTED, suite.expectations_for(__LINE__ - 1).first.expected
end

def test_expectations_for_should_only_run_the_test_on_the_specified_line_with_literal
suite = Expectations::Suite.new
suite.expect(1) { 2 }
suite.expect(:expected) { 2 }
assert_equal :expected, suite.expectations_for(__LINE__ - 1).first.expected
end
end

note: these tests are fairly clean by design, the value of expecting literals can be even greater as the tests become more complex, thus harder to follow.

Expecting literals also makes your life easier when a test breaks. If neither your expected or actual values are literals, you will need to determine the value of both of them when a test breaks. Conversely, if your expected is a literal, only the actual value will need to be traced when a test is broken. For example the following tests both verify the result of the popularity method; however, the 2nd version is explicit in the value in expects.

require 'test/unit'

WEIGHT = 1.14

class Topic
def initialize(votes)
@votes = votes
end

def popularity
WEIGHT * @votes * @votes
end
end

class CircleTest < Test::Unit::TestCase
def test_popularity_with_variables
votes = 2
assert_equal WEIGHT * votes * votes, Topic.new(votes).popularity
end

def test_popularity_with_literals
assert_equal 4.56, Topic.new(2).popularity
end
end

In the above example, if the second test breaks it's because the calculation changed in some way. When the calculation changes, it's valuable to get feedback that it changed (by way of a breaking test). The first test doesn't use any literals, but it will not break if the value of WEIGHT is changed. Some people might consider this valuable or perhaps even more maintainable, I'd consider it a bug waiting to happen.

In reality, it's not likely that the programmer is responsible for setting the value of WEIGHT. Most businesses would have a subject matter expert that would give the value of weight and express (via requirements) the formula for popularity. The programmer would write the actual Ruby code, but using literal numbers for the expected value would allow the programmer to verify the functionality with the business. If in the future the business wants to change the value of WEIGHT, the programmers and the business could first change the tests to represent the new expected values, and then they could change the implementation. When you change code, it's always valuable to have a breaking test. Even if the business didn't sit with the programmer, the programmer could request the expected values as acceptance criteria. The tests will be easier to read, but they will also protect the business from a mistaken change to WEIGHT.

There are times when it's not possible to use a literal as the expected value. For example, when you need to assert a time or date you'll need to use a Time or Date object respectively. You can handle these situations in various ways. I prefer to use expected values that are as close to literals as I can get. This means using a literal as an argument to a method or calling to_s on the actual and expecting a string. The following two examples illustrate both of these techniques.

require 'test/unit'
require 'date'

class DatesTest < Test::Unit::TestCase
def test_valentines_day_last_year
assert_equal Date.parse("2/14/2007"), ValentinesDay.last_year
end

def test_valentines_day_last_year_to_s
assert_equal "2007-02-14", ValentinesDay.last_year.to_s
end
end

It's not always possible to use literals for expected values, but if you strive to use literals whenever possible your tests will be more readable and traceable in the future.

3 comments:

  1. This comment relates more so to the one assertion per guideline rule but also ties in with your current post.

    I agree that you should only do one expectation per test, however I think that it is not testing the method enough. Eg: (and sorry for this formatting)

    def list
    mylist = List.find()
    view_adapter.render(mylist)
    my_rendered_list = mylist
    end

    To test the controller method above, these tests do the trick.

    test 'should return my_rendered_list object' do
    get :list
    assert_equal :expected, assigns('my_rendered_list')
    end
    test 'should render view adapter for mylist' do
    view_adapter.expects(:render).with(mylist).returns(:expected)
    get :list
    end

    However, they can be also be valid for the method:

    def list
    mylist = List.find()
    view_adapter.render(mylist)
    my_rendered_list = mylist
    end


    My test would look something along the lines of:

    test 'should render view adapter for mylist' do
    view_adapter.expects(:render).with(mylist).returns(:expected)
    get :list
    assert_equal :expected, assigns('my_rendered_list')
    end


    Any thoughts?

    ReplyDelete
  2. Hello Sarah,
    It's a bit tough to get context on your question (sorry), but I think you are asking if I'd combine both the tests since neither is doing much.

    The easy answer is no, since I like sticking to one expectation per test, but I can see why you think these tests are too anemic.

    Instead of putting the two together, I'd probably look for other ways to test the method, or other ways to model the solution. I think tests reflect code quality, so any time something is hard/ugly to test, then there might be a problem.

    Or, if you prefer putting them together, that's probably okay also. You could try that route and see if it's more or less painful than the way that I suggest. The goal is to get the solution with the least pain.

    Cheers, Jay

    ReplyDelete
  3. Hey Jay,
    I guess I didn't explain myself too well. I was thinking of the case where you want to test that your method (list) makes a call to another object (view_adapter with the call render) and you want to ensure that your method (list) assigns the return from render correctly. [hmm....this probably is'nt any clearer]
    I was thinking along the lines that in controller tests you generally want to call get a particular object from the model and assign it to a variable for your view. So I would want mock the call and assert that the assigns has been made correctly.

    I guess I just wonder if expecting on the method call in one test and validating that you are returned the correct object in another test is thorough enough.
    Looking at the tests again now though, I guess it is. Even observing the test names I gave, it is obvious that I am testing two different behaviors, and therefore they should be kept separate.

    Anyway, thanks for tuning me onto behavior based testing. It has helped a lot in my work.

    Cheers
    Sarah

    ReplyDelete

Note: Only a member of this blog may post a comment.