Monday, September 5, 2011

Unit Testing a Simple Property Model

Previously I showed the beginning of a Ruby implementation of a simple property model. Well, I decided to create some unit tests to help verify that my property model correctly handles properties, especially with the prototype delegation.

RSpec seems to be popular in the rails world, so I used rspec to write my tests. This exercise provided me with examples of the good, the bad, and the ugly with unit tests. The good was straightforward. I don't have an application yet, just this simple library and unit tests provided an easy way to test it. Even better, at one point I changed an implementation detail that required some rewriting of the property model, and by rerunning the tests, I had confidence that I didn't break anything.

The ugly? This is definitely a good example of ugly. I have the unit testing code at the bottom of this post. I found I had a lot of duplication, so I created "shared_examples_for" to reduce redundancy. However, the resulting test code is still really long and while the shared examples helps with sharing tests, I am not sure they help with readability. Now, as Trevor commented, its possible (ok, likely) that the unit tests are ugly because they weren't written well. I will admit to being a newbie at RSpec. I am sure that they could be written better, but I am not convinced they wouldn't still be somewhat ugly.

The bad? Well, there isn't too much bad, yet. It did take time to create the tests, but I had to validate my code somehow. And it is possibly (likely?) that my tests aren't as thorough as they should be, so maybe I have some false confidence in the correctness of my code. But all in all, despite what I said last paragraph, this wasn't that good of an example of the bad. At least not yet. The bad will come when I spend time trying to clean up these tests, rather than using that time to write new code.

And without further ado, here are these unit tests.

require 'spec_helper'

describe Thing do
  let(:foo) {5}
  let(:bar) {"hello"}

  shared_examples_for "simple properties" do
    describe "keys" do
      it {thing.keys.size.should == 2}
    end
    describe "foo" do
      it {thing['foo'].should == foo}
      it {thing.foo.should == foo}
    end
    describe "bar" do
      it {thing['bar'].should == bar}
      it {thing.bar.should == bar}
    end
    describe "baz" do
      it {thing['baz'].should be_nil}
      it {thing.baz.should be_nil}
    end
    describe "to_hash" do
      it {thing.to_hash.should_not be_nil}
      it {thing.to_hash.size.should == 2}
      it {thing.to_hash['foo'].should == foo}
      it {thing.to_hash['bar'].should == bar}
    end
  end

  shared_examples_for "nested properties" do
    describe "keys" do
      it {thing.keys.size.should == 4}
      it {thing.self_keys.size.should == 2}
      it {thing.self_keys.include?('b2').should be_true}
      it {thing.self_keys.include?('bye').should be_true}
      it {thing.self_keys.include?('bar').should be_false}
    end
    describe "foo" do
      it {thing['foo'].should == 5}
      it {thing.foo.should == 5}
    end
    describe "bar" do
      it {thing['bar'].should == "hello"}
      it {thing.bar.should == "hello"}
    end
    describe "b2" do
      it {thing['b2'].should == 15}
      it {thing.b2.should == 15}
    end
    describe "bye" do
      it {thing['bye'].should == "bye"}
      it {thing.bye.should == "bye"}
    end
    describe "baz" do
      it {thing['baz'].should be_nil}
      it {thing.baz.should be_nil}
    end
    describe "to_hash" do
      it {thing.to_hash.should_not be_nil}
      it {thing.to_hash.size.should == 4}
      it {thing.to_hash['foo'].should == 5}
      it {thing.to_hash['bar'].should == "hello"}
      it {thing.to_hash['b2'].should == 15}
      it {thing.to_hash['bye'].should == "bye"}
    end
  end
 
  describe "no inheritance" do
    let(:thing) {Thing.new}
    subject {thing}

    describe "new object" do
      describe "keys" do
        it {thing.keys.should_not be_nil}
        it {thing.keys.should be_empty}
      end
      describe "properties" do
        it {thing['foo'].should be_nil}
        it {thing.foo.should be_nil}
      end
      describe "to_hash" do
        it {thing.to_hash.should_not be_nil}
        it {thing.to_hash.size.should == 0}
      end
    end
   
    describe "adding properties to object" do
      before(:each) do
        thing['foo'] = 5
        thing['bar'] = "hello"
      end
      it_should_behave_like "simple properties"
      it {thing.self_keys.size.should == 2}
      it {thing.self_keys.include?('foo').should be_true}
      it {thing.self_keys.include?('bar').should be_true}
      it {thing.self_keys.include?('baz').should be_false}
    end
    describe "adding fields to object" do
      before(:each) do
        thing.foo = 5
        thing.bar = "hello"
      end
      it_should_behave_like "simple properties"
      it {thing.self_keys.size.should == 2}
      it {thing.self_keys.include?('foo').should be_true}
      it {thing.self_keys.include?('bar').should be_true}
      it {thing.self_keys.include?('baz').should be_false}
    end
  end

  describe "inheritance" do
    let(:thing) do
      base = Thing.new
      base.foo = 5
      base['bar'] = 'hello'
      foo = Thing.new
      foo.prototype = base
      foo
    end
    subject {thing}
 
    describe "new object" do
      it_should_behave_like "simple properties"
      it {thing.self_keys.include?('bar').should be_false}
      it {thing.self_keys.include?('baz').should be_false}
    end
   
    describe "adding properties to object" do
      before(:each) do
        thing['b2'] = 15
        thing['bye'] = "bye"
      end
      it_should_behave_like "nested properties"
    end
    describe "adding fields to object" do
      before(:each) do
        thing.b2 = 15
        thing.bye = "bye"
      end
      it_should_behave_like "nested properties"
    end
    describe "adding redundant fields to object" do
      let(:foo) {15}
      let(:bar) {"bar2"}
      before(:each) do
        thing.foo = foo
        thing.bar = bar
      end
      it_should_behave_like "simple properties"
      it {thing.self_keys.size.should == 2}
      it {thing.self_keys.include?('foo').should be_true}
      it {thing.self_keys.include?('bar').should be_true}
      it {thing.self_keys.include?('baz').should be_false}
    end
  end
end


And here is the output of running the above tests:


$ rspec spec/models/thing_spec.rb

Thing
  no inheritance
    new object
      keys
        should not be nil
        should be empty
      properties
        should be nil
        should be nil
      to_hash
        should not be nil
        should == 0
    adding properties to object
      should == 2
      should be true
      should be true
      should be false
      it should behave like simple properties
        keys
          should == 2
        foo
          should == 5
          should == 5
        bar
          should == "hello"
          should == "hello"
        baz
          should be nil
          should be nil
        to_hash
          should not be nil
          should == 2
          should == 5
          should == "hello"
    adding fields to object
      should == 2
      should be true
      should be true
      should be false
      it should behave like simple properties
        keys
          should == 2
        foo
          should == 5
          should == 5
        bar
          should == "hello"
          should == "hello"
        baz
          should be nil
          should be nil
        to_hash
          should not be nil
          should == 2
          should == 5
          should == "hello"
  inheritance
    new object
      should be false
      should be false
      it should behave like simple properties
        keys
          should == 2
        foo
          should == 5
          should == 5
        bar
          should == "hello"
          should == "hello"
        baz
          should be nil
          should be nil
        to_hash
          should not be nil
          should == 2
          should == 5
          should == "hello"
    adding properties to object
      it should behave like nested properties
        keys
          should == 4
          should == 2
          should be true
          should be true
          should be false
        foo
          should == 5
          should == 5
        bar
          should == "hello"
          should == "hello"
        b2
          should == 15
          should == 15
        bye
          should == "bye"
          should == "bye"
        baz
          should be nil
          should be nil
        to_hash
          should not be nil
          should == 4
          should == 5
          should == "hello"
          should == 15
          should == "bye"
    adding fields to object
      it should behave like nested properties
        keys
          should == 4
          should == 2
          should be true
          should be true
          should be false
        foo
          should == 5
          should == 5
        bar
          should == "hello"
          should == "hello"
        b2
          should == 15
          should == 15
        bye
          should == "bye"
          should == "bye"
        baz
          should be nil
          should be nil
        to_hash
          should not be nil
          should == 4
          should == 5
          should == "hello"
          should == 15
          should == "bye"
    adding redundant fields to object
      should == 2
      should be true
      should be true
      should be false
      it should behave like simple properties
        keys
          should == 2
        foo
          should == 15
          should == 15
        bar
          should == "bar2"
          should == "bar2"
        baz
          should be nil
          should be nil
        to_hash
          should not be nil
          should == 2
          should == 15
          should == "bar2"

Finished in 0.06217 seconds
106 examples, 0 failures