Permalink
Browse files

Add MentalModel::Matcher w/ some specs

  • Loading branch information...
1 parent 4b52d2f commit 47cb35da9c9a94efeb25ab36a79f87cf65bbcc8b @geeksam geeksam committed Mar 30, 2012
Showing with 198 additions and 0 deletions.
  1. +80 −0 lib/kookaburra/mental_model.rb
  2. +118 −0 spec/kookaburra/mental_model_matcher_spec.rb
@@ -114,5 +114,85 @@ def dup
self.class.new(@name, new_data)
end
end
+
+ # This is a custom matcher that matches the RSpec matcher API.
+ # (The test_helpers.rb file provides a match function
+ # for RSpec and a custom assertion for Test::Unit.)
+ class Matcher
+ attr_reader :expected_items, :unexpected_items, :collection_key
+
+ def initialize(mental_model, collection_key)
+ @collection_key = collection_key
+
+ mental_model.send(collection_key).tap do |collection|
+ @expected_items = collection.values
+ @unexpected_items = collection.deleted.values
+ end
+ end
+
+ def matches?(actual)
+ @actual = actual
+ clear_memoization!
+ expected_items_not_found.empty? && unexpected_items_found.empty?
+ end
+
+ def failure_message_for_should
+ message = "expected #{@collection_key} to match the user's mental model, but:\n"
+ if expected_items_not_found.present?
+ message += "expected to be present: #{pp_array(expected_items)}\n"
+ message += "the missing elements were: #{pp_array(expected_items_not_found)}\n"
+ end
+ if unexpected_items_found.present?
+ message += "expected to not be present: #{pp_array(unexpected_items)}\n"
+ message += "the unexpected extra elements: #{pp_array(unexpected_items_found)}\n"
+ end
+ message
+ end
+
+ def failure_message_for_should_not
+ "expected #{@collection_key} not to match the user's mental model"
+ end
+
+ def description
+ "match the user's mental model of #{@collection_key}"
+ end
+
+ protected
+
+ def clear_memoization!
+ @expected_items_not_found = @unexpected_items_found = nil
+ end
+
+ def expected_items_not_found
+ @expected_items_not_found ||= begin
+ difference_between_arrays(expected_items, @actual)
+ end
+ end
+
+ def unexpected_items_found
+ @unexpected_items_found ||= begin
+ unexpected_items_not_found = difference_between_arrays(unexpected_items, @actual)
+ difference_between_arrays(unexpected_items, unexpected_items_not_found)
+ end
+ end
+
+ # (Swiped from RSpec's array matcher)
+ # Returns the difference of arrays, accounting for duplicates.
+ # e.g., difference_between_arrays([1, 2, 3, 3], [1, 2, 3]) # => [3]
+ def difference_between_arrays(array_1, array_2)
+ difference = array_1.dup
+ array_2.each do |element|
+ if index = difference.index(element)
+ difference.delete_at(index)
+ end
+ end
+ difference
+ end
+
+ def pp_array(array)
+ array = array.sort if array.all? { |e| e.respond_to?(:<=>) }
+ array.inspect
+ end
+ end
end
end
@@ -0,0 +1,118 @@
+require 'kookaburra/mental_model'
+
+describe Kookaburra::MentalModel::Matcher do
+ let(:mm) { Kookaburra::MentalModel.new }
+
+ def matcher_for(collection_key)
+ Kookaburra::MentalModel::Matcher.new(mm, collection_key)
+ end
+
+ let(:matcher) { matcher_for(:widgets).tap { |m| m.matches?(target) } }
+
+ let(:foo) { { :name => 'Foo', :type => 'widget' } }
+ let(:bar) { { :name => 'Bar', :type => 'widget' } }
+ let(:yak) { { :name => 'Yak', :type => 'widget' } }
+
+ context "when the named collection has nothing in it" do
+ context "and the actual list is an empty array" do
+ let(:target) { [] }
+
+ it "#matches? returns true when given an empty array" do
+ matcher.matches?(target).should be_true
+ end
+ end
+
+ context "and the actual list is [foo] (foo not in mental model)" do
+ let(:target) { [foo] }
+ it "#matches? returns true" do
+ matcher.matches?(target).should be_true
+ end
+ end
+ end
+
+ context "when the named collection has foo in it" do
+ before(:each) do
+ mm.widgets[:foo] = foo
+ end
+
+ context "and the actual list is an empty array (foo missing)" do
+ let(:target) { [] }
+
+ it '#matches? returns false' do
+ matcher.matches?(target).should be_false
+ end
+
+ it "#failure_message_for_should complains about missing element" do
+ matcher.failure_message_for_should.should == <<-EOF
+expected widgets to match the user's mental model, but:
+expected to be present: #{[foo].inspect}
+the missing elements were: #{[foo].inspect}
+EOF
+ end
+ end
+
+ context "and the actual list is [foo] (OK)" do
+ let(:target) { [foo] }
+
+ it "#matches? returns true" do
+ matcher.matches?(target).should be_true
+ end
+ end
+
+ context "and the actual list is [foo, bar] (bar not in mental model)" do
+ let(:target) { [foo, bar] }
+
+ it "#matches? returns true" do
+ matcher.matches?(target).should be_true
+ end
+ end
+ end
+
+ context "when the named collection has foo in it, and deleted bar" do
+ before(:each) do
+ mm.widgets[:foo] = foo
+ mm.widgets[:bar] = bar
+ mm.widgets.delete(:bar)
+ end
+
+ context "when the actual list is an empty array (foo missing)" do
+ let(:target) { [] }
+
+ it "#matches? returns false" do
+ matcher.matches?(target).should be_false
+ end
+ end
+
+ context "when the actual list is [bar] (foo missing, bar deleted)" do
+ let(:target) { [bar] }
+
+ it "#matches? returns false" do
+ matcher.matches?(target).should be_false
+ end
+ end
+
+ context "when the actual list is [foo, bar] (bar deleted)" do
+ let(:target) { [foo, bar] }
+
+ it "#matches? returns false" do
+ matcher.matches?(target).should be_false
+ end
+ end
+
+ context "when the actual list is [foo] (foo expected)" do
+ let(:target) { [foo] }
+
+ it "#matches? returns true" do
+ matcher.matches?(target).should be_true
+ end
+ end
+
+ context "when the actual list is [foo, yak] (foo expected; yak not in mental model)" do
+ let(:target) { [foo, yak] }
+
+ it "#matches? returns true" do
+ matcher.matches?(target).should be_true
+ end
+ end
+ end
+end

0 comments on commit 47cb35d

Please sign in to comment.