Permalink
Browse files

major refactoring, ~25% less code, much abstracted, thus more consist…

…ent, flexible and powerful. added :in and :not_in conditions, arbitrarily nesteable contexts and :with and/or combinations
  • Loading branch information...
1 parent 11ca70c commit 1b2a28eda5671e067927d5d7647cdf4352f1874c Sven Fuchs committed Dec 25, 2008
View
@@ -37,7 +37,7 @@ So it allows to write test/unit tests like this:
end
end
- with :login_as_user, :no_login do
+ with [:login_as_user, :no_login] do
it_redirects_to '/login'
end
end
View
@@ -1,4 +1,4 @@
-require File.dirname(__FILE__) + '/test/test_helper'
+require File.dirname(__FILE__) + '/test/helper'
require 'rubygems'
require 'actionpack'
@@ -80,13 +80,21 @@ class ActionController::TestCase
before { @params = valid_article_params }
end
- share :invalid_article_params do
+ share :invalid_article_params, 'missing title' do
before { @params = valid_article_params.except(:title) }
end
- share :invalid_article_params do
+ share :invalid_article_params, 'missing body' do
before { @params = valid_article_params.except(:body) }
end
+
+ share :caching do
+ before { @caching = true }
+ end
+
+ share :observers do
+ before { @observers = true }
+ end
def it_redirects_to(path = nil, &block)
path = instance_eval(&block) if block
@@ -109,6 +117,8 @@ def valid_article_params
# and now the fun starts ...
class ArticlesControllerTest < ActionController::TestCase
+ with_common :caching, :observers
+
describe 'POST to :create' do
before do
# set up some preconditions
@@ -117,7 +127,9 @@ class ArticlesControllerTest < ActionController::TestCase
action { post :create, @params }
- it "has called the before block" do
+ it "has called the before blocks" do
+ assert @caching
+ assert @observers
assert @before_block_called
end
@@ -137,7 +149,7 @@ class ArticlesControllerTest < ActionController::TestCase
end
end
- with :login_as_user, :no_login do
+ with [:login_as_user, :no_login] do
it_redirects_to { '/login' }
end
end
View
@@ -1,7 +1,7 @@
+require 'with/node'
require 'with/sharing'
require 'with/context'
-require 'with/group'
-require 'with/named_block'
+require 'with/call'
module With
def self.included(base)
@@ -10,19 +10,38 @@ def self.included(base)
module ClassMethods
include Sharing
-
- def inherited(base)
- base.instance_variable_set(:@shared, @shared)
+
+ def with_common(*names)
+ @with_common ||= []
+ @with_common += names
end
-
+
def describe(name, &block)
- group = Group.new name, &block
- shared.each {|name, groups| group.share(name, *groups) }
- group.compile(self)
- group
+ context = Context.build(*with_common + [name], &block).first
+ context.compile(self)
+ context
end
end
+
+ class << self
+ def applies?(names, conditions)
+ conditions[:in].nil? || names.include?(conditions[:in]) and
+ conditions[:not_in].nil? || !names.include?(conditions[:not_in])
+ end
+ def aspects
+ @@aspects ||= []
+ end
+
+ def aspect?(aspect)
+ self.aspects.include?(aspect)
+ end
+ end
+
+ # hmm, i can't see a way to solve nested it blocks rather than this because
+ # :it usually indicates an assertion and within assertions we want to be able
+ # to use instance vars as in:
+ # it "articles should be new" do @article.new_record?.should == true end
def it(name, &block)
yield
end
View
@@ -0,0 +1,31 @@
+module With
+ extend Sharing
+
+ class Call
+ attr_reader :name, :block
+
+ def initialize(name, conditions = {}, &block)
+ raise "need to provide a block" unless block
+
+ @name = name
+ @conditions = conditions
+ @block = Proc.new {
+ @_with_current_context = name
+ instance_eval &block
+ }
+ end
+
+ def applies?(context)
+ names = context.parents.map(&:name) << context.name
+ With.applies?(names, @conditions)
+ end
+
+ def to_proc
+ @block
+ end
+
+ def call
+ to_proc.call
+ end
+ end
+end
View
@@ -1,78 +1,86 @@
module With
- class Context
- attr_accessor :parent
- attr_reader :name, :children, :action, :assertions, :preconditions
-
- def initialize(name, action, preconditions, assertions, &block)
- @name, @action, @preconditions, @assertions = name, action, [], []
- @children = []
-
- instance_eval &block if block
-
- @preconditions += preconditions
- @assertions += assertions
- end
-
- def calls
- [find_action, collect_preconditions, collect_assertions]
- end
-
- def parents
- parent ? parent.parents + [parent] : []
+ class Context < Node
+ class << self
+ def build(*names, &block)
+ context = new
+
+ names.each do |names|
+ children = Array(names).map do |name|
+ name.is_a?(Symbol) ? With.shared(name) : new(name)
+ end.flatten
+ context.append_children children
+ end
+
+ context.children.each do |child|
+ child.parent = nil
+ child.leafs.each { |leaf| leaf.instance_eval(&block) } if block
+ end
+ end
end
-
- def leafs
- return [self] if children.empty?
- children.map { |child| child.leafs }.flatten
+
+ def initialize(name = nil, &block)
+ super(name)
+ instance_eval &block if block
end
-
- def add_children(*children)
- children.each do |child|
- child.parent = self
- @children << child
+
+ def with(*names, &block)
+ options = names.last.is_a?(Hash) ? names.pop : {}
+ Context.build(*names, &block).each do |child|
+ add_child child
+ child.filter(options) unless options.empty?
end
end
-
- def append_children(children)
- leafs.each { |leaf| leaf.add_children *children }
+
+ [:before, :action, :assertion, :after].each do |name|
+ class_eval <<-code
+ def #{name}(name = nil, options = {}, &block)
+ contexts = options[:with] ? with(*options.delete(:with)) : [self]
+ contexts.each {|c| c.calls(:#{name}) << Call.new(name, options, &block) }
+ end
+ code
end
+ alias :expect :before
+ alias :it :assertion
def compile(target)
leafs.each { |leaf| define_test_method(target, leaf) }
end
-
+
protected
- def find_action
- @action || parent && parent.find_action
- end
-
- def collect_preconditions
- (parent ? parent.collect_preconditions : []) + @preconditions
- end
-
- def collect_assertions
- (parent ? parent.collect_assertions : []) + @assertions
+ def method_missing(method_name, *args, &block)
+ options = {}
+ if args.last.is_a?(Hash) and [:in, :not_in].select{|key| args.last.has_key?(key) }
+ [:with, :in, :not_in].each { |key| options[key] = args.last.delete(key) }
+ args.pop if args.last.empty?
+ end
+ assertion ([method_name] << args.map(&:inspect)).join('_'), options do
+ send method_name, *args, &block
+ end
end
def define_test_method(target, context)
- action, preconditions, assertions = *context.calls
- method_name = generate_test_method_name(context, assertions)
-
+ method_name = generate_test_method_name(context)
target.send :define_method, method_name, &lambda {
- preconditions.map { |precondition| instance_eval &precondition }
- instance_eval &action if action
- assertions.map { |assertion| instance_eval &assertion }
+ @_with_contexts = (context.parents << context).map(&:name)
+ [:before, :action, :assertion, :after].each do |stage|
+ @_with_current_stage = stage
+ context.collect(stage).map { |call| instance_eval &call }
+ end
}
end
- # TODO urghs.
- def generate_test_method_name(context, assertions)
+ # TODO urghs. super ugly and doesn't even really work well.
+ # Need some better ideas for generating readable method names.
+ # Maybe even play with method names containing \n characters?
+ def generate_test_method_name(context)
contexts = context.parents << context
+ assertions = context.calls(:assertion)
+
name = "test_#{context.object_id}_#{contexts.shift.name}_"
name += contexts.map { |c| "with_#{c.name}_" }.join('and_')
name += assertions.map { |a| "it_#{a.name}_" }.join('and_')
- name.gsub(/ /, '_').gsub('it_it_', 'it_').gsub('__', '_').gsub(/_$/, '')
+ name.gsub(/\W/, '_').gsub('it_it_', 'it_').gsub('__', '_').gsub('__', '_').gsub(/"|(_$)/, '')
end
end
end
View
@@ -1,97 +0,0 @@
-module With
- class Group
- include Sharing
-
- attr_accessor :parent
- attr_reader :names
-
- def initialize(*names, &block)
- @names = names
- instance_eval &block if block
- end
-
- def with(*names, &block)
- add_child(*names, &block)
- end
-
- def assertion(name = nil, options = {}, &block)
- group = options[:with] ? with(*options[:with]) : self
- group.assertions << NamedBlock.new(name, &block)
- end
- alias :it :assertion
-
- def before(name = nil, &block)
- name ||= "before #{block.inspect}"
- preconditions << NamedBlock.new(name, &block)
- end
-
- def action(name = nil, &block)
- name ||= "action #{block.inspect}"
- @action = NamedBlock.new(name, &block)
- end
-
- def children
- @children ||= []
- end
-
- def preconditions
- @preconditions ||= []
- end
-
- def assertions
- @assertions ||= []
- end
-
- def compile(target)
- expand.first.compile(target)
- end
-
- protected
-
- def expand
- contexts = use_shared? ? shared_contexts : [to_context]
- contexts.each do |context|
- context.append_children children.map{|c| c.expand }.flatten
- end
- end
-
- def use_shared?
- names.first.is_a?(Symbol)
- end
-
- def shared_group(name)
- shared[name] || parent && parent.shared_group(name) or raise "could not find shared context #{name.inspect}"
- end
-
- def shared_contexts
- names.map do |name|
- shared_group(name).map do |group|
- group.to_context(@action, preconditions, assertions)
- end
- end.flatten
- end
-
- def to_context(action = nil, preconditions = [], assertions = [])
- action ||= @action
- # raise if there's more than one name?
- # or maybe better have separate attributes for name and shared_names?
- Context.new(names.first, action, self.preconditions + preconditions, self.assertions + assertions)
- end
-
- def add_child(*names, &block)
- child = Group.new(*names, &block)
- child.parent = self
- children << child
- child
- end
-
- def method_missing(method_name, *args, &block)
- description = method_name
- description = "#{description}_#{args.map(&:inspect).join('_')}".to_sym unless args.empty?
-
- assertion description do
- send method_name, *args, &block
- end
- end
- end
-end
Oops, something went wrong.

0 comments on commit 1b2a28e

Please sign in to comment.