funktional 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. data/MIT-LICENSE +20 -0
  2. data/Manifest +60 -0
  3. data/README.rdoc +292 -0
  4. data/Rakefile +36 -0
  5. data/funktional.gemspec +31 -0
  6. data/lib/funktional/assertion.rb +9 -0
  7. data/lib/funktional/assigned_assertion.rb +43 -0
  8. data/lib/funktional/attribute_test_helper.rb +27 -0
  9. data/lib/funktional/context/assigned_should_block.rb +32 -0
  10. data/lib/funktional/context/collector.rb +21 -0
  11. data/lib/funktional/context/count_should_block.rb +21 -0
  12. data/lib/funktional/context/delegating_should_block.rb +30 -0
  13. data/lib/funktional/context/element_should_block.rb +29 -0
  14. data/lib/funktional/context/flashed_should_block.rb +17 -0
  15. data/lib/funktional/context/should_block.rb +60 -0
  16. data/lib/funktional/context/should_create_block.rb +17 -0
  17. data/lib/funktional/context/should_delete_block.rb +17 -0
  18. data/lib/funktional/context/should_not_block.rb +20 -0
  19. data/lib/funktional/context/should_not_create_block.rb +17 -0
  20. data/lib/funktional/context/should_not_delete_block.rb +17 -0
  21. data/lib/funktional/context/should_not_send_email_block.rb +18 -0
  22. data/lib/funktional/context/stack_recorder.rb +35 -0
  23. data/lib/funktional/context.rb +126 -0
  24. data/lib/funktional/email_assertion.rb +50 -0
  25. data/lib/funktional/flashed_assertion.rb +12 -0
  26. data/lib/funktional/model_assertions.rb +108 -0
  27. data/lib/funktional/random_characters.rb +11 -0
  28. data/lib/funktional/recursive_assertion.rb +23 -0
  29. data/lib/funktional/route_checker.rb +49 -0
  30. data/lib/funktional/setup.rb +11 -0
  31. data/lib/funktional/test_class_methods.rb +28 -0
  32. data/lib/funktional/test_instance_methods.rb +131 -0
  33. data/lib/funktional.rb +42 -0
  34. data/tasks/should_b_tasks.rake +4 -0
  35. data/test/fixtures/posts.yml +4 -0
  36. data/test/fixtures/users.yml +9 -0
  37. data/test/functional/users_controller_test.rb +59 -0
  38. data/test/test-app/app/controllers/application_controller.rb +10 -0
  39. data/test/test-app/app/controllers/users_controller.rb +33 -0
  40. data/test/test-app/app/helpers/application_helper.rb +3 -0
  41. data/test/test-app/app/models/post.rb +3 -0
  42. data/test/test-app/app/models/user.rb +7 -0
  43. data/test/test-app/app/views/users/edit.html.erb +0 -0
  44. data/test/test-app/app/views/users/index.html.erb +0 -0
  45. data/test/test-app/app/views/users/new.html.erb +0 -0
  46. data/test/test-app/config/boot.rb +110 -0
  47. data/test/test-app/config/database.yml +4 -0
  48. data/test/test-app/config/environment.rb +12 -0
  49. data/test/test-app/config/environments/test.rb +0 -0
  50. data/test/test-app/config/initializers/funktional.rb +8 -0
  51. data/test/test-app/config/initializers/new_rails_defaults.rb +21 -0
  52. data/test/test-app/config/routes.rb +5 -0
  53. data/test/test-app/db/migrate/001_create_users.rb +18 -0
  54. data/test/test-app/db/migrate/002_create_posts.rb +13 -0
  55. data/test/test-app/public/404.html +30 -0
  56. data/test/test-app/public/422.html +30 -0
  57. data/test/test-app/public/500.html +30 -0
  58. data/test/test-app/script/console +3 -0
  59. data/test/test-app/script/generate +3 -0
  60. data/test/test_helper.rb +26 -0
  61. data/test/unit/user_test.rb +12 -0
  62. metadata +159 -0
@@ -0,0 +1,60 @@
1
+ module Funktional
2
+ class ShouldBlock
3
+
4
+ def self.build(options, context, &blk)
5
+ return self.new(name = options, context, &blk) if options.is_a? String
6
+
7
+ options = {:render_404 => 'public/404'} if options == :render_404
8
+
9
+ case options.keys.first
10
+ when :create
11
+ ShouldCreateBlock.new(options[:create], context)
12
+ when :delete
13
+ ShouldDeleteBlock.new(options[:delete], context)
14
+ else
15
+ DelegatingShouldBlock.new(options, context, &blk)
16
+ end
17
+ end
18
+
19
+ def initialize(should_name, context, &blk)
20
+ raise 'block required' unless block_given?
21
+ @should_name, @blk, @context = should_name, blk, context
22
+ end
23
+
24
+ def to_test
25
+ context, blk, before = @context, @blk, @before
26
+
27
+ @context.test_unit_class.send(:define_method, build_test_name) do
28
+ begin
29
+ context.run_parent_setup_blocks(self)
30
+ before.bind(self).call if before
31
+
32
+ context.run_current_setup_blocks(self)
33
+ blk.bind(self).call
34
+ ensure
35
+ context.run_all_teardown_blocks(self)
36
+ end
37
+ end
38
+ end
39
+
40
+ protected
41
+
42
+ def test_and_line_no
43
+ stack_frame = caller.grep(/bind/).last
44
+ stack_frame.split(':in ').first
45
+ end
46
+
47
+ def build_test_name
48
+ test_name = test_name_parts.flatten.join(' ')
49
+
50
+ if @context.test_unit_class.instance_methods.include?(test_name)
51
+ warn " * WARNING: '#{test_name}' is already defined"
52
+ end
53
+ return test_name.to_sym
54
+ end
55
+
56
+ def test_name_parts
57
+ ["test:", @context.full_name, "should", "#{@should_name}."]
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,17 @@
1
+ module Funktional
2
+ class ShouldCreateBlock < ShouldBlock
3
+
4
+ def initialize(klass, context)
5
+ @context = context
6
+ @should_name = "create a #{klass}"
7
+
8
+ @before = lambda { @before_setup_count = klass.count }
9
+ where = test_and_line_no
10
+
11
+ @blk = lambda do
12
+ expected_count = @before_setup_count + 1
13
+ assert_equal expected_count, klass.count, "New [#{klass}] was not created. #{where}"
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,17 @@
1
+ module Funktional
2
+ class ShouldDeleteBlock < ShouldBlock
3
+
4
+ def initialize(klass, context)
5
+ @context = context
6
+ @should_name = "delete a #{klass}"
7
+
8
+ @before = lambda { @before_setup_count = klass.count }
9
+ where = test_and_line_no
10
+
11
+ @blk = lambda do
12
+ expected_count = @before_setup_count - 1
13
+ assert_equal expected_count, klass.count, "New [#{klass}] was not deleted. #{where}"
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,20 @@
1
+ module Funktional
2
+ class ShouldNotBlock
3
+
4
+ def self.build(options, context, &blk)
5
+
6
+ if options.is_a? Symbol and options.eql? :send_email
7
+ return ShouldNotSendEmailBlock.new(context)
8
+ end
9
+
10
+ case options.keys.first
11
+ when :create
12
+ ShouldNotCreateBlock.new(options[:create], context)
13
+ when :delete
14
+ ShouldNotDeleteBlock.new(options[:delete], context)
15
+ else
16
+ raise "Unknown assertion [should_not #{options.inspect}]"
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,17 @@
1
+ module Funktional
2
+ class ShouldNotCreateBlock < ShouldBlock
3
+
4
+ def initialize(klass, context)
5
+ @context = context
6
+ @should_name = "not create a #{klass}"
7
+
8
+ @before = lambda { @before_setup_count = klass.count }
9
+ where = test_and_line_no
10
+
11
+ @blk = lambda do
12
+ expected_count = @before_setup_count
13
+ assert_equal expected_count, klass.count, "New [#{klass}] was created. #{where}"
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,17 @@
1
+ module Funktional
2
+ class ShouldNotDeleteBlock < ShouldBlock
3
+
4
+ def initialize(klass, context)
5
+ @context = context
6
+ @should_name = "not delete a #{klass}"
7
+
8
+ @before = lambda { @before_setup_count = klass.count }
9
+ where = test_and_line_no
10
+
11
+ @blk = lambda do
12
+ expected_count = @before_setup_count
13
+ assert_equal expected_count, klass.count, "New [#{klass}] was deleted. #{where}"
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,18 @@
1
+ module Funktional
2
+ class ShouldNotSendEmailBlock < ShouldBlock
3
+
4
+ def initialize(context)
5
+ @context = context
6
+ @should_name = "not send an email"
7
+
8
+ @before = lambda { @before_setup_count = ActionMailer::Base.deliveries.size }
9
+ where = test_and_line_no
10
+
11
+ @blk = lambda do
12
+ expected_count = @before_setup_count
13
+ current = ActionMailer::Base.deliveries.size
14
+ assert_equal expected_count, current, "New [Email] was sent."
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,35 @@
1
+ module Funktional
2
+ class BlankSlate
3
+ instance_methods.each { |m| undef_method m unless m =~ /^__/ }
4
+ end
5
+
6
+ class StackRecorder < BlankSlate
7
+ attr_reader :__target
8
+
9
+ def initialize(target)
10
+ @__target = target
11
+ @__methods = []
12
+ end
13
+
14
+ def __meth(index)
15
+ @__methods[index]
16
+ end
17
+
18
+ def __each_called
19
+ @__methods.each { |meth| yield [meth[:name], meth[:args], meth[:block]] }
20
+ end
21
+
22
+ def __last_value
23
+ @__methods.last[:args].first
24
+ end
25
+
26
+ def method_missing(method_name, *args, &block)
27
+ @__methods << {
28
+ :name => method_name,
29
+ :args => args,
30
+ :block => block
31
+ }
32
+ return self
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,126 @@
1
+ # borrowing heavily from shoulda contexts.
2
+
3
+ module Funktional
4
+ class Context
5
+
6
+ attr_accessor :name
7
+ attr_accessor :parent
8
+ attr_accessor :sub_contexts
9
+ attr_accessor :setup_blocks
10
+ attr_accessor :teardown_blocks
11
+ attr_accessor :should_blocks
12
+
13
+ def initialize(name, parent, &blk)
14
+ Funktional.add_context(self)
15
+ self.name = name
16
+ self.parent = parent
17
+ self.setup_blocks = []
18
+ self.teardown_blocks = []
19
+ self.should_blocks = []
20
+ self.sub_contexts = []
21
+
22
+ merge_block(&blk)
23
+ Funktional.remove_context
24
+ end
25
+
26
+ def merge_block(&blk)
27
+ blk.bind(self).call
28
+ end
29
+
30
+ def context(name, &blk)
31
+ self.sub_contexts << Context.new(name, self, &blk)
32
+ end
33
+
34
+ def setup(&blk)
35
+ self.setup_blocks << blk
36
+ end
37
+
38
+ alias :before :setup
39
+
40
+ def teardown(&blk)
41
+ self.teardown_blocks << blk
42
+ end
43
+
44
+ alias :after :teardown
45
+
46
+ def should(options, &blk)
47
+ self.should_blocks << ShouldBlock.build(options, self, &blk)
48
+ end
49
+
50
+ def should_not(options, &blk)
51
+ self.should_blocks << ShouldNotBlock.build(options, self, &blk)
52
+ end
53
+
54
+ def element(selector)
55
+ recorder = StackRecorder.new(selector)
56
+ self.should_blocks << ElementShouldBlock.new(recorder, self)
57
+
58
+ return recorder
59
+ end
60
+
61
+ def count(selector)
62
+ recorder = StackRecorder.new(selector)
63
+ self.should_blocks << CountShouldBlock.new(recorder, self)
64
+
65
+ return recorder
66
+ end
67
+
68
+ def assigned(target)
69
+ recorder = StackRecorder.new(target)
70
+ self.should_blocks << AssignedShouldBlock.new(recorder, self)
71
+
72
+ return recorder
73
+ end
74
+
75
+ def flashed(kind)
76
+ recorder = StackRecorder.new(kind)
77
+ self.should_blocks << FlashedShouldBlock.new(recorder, self)
78
+
79
+ return recorder
80
+ end
81
+
82
+ def full_name
83
+ parent_name = parent.full_name if sub_context?
84
+ return [parent_name, name].join(" ").strip
85
+ end
86
+
87
+ def sub_context?
88
+ parent.is_a?(self.class)
89
+ end
90
+
91
+ def test_unit_class
92
+ sub_context? ? parent.test_unit_class : parent
93
+ end
94
+
95
+ def run_all_setup_blocks(binding)
96
+ run_parent_setup_blocks(binding)
97
+ run_current_setup_blocks(binding)
98
+ end
99
+
100
+ def run_parent_setup_blocks(binding)
101
+ self.parent.run_all_setup_blocks(binding) if sub_context?
102
+ end
103
+
104
+ def run_current_setup_blocks(binding)
105
+ setup_blocks.each do |setup_block|
106
+ setup_block.bind(binding).call
107
+ end
108
+ end
109
+
110
+ def run_all_teardown_blocks(binding)
111
+ teardown_blocks.reverse.each do |teardown_block|
112
+ teardown_block.bind(binding).call
113
+ end
114
+ self.parent.run_all_teardown_blocks(binding) if sub_context?
115
+ end
116
+
117
+ def build
118
+ self.should_blocks.map(&:to_test)
119
+ sub_contexts.each { |context| context.build }
120
+ end
121
+
122
+ def method_missing(method, *args, &blk)
123
+ test_unit_class.send(method, *args, &blk)
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,50 @@
1
+ module Funktional
2
+ class EmailAssertion < Funktional::Assertion
3
+ def initialize(expectations)
4
+ if ActionMailer::Base.deliveries.size < 1
5
+ flunk 'No emails have been sent'
6
+ end
7
+
8
+ email = ActionMailer::Base.deliveries.last
9
+
10
+ expectations.each_key do |key|
11
+ case key
12
+ when :from
13
+ then check_from(expectations[:from], email.from)
14
+ when :to
15
+ then check_to(expectations[:to], email.to)
16
+ when :subject
17
+ then assert_equal expectations[:subject], email.subject
18
+ when :containing
19
+ then check_containing(expectations[:containing], email.body)
20
+ else
21
+ flunk "Assertion key: [#{key}] not recognised"
22
+ end
23
+ end
24
+ end
25
+
26
+ def check_from(expected_from, email_from)
27
+ if email_from.nil?
28
+ flunk 'email is missing a [from]'
29
+ end
30
+ assert_equal expected_from, email_from[0]
31
+ end
32
+
33
+ def check_to(expected_to, email_to)
34
+ if email_to.nil?
35
+ flunk 'email is missing a [to]'
36
+ end
37
+ assert_equal expected_to, email_to[0]
38
+ end
39
+
40
+ def check_containing(should_contain, body)
41
+ if should_contain.is_a? Array
42
+ should_contain.each do |should_i|
43
+ assert_match /#{Regexp.escape(should_i)}/, body
44
+ end
45
+ else
46
+ assert_match /#{Regexp.escape(should_contain)}/, body
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,12 @@
1
+ module Funktional
2
+ class FlashedAssertion < Funktional::Assertion
3
+ def initialize(symbol)
4
+ @symbol = symbol
5
+ end
6
+
7
+ def should_be(expected_value)
8
+ assert_not_nil flash[@symbol], "Flash is empty [#{expected_value}] expected."
9
+ assert_equal expected_value, flash[@symbol]
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,108 @@
1
+ module Funktional
2
+ module ModelAssertions
3
+ def should_require_a(field, expected_error_message = nil)
4
+ check_test_instance!
5
+ must_be_valid_before_test!
6
+
7
+ assign_nil_to(field, expected_error_message) do
8
+ assert_invalid("'#{self.class.name}' should require a '#{field}'")
9
+ end
10
+ end
11
+
12
+ alias :should_require_an :should_require_a
13
+
14
+ def should_not_require_a(field)
15
+ check_test_instance!
16
+
17
+ assign_nil_to(field) do
18
+ assert_valid
19
+ end
20
+ end
21
+
22
+ alias :should_not_require_an :should_not_require_a
23
+
24
+ def should_have_invalid(field, expected_error_message = nil)
25
+ check_test_instance!
26
+
27
+ assert_invalid("'#{self.class.name}' should have invalid '#{field}'")
28
+ assert_message(field, expected_error_message) if expected_error_message
29
+ end
30
+
31
+ def should_mass_assign(*fields)
32
+ check_test_instance!
33
+ fields.each do |field|
34
+ Funktional.test_instance.assert self.class.accessible_attributes.include?(field.to_s)
35
+ end
36
+ end
37
+
38
+ def should_not_mass_assign(*fields)
39
+ check_test_instance!
40
+ accessible_attrib = self.class.accessible_attributes.to_a
41
+ protected_attrib = self.class.protected_attributes.to_a
42
+ fields = fields.map(&:to_s)
43
+
44
+ if accessible_attrib.any?
45
+ common_fields = (accessible_attrib & fields)
46
+ @test.assert((common_fields.size == 0), "#{common_fields.inspect} marked accessible")
47
+ elsif protected_attrib.any?
48
+ common_fields = (protected_attrib & fields)
49
+ difference = (fields - protected_attrib)
50
+ @test.assert((common_fields.size == fields.size), "#{difference.inspect} not protected")
51
+ else
52
+ @test.flunk "#{fields.inspect} not protected"
53
+ end
54
+ end
55
+
56
+ def should_protect(*fields)
57
+ check_test_instance!
58
+ fields.each do |field|
59
+ Funktional.test_instance.assert self.class.protected_attributes.include?(field.to_s)
60
+ end
61
+ end
62
+
63
+ def should_respond_to(method)
64
+ Funktional.test_instance.assert_respond_to self, method
65
+ end
66
+
67
+ private
68
+
69
+ def assign_nil_to(field, expected_error_message = nil)
70
+ saved_existing_value = self.send field
71
+ self.send "#{field}=", nil
72
+
73
+ yield
74
+
75
+ assert_message(field, expected_error_message) if expected_error_message
76
+ self.send "#{field}=", saved_existing_value
77
+
78
+ # clear errors
79
+ self.valid?
80
+ end
81
+
82
+ def assert_invalid(message)
83
+ Funktional.test_instance.assert self.invalid?, message
84
+ end
85
+
86
+ def assert_valid
87
+ Funktional.test_instance.assert self.valid?
88
+ end
89
+
90
+ def assert_message(field, expected_error_message)
91
+ Funktional.test_instance.assert_equal(expected_error_message, self.errors[field])
92
+ end
93
+
94
+ def must_be_valid_before_test!
95
+ if self.invalid?
96
+ error = "Funktional requires a valid instance of #{self.class.name}"
97
+ Funktional.test_instance.flunk error
98
+ end
99
+ end
100
+
101
+ def check_test_instance!
102
+ if Funktional.test_instance.nil?
103
+ raise Funktional::Setup::Error, 'Did you forget to (setup :funktional) in test/test_helper?'
104
+ end
105
+ @test = Funktional.test_instance
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,11 @@
1
+ module Funktional
2
+ module RandomCharacters
3
+ def random_characters
4
+ chars = 'abcdefghjkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789'
5
+ str = ''
6
+ self.times { str << chars[rand(chars.size)] }
7
+
8
+ return str
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,23 @@
1
+ module Funktional
2
+ class RecursiveAssertion < Funktional::Assertion
3
+ def initialize(assigned, method)
4
+ @test = Funktional.test_instance
5
+ @assigned = assigned
6
+ @method = method
7
+
8
+ @test.assert_respond_to @assigned, @method
9
+ end
10
+
11
+ def should_be(expected_value)
12
+ value = @assigned.send @method
13
+ @test.assert_equal expected_value, value
14
+ end
15
+
16
+ protected
17
+
18
+ def method_missing(method, *args)
19
+ @assigned = @assigned.send @method
20
+ RecursiveAssertion.new(@assigned, method)
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,49 @@
1
+ module Funktional
2
+
3
+ class RouteChecker
4
+ instance_methods.each do |method|
5
+ undef_method(method) unless %w(__send__ __id__ instance_eval).include?(method)
6
+ end
7
+
8
+ attr_reader :__path_and_method, :__controller_action_etc
9
+
10
+ def self.build(params, &blk)
11
+ checker = self.new(params)
12
+
13
+ checker.instance_eval(&blk)
14
+ return checker
15
+ end
16
+
17
+ def initialize(params)
18
+ @__path_and_method = __get_path_and_method(params)
19
+ @__controller_action_etc = {}
20
+ end
21
+
22
+ def __test_name
23
+ path = @__path_and_method[:path]
24
+ method = @__path_and_method[:method]
25
+
26
+ "route '#{path}' :method '#{method}' to #{@__controller_action_etc.inspect}"
27
+ end
28
+
29
+ def method_missing(method_name, value)
30
+ @__controller_action_etc[method_name] = value
31
+ end
32
+
33
+ private
34
+
35
+ def __get_path_and_method(params)
36
+ if params.is_a? Hash
37
+ {
38
+ :path => params[:route],
39
+ :method => params[:method].to_sym
40
+ }
41
+ else
42
+ {
43
+ :path => params,
44
+ :method => :get
45
+ }
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,11 @@
1
+ module Funktional
2
+ mattr_accessor :test_instance
3
+
4
+ module Setup
5
+ def funktional
6
+ Funktional.test_instance = self
7
+ end
8
+
9
+ class Error < RuntimeError; end
10
+ end
11
+ end
@@ -0,0 +1,28 @@
1
+ module Funktional
2
+
3
+ module TestClassMethods
4
+ def context(name, &blk)
5
+ if Funktional.current_context
6
+ Funktional.current_context.context(name, &blk)
7
+ else
8
+ context = Funktional::Context.new(name, self, &blk)
9
+ context.build
10
+ end
11
+ end
12
+
13
+ def should(name, &blk)
14
+ if Funktional.current_context
15
+ raise 'block required' unless block_given?
16
+ Funktional.current_context.should(name, &blk)
17
+ else
18
+ context_name = self.name.gsub(/Test/, "")
19
+
20
+ context = Funktional::Context.new(context_name, self) do
21
+ raise 'block required' unless block_given?
22
+ should(name, &blk)
23
+ end
24
+ context.build
25
+ end
26
+ end
27
+ end
28
+ end