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 +4 -4
- data/app/helpers/effective_test_bot_controller_helper.rb +1 -0
- data/lib/effective_test_bot.rb +61 -0
- data/lib/effective_test_bot/engine.rb +18 -3
- data/lib/effective_test_bot/version.rb +1 -1
- data/lib/generators/templates/test_helper.rb +1 -1
- data/lib/tasks/effective_test_bot_tasks.rake +10 -1
- data/test/concerns/test_botable/base_dsl.rb +142 -0
- data/test/concerns/test_botable/crud_dsl.rb +92 -0
- data/test/concerns/test_botable/member_dsl.rb +41 -0
- data/test/concerns/test_botable/page_dsl.rb +39 -0
- data/test/concerns/test_botable/redirect_dsl.rb +37 -0
- data/test/concerns/test_botable/wizard_dsl.rb +41 -0
- data/test/support/effective_test_bot_assertions.rb +51 -19
- data/test/support/effective_test_bot_form_helper.rb +9 -5
- data/test/support/effective_test_bot_login_helper.rb +3 -3
- data/test/support/effective_test_bot_test_helper.rb +4 -2
- data/test/test_bot/integration/application_test.rb +86 -0
- data/test/test_bot/integration/minitest_test.rb +16 -11
- data/test/test_bot/models/user_test.rb +2 -0
- data/test/test_botable/base_test.rb +62 -0
- data/test/test_botable/crud_test.rb +68 -92
- data/test/test_botable/member_test.rb +20 -0
- data/test/test_botable/page_test.rb +23 -0
- data/test/test_botable/redirect_test.rb +18 -0
- data/test/test_botable/wizard_test.rb +31 -0
- metadata +14 -4
- data/test/concerns/test_botable/crud_test.rb +0 -145
- data/test/test_bot/integration/home_page_test.rb +0 -10
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: faf3170be69a804110713bd4fffd0d2802841fa2
|
4
|
+
data.tar.gz: 3ac29840a0b524fad45cded4d7d81c3bcb56bee4
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e1bc8fe65d07f530b193e9498e5d3537944d09903727f0407cc7c93bccfeed52addaa1383a56faea2bc005be2429b1e5d856da74da5bc7209e5949425dfb3d7c
|
7
|
+
data.tar.gz: 4174f5a21a7cfa52081df5d86de979b48cfe1a44fe167a5974b84653cb64f4bcd24555d0787b269336f33540376684bafc566937c46df7186e91b6a6c48c607e
|
data/lib/effective_test_bot.rb
CHANGED
@@ -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
|
19
|
-
|
20
|
-
|
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,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
|