effective_test_bot 0.4.1 → 0.4.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: e103116aeed8578d3cb62239ab87759208152860
4
- data.tar.gz: a4978f5794f46d577111f8bbfbfbf32c0aafa9d6
3
+ metadata.gz: faf3170be69a804110713bd4fffd0d2802841fa2
4
+ data.tar.gz: 3ac29840a0b524fad45cded4d7d81c3bcb56bee4
5
5
  SHA512:
6
- metadata.gz: a18ae0e701b4e5abcd1275070c9347c10116b626896c6b59edcb7bfe2dcdfd987e7cf17a1fad08b8f6879b4de3eaa0182ccd81603d8f7b22e4108f84d56f0d92
7
- data.tar.gz: c764261c4bbfbef91ac98864877dbae1f2e549b24b3cc01ed215a934c14cc3c68891fa821ea6367ddd5ef89e8af6d1cfa242b8f742b2fccf5ad300d6b6e0a7aa
6
+ metadata.gz: e1bc8fe65d07f530b193e9498e5d3537944d09903727f0407cc7c93bccfeed52addaa1383a56faea2bc005be2429b1e5d856da74da5bc7209e5949425dfb3d7c
7
+ data.tar.gz: 4174f5a21a7cfa52081df5d86de979b48cfe1a44fe167a5974b84653cb64f4bcd24555d0787b269336f33540376684bafc566937c46df7186e91b6a6c48c607e
@@ -1,4 +1,5 @@
1
1
  module EffectiveTestBotControllerHelper
2
+ # This is included as an after_filter
2
3
  def assign_test_bot_http_headers
3
4
  response.headers['Test-Bot-Flash'] = Base64.encode64(flash.to_hash.to_json)
4
5
 
@@ -2,8 +2,69 @@ require "effective_test_bot/engine"
2
2
  require "effective_test_bot/version"
3
3
 
4
4
  module EffectiveTestBot
5
+ mattr_accessor :except
6
+ mattr_accessor :only
7
+
5
8
  def self.setup
6
9
  yield self
7
10
  end
8
11
 
12
+ def self.skip?(test, assertion = nil)
13
+ value = [test.to_s.presence, assertion.to_s.presence].compact.join(' ')
14
+ return false if value.blank?
15
+
16
+ if onlies.present?
17
+ onlies.find { |only| value.start_with?(only) }.blank? # Let partial matches work
18
+ elsif excepts.present?
19
+ excepts.find { |except| except == value }.present?
20
+ else
21
+ false
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ def self.onlies
28
+ @@onlines ||= begin
29
+ flatten_and_sort(
30
+ if ENV['TEST_BOT_TEST'].present?
31
+ ENV['TEST_BOT_TEST'].to_s.gsub('[', '').gsub(']', '').split(',').map { |str| str.strip }
32
+ else
33
+ only
34
+ end
35
+ )
36
+ end
37
+ end
38
+
39
+ def self.excepts
40
+ @@excepts ||= flatten_and_sort(except)
41
+ end
42
+
43
+ # config.except = [
44
+ # 'assert_path',
45
+ # 'users#show',
46
+ # 'users#create_invalid' => ['assert_path'],
47
+ # 'users#create_invalid' => 'assert_unpermitted_params',
48
+ # 'report_total_allocation_index_path'
49
+ # ]
50
+
51
+ # We need to flatten any Hashes into
52
+ # 'users#create_invalid' => ['assert_path', 'assert_page_title'],
53
+ # into this
54
+ # ['users#create_invalid assert_path'
55
+ # 'users#create_invalid assert_page_title']
56
+
57
+ def self.flatten_and_sort(skips)
58
+ Array(skips).flat_map do |skip|
59
+ case skip
60
+ when Symbol
61
+ skip.to_s
62
+ when Hash
63
+ skip.keys.product(skip.values.flatten).map { |p| p.join(' ') }
64
+ else
65
+ skip
66
+ end
67
+ end.compact.sort
68
+ end
69
+
9
70
  end
@@ -14,10 +14,23 @@ module EffectiveTestBot
14
14
 
15
15
  initializer 'effective_test_bot.test_suite' do |app|
16
16
  Rails.application.config.to_prepare do
17
+ # test/test_botable/
18
+ ActionDispatch::IntegrationTest.include BaseTest
17
19
  ActionDispatch::IntegrationTest.include CrudTest
18
- ActionDispatch::IntegrationTest.include TestBotable::CrudTest
19
-
20
- # A whole bunch of helper methods
20
+ ActionDispatch::IntegrationTest.include MemberTest
21
+ ActionDispatch::IntegrationTest.include PageTest
22
+ ActionDispatch::IntegrationTest.include RedirectTest
23
+ ActionDispatch::IntegrationTest.include WizardTest
24
+
25
+ # test/concerns/test_botable/
26
+ ActionDispatch::IntegrationTest.include TestBotable::BaseDsl
27
+ ActionDispatch::IntegrationTest.include TestBotable::CrudDsl
28
+ ActionDispatch::IntegrationTest.include TestBotable::MemberDsl
29
+ ActionDispatch::IntegrationTest.include TestBotable::PageDsl
30
+ ActionDispatch::IntegrationTest.include TestBotable::RedirectDsl
31
+ ActionDispatch::IntegrationTest.include TestBotable::WizardDsl
32
+
33
+ # test/support/
21
34
  ActionDispatch::IntegrationTest.include EffectiveTestBotAssertions
22
35
  ActionDispatch::IntegrationTest.include EffectiveTestBotFormHelper
23
36
  ActionDispatch::IntegrationTest.include EffectiveTestBotLoginHelper
@@ -29,6 +42,8 @@ module EffectiveTestBot
29
42
  ActiveSupport.on_load :action_controller do
30
43
  if Rails.env.test?
31
44
  ActionController::Base.send :include, ::EffectiveTestBotControllerHelper
45
+
46
+ ActionController::Base.send :before_filter, :expires_now # Prevent 304 Not Modified caching
32
47
  ActionController::Base.send :after_filter, :assign_test_bot_http_headers
33
48
 
34
49
  ApplicationController.instance_exec do
@@ -1,3 +1,3 @@
1
1
  module EffectiveTestBot
2
- VERSION = '0.4.1'.freeze
2
+ VERSION = '0.4.2'.freeze
3
3
  end
@@ -34,7 +34,7 @@ class ActionDispatch::IntegrationTest
34
34
  # end
35
35
 
36
36
  def after_teardown # I reset sessions here so capybara-screenshot can still make screenshots when tests fail
37
- super() and Capybara.reset_sessions!
37
+ super(); Capybara.reset_sessions!
38
38
  end
39
39
 
40
40
  end
@@ -1,9 +1,19 @@
1
1
  require 'rake/testtask'
2
2
  require 'rails/test_unit/sub_test_task'
3
3
 
4
+ # rake test:bot
5
+ # rake test:bot TEST=documents#new
6
+ # rake test:bot TEST=documents#new,documents#show
7
+ # rake test:bot TEST=documents#new path,documents#show,documents#update_valid no_unpermitted_params
8
+
4
9
  namespace :test do
5
10
  desc 'Runs Effective Test Bot'
6
11
  task :bot do
12
+ if ENV['TEST'].present?
13
+ ENV['TEST_BOT_TEST'] = ENV['TEST']
14
+ ENV['TEST'] = nil
15
+ end
16
+
7
17
  Rake::Task["test:effective_test_bot"].invoke
8
18
  end
9
19
 
@@ -18,5 +28,4 @@ namespace :test do
18
28
  load(seeds) if File.exists?(seeds)
19
29
  end
20
30
 
21
-
22
31
  end
@@ -0,0 +1,142 @@
1
+ module TestBotable
2
+ module BaseDsl
3
+ extend ActiveSupport::Concern
4
+
5
+ module ClassMethods
6
+ TEST_BOT_TEST_PREFIXES = ['crud_test', 'member_test', 'page_test', 'redirect_test', 'wizard_test']
7
+
8
+ # Parses and validates lots of options
9
+ # This is a big manual merge wherein we translate some DSL methods into one consistent Hash here
10
+ # The output is what gets sent to each test and defined as lets
11
+
12
+ # {
13
+ # user: User<1> instance
14
+ # controller_namespace: 'admin'
15
+ # controller: 'jobs'
16
+ # skips: []
17
+ # resource: Post<1> instance
18
+ # resource_class: Post
19
+ # resource_name: 'post'
20
+ # resource_attributes: {:user_id => 3} attributes, if called with an instance of ActiveRecord with non-default attributes
21
+ # current_test: posts#new (if from the class level) or NIL if from the instance level
22
+ # }
23
+ #
24
+
25
+ def normalize_test_bot_options!(options)
26
+ raise 'expected options to be a Hash' unless options.kind_of?(Hash)
27
+ raise 'expected key :user to be a User' unless options[:user].kind_of?(User)
28
+ #raise 'expected key :current_test to be a String' unless options[:current_test].kind_of?(String)
29
+
30
+ # Controller stuff
31
+ options[:controller_namespace] ||= options[:namespace]
32
+
33
+ # Skip stuff
34
+ skips = options[:skip] || options[:skips]
35
+ unless skips.blank? || skips.kind_of?(Symbol) || (skips.kind_of?(Array) && skips.all? { |s| s.kind_of?(Symbol) })
36
+ raise 'expected skips to be a Symbol or Array of Symbols'
37
+ end
38
+ options[:skips] = Array(skips)
39
+
40
+ # Resource could be an ActiveRecord Class, Instance of a class, or String
41
+ # Build a resource stuff
42
+ if options[:resource].present?
43
+ obj = options[:resource]
44
+ raise 'expected resource to be a Class or Instance or String' unless obj.kind_of?(Class) || obj.kind_of?(ActiveRecord::Base) || obj.kind_of?(String)
45
+
46
+ if obj.kind_of?(String) # Let's assume this is a controller, 'admin/jobs', or just 'jobs', or 'jobs_controller'
47
+ obj.sub!('_controller', '')
48
+
49
+ (*namespace, klass) = obj.split('/')
50
+ namespace = Array(namespace).join('/').presence
51
+
52
+ # See if I can turn it into a model
53
+ klass = klass.classify.safe_constantize
54
+
55
+ raise "failed to constantize resource from string #{obj}, unable to proceed" unless klass.present?
56
+
57
+ options[:controller_namespace] ||= namespace
58
+ options[:controller] = obj.dup
59
+
60
+ obj = klass
61
+ end
62
+
63
+ # Make sure Obj.new() works
64
+ if obj.kind_of?(Class) && (obj.new() rescue false) == false
65
+ raise "failed to initialize resource with #{obj}.new(), unable to proceed"
66
+ end
67
+
68
+ # Parse the resource and resource class
69
+ resource = obj.kind_of?(Class) ? obj.new() : obj
70
+ resource_class = obj.kind_of?(Class) ? obj : obj.class
71
+
72
+ # If obj is an ActiveRecord object with attributes, Post.new(:title => 'My Title')
73
+ # then compute any explicit attributes, so forms will be filled with those values
74
+ resource_attributes = if obj.kind_of?(ActiveRecord::Base)
75
+ empty = resource_class.new()
76
+ {}.tap { |atts| resource.attributes.each { |k, v| atts[k] = v if empty.attributes[k] != v } }
77
+ end || {}
78
+
79
+ options[:resource] = resource
80
+ options[:resource_class] = resource_class
81
+ options[:resource_name] = resource_class.name.underscore
82
+ options[:resource_attributes] = resource_attributes
83
+
84
+ options[:normalized] = true
85
+ end
86
+
87
+ options
88
+ end
89
+
90
+ # Run any test_bot tests first, in the order they're defined
91
+ # then the rest of the tests with whatever order they come in
92
+ def runnable_methods
93
+ public_instance_methods.select do |name|
94
+ name = name.to_s
95
+ TEST_BOT_TEST_PREFIXES.any? { |prefix| name.starts_with?(prefix) }
96
+ end.map(&:to_s) + super
97
+ end
98
+
99
+ protected
100
+
101
+ # You can't define multiple methods with the same name
102
+ # So we need to create a unique name, where appropriate, that still looks good in MiniTest output
103
+ def test_bot_method_name(test_family, current_test)
104
+ number_of_tests = if current_test.blank?
105
+ @num_defined_test_bot_tests ||= {}
106
+ @num_defined_test_bot_tests[test_family] = (@num_defined_test_bot_tests[test_family] || 0) + 1
107
+ end
108
+
109
+ if current_test.present?
110
+ "#{test_family}: (#{current_test})"
111
+ elsif number_of_tests > 1
112
+ "#{test_family}: (#{number_of_tests})"
113
+ else
114
+ "#{test_family}:"
115
+ end
116
+ end
117
+ end
118
+
119
+ # Instance Methods
120
+
121
+
122
+ # Using reverse_merge! in the dsl action_tests makes sure that the
123
+ # class level can assign a current_test variable
124
+ # wheras the action level ones it's not present.
125
+ def assign_test_bot_lets!(options)
126
+ lets = if options.kind_of?(Hash) && options[:normalized]
127
+ options
128
+ else
129
+ self.class.normalize_test_bot_options!(options)
130
+ end
131
+
132
+ lets.each { |k, v| self.class.let(k) { v } } # Using the minitest spec let(:foo) { 'bar' } syntax
133
+
134
+ # test_bot may leak some lets from one test to the next, if they're not overridden
135
+ # I take special care to undefine just current test so far
136
+ self.class.let(:current_test) { nil } if options[:current_test].blank?
137
+
138
+ lets
139
+ end
140
+
141
+ end
142
+ end
@@ -0,0 +1,92 @@
1
+ # This DSL gives a class level and an instance level way of calling specific test suite
2
+ #
3
+ # class PostsTest < ActionDispatch::IntegrationTest
4
+ # crud_test(Post || 'admin/posts', User.first, except: :show, skip: {create_valid: :path, update_invalid: [:path, :flash]})
5
+ #
6
+ # test 'a one-off action' do
7
+ # crud_action_test(:new, Post, User.first, skip: :title)
8
+ # end
9
+ # end
10
+
11
+ module TestBotable
12
+ module CrudDsl
13
+ extend ActiveSupport::Concern
14
+
15
+ CRUD_TESTS = [:new, :create_valid, :create_invalid, :edit, :update_valid, :update_invalid, :index, :show, :destroy]
16
+
17
+ module ClassMethods
18
+
19
+ # All this does is define a 'test_bot' method for each required action on this class
20
+ # So that MiniTest will see the test functions and run them
21
+ def crud_test(resource, user, options = {})
22
+ # This skips paramaters is different than the initializer skips, which affect just the rake task
23
+
24
+ # These are specificially for the DSL
25
+ # In the class method, this value is a Hash, in the instance method it's expecting an Array
26
+ skips = options.delete(:skip) || options.delete(:skips) || {} # So you can skip sub tests
27
+ raise 'invalid skip syntax, expecting skip: {create_invalid: [:path]}' unless skips.kind_of?(Hash)
28
+
29
+ label = options.delete(:label).presence
30
+ only = options.delete(:only)
31
+ except = options.delete(:except)
32
+
33
+ begin
34
+ normalize_test_bot_options!(options.merge!(user: user, resource: resource))
35
+ rescue => e
36
+ raise "Error: #{e.message}. Expected usage: crud_test(Post || Post.new, User.first, only: [:new, :create], skip: {create_invalid: [:path]})"
37
+ end
38
+
39
+ crud_tests_to_define(only, except).each do |test|
40
+ options_for_method = options.dup
41
+
42
+ options_for_method[:skips] = Array(skips[test]) if skips[test]
43
+ options_for_method[:current_test] = [
44
+ options[:controller_namespace].presence,
45
+ options[:resource_name].pluralize
46
+ ].compact.join('/') + '#' + test.to_s
47
+
48
+ method_name = test_bot_method_name('crud_test', label || options_for_method[:current_test])
49
+
50
+ define_method(method_name) { crud_action_test(test, resource, user, options_for_method) }
51
+ end
52
+ end
53
+
54
+ private
55
+
56
+ # Parses the incoming options[:only] and [:except]
57
+ # To only define the appropriate methods
58
+ # This guarantees the functions will be defined in the same order as CRUD_TESTS
59
+ def crud_tests_to_define(only, except)
60
+ if only
61
+ only = Array(only).flatten.compact.map { |x| x.to_sym }
62
+ only = only + [:create_valid, :create_invalid] if only.delete(:create)
63
+ only = only + [:update_valid, :update_invalid] if only.delete(:update)
64
+
65
+ CRUD_TESTS & only
66
+ elsif except
67
+ except = Array(except).flatten.compact.map { |x| x.to_sym }
68
+ except = except + [:create_valid, :create_invalid] if except.delete(:create)
69
+ except = except + [:update_valid, :update_invalid] if except.delete(:update)
70
+
71
+ CRUD_TESTS - except
72
+ else
73
+ CRUD_TESTS
74
+ end
75
+ end
76
+ end
77
+
78
+ # Instance Methods - Call me from within a test
79
+ #
80
+ # If obj is a Hash {:resource => ...} just skip over parsing options
81
+ # And assume it's already been done (by the ClassMethod crud_test)
82
+ def crud_action_test(test, resource, user = nil, options = {})
83
+ begin
84
+ assign_test_bot_lets!(options.reverse_merge!(user: user, resource: resource))
85
+ rescue => e
86
+ raise "Error: #{e.message}. Expected usage: crud_action_test(:new, Post || Post.new, User.first, options_hash)"
87
+ end
88
+
89
+ self.send("test_bot_#{test}_test")
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,41 @@
1
+ # This DSL gives a class level and an instance level way of calling specific test suite
2
+ #
3
+ # class PostsTest < ActionDispatch::IntegrationTest
4
+ # member_test('admin/jobs', 'unarchive', User.first, Post.first)
5
+ #
6
+ # test 'a one-off action' do
7
+ # member_action_test('admin/jobs', 'unarchive', User.first)
8
+ # end
9
+ # end
10
+
11
+ # A member_test assumes assumes route.name.present? && route.verb.to_s.include?('GET') && route.path.required_names == ['id']
12
+ # we HAVE TO build or have available one of these resources so we can pass the ID to it and see what happens :)
13
+
14
+ module TestBotable
15
+ module MemberDsl
16
+ extend ActiveSupport::Concern
17
+
18
+ module ClassMethods
19
+
20
+ def member_test(controller, action, user, obj_to_param = nil, options = {})
21
+ options[:current_test] = "#{controller}##{action}"
22
+ method_name = test_bot_method_name('member_test', options.delete(:label) || options[:current_test])
23
+
24
+ define_method(method_name) { member_action_test(controller, action, user, obj_to_param, options) }
25
+ end
26
+
27
+ end
28
+
29
+ # Instance Methods - Call me from within a test
30
+ def member_action_test(controller, action, user, member = nil, options = {})
31
+ begin
32
+ assign_test_bot_lets!(options.reverse_merge!(resource: controller, action: action, user: user, member: member))
33
+ rescue => e
34
+ raise "Error: #{e.message}. Expected usage: member_test('admin/jobs', 'unarchive', User.first, Post.first || nil)"
35
+ end
36
+
37
+ self.send(:test_bot_member_test)
38
+ end
39
+
40
+ end
41
+ end