activeexperiment 0.1.0.alpha

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +0 -0
  3. data/MIT-LICENSE +20 -0
  4. data/README.md +373 -0
  5. data/lib/active_experiment/base.rb +76 -0
  6. data/lib/active_experiment/cache/active_record_cache_store.rb +116 -0
  7. data/lib/active_experiment/cache/redis_hash_cache_store.rb +68 -0
  8. data/lib/active_experiment/cache.rb +28 -0
  9. data/lib/active_experiment/caching.rb +172 -0
  10. data/lib/active_experiment/callbacks.rb +111 -0
  11. data/lib/active_experiment/capturable.rb +117 -0
  12. data/lib/active_experiment/configured_experiment.rb +74 -0
  13. data/lib/active_experiment/core.rb +177 -0
  14. data/lib/active_experiment/executed.rb +86 -0
  15. data/lib/active_experiment/execution.rb +156 -0
  16. data/lib/active_experiment/gem_version.rb +18 -0
  17. data/lib/active_experiment/instrumentation.rb +45 -0
  18. data/lib/active_experiment/log_subscriber.rb +178 -0
  19. data/lib/active_experiment/logging.rb +62 -0
  20. data/lib/active_experiment/railtie.rb +69 -0
  21. data/lib/active_experiment/rollout.rb +106 -0
  22. data/lib/active_experiment/rollouts/inactive_rollout.rb +24 -0
  23. data/lib/active_experiment/rollouts/percent_rollout.rb +84 -0
  24. data/lib/active_experiment/rollouts/random_rollout.rb +46 -0
  25. data/lib/active_experiment/rollouts.rb +127 -0
  26. data/lib/active_experiment/rspec.rb +12 -0
  27. data/lib/active_experiment/run_key.rb +55 -0
  28. data/lib/active_experiment/segments.rb +69 -0
  29. data/lib/active_experiment/test_case.rb +11 -0
  30. data/lib/active_experiment/test_helper.rb +267 -0
  31. data/lib/active_experiment/variants.rb +145 -0
  32. data/lib/active_experiment/version.rb +11 -0
  33. data/lib/active_experiment.rb +27 -0
  34. data/lib/activeexperiment.rb +8 -0
  35. data/lib/rails/generators/experiment/USAGE +12 -0
  36. data/lib/rails/generators/experiment/experiment_generator.rb +53 -0
  37. data/lib/rails/generators/experiment/templates/application_experiment.rb.tt +4 -0
  38. data/lib/rails/generators/experiment/templates/experiment.rb.tt +35 -0
  39. data/lib/rails/generators/rspec/experiment/experiment_generator.rb +20 -0
  40. data/lib/rails/generators/rspec/experiment/templates/experiment_spec.rb.tt +7 -0
  41. data/lib/rails/generators/test_unit/experiment/experiment_generator.rb +21 -0
  42. data/lib/rails/generators/test_unit/experiment/templates/experiment_test.rb.tt +9 -0
  43. metadata +118 -0
@@ -0,0 +1,267 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/class/subclasses"
4
+ require "active_support/testing/assertions"
5
+
6
+ module ActiveExperiment
7
+ # Provides helper methods for testing Active Experiments.
8
+ module TestHelper
9
+ include ActiveSupport::Testing::Assertions
10
+
11
+ def setup
12
+ super
13
+ clear_executed_experiments
14
+ end
15
+
16
+ def teardown
17
+ super
18
+ clear_executed_experiments
19
+ end
20
+
21
+ # Returns all of the experiments that have been executed.
22
+ def executed_experiments
23
+ ActiveExperiment::Executed.as_array
24
+ end
25
+
26
+ # Clears the list of executed experiments.
27
+ def clear_executed_experiments
28
+ ActiveExperiment::Executed.clear_all
29
+ end
30
+
31
+ # Provides the ability to stub an experiment's variant assignment, by
32
+ # changing the experiment to use the MockRollout within the scope of the
33
+ # provided block.
34
+ #
35
+ # class SubjectExperiment < ActiveExperiment::Base
36
+ # variant(:red) { "red" }
37
+ # variant(:blue) { "blue" }
38
+ # variant(:green) { "green" }
39
+ # end
40
+ #
41
+ # Given the above is our experiment, the variant assignment can be stubbed
42
+ # to always assign the green variant.
43
+ #
44
+ # stub_experiment(SubjectExperiment, :green) do
45
+ # assert_equal "green", SubjectExperiment.run
46
+ # end
47
+ #
48
+ # Or an array can be used to have green be assigned for the first run, and
49
+ # blue for the second, green for the third, and so on.
50
+ #
51
+ # stub_experiment(SubjectExperiment, :green, :blue) do
52
+ # assert_equal "green", SubjectExperiment.run
53
+ # assert_equal "blue", SubjectExperiment.run
54
+ # assert_equal "green", SubjectExperiment.run
55
+ # end
56
+ #
57
+ # A lambda or proc can be used to provide custom variant assignment using
58
+ # whatever logic required.
59
+ #
60
+ # resolver = lambda do |experiment|
61
+ # experiment.context[:id] == 42 ? :green : :blue
62
+ # end
63
+ #
64
+ # stub_experiment(SubjectExperiment, resolver) do
65
+ # assert_equal "blue", SubjectExperiment.run(id: 1)
66
+ # assert_equal "blue", SubjectExperiment.run(id: 2)
67
+ # assert_equal "green", SubjectExperiment.run(id: 42)
68
+ # end
69
+ #
70
+ # Options can also be passed, for instance to simulate when an experiment
71
+ # should be skipped.
72
+ #
73
+ # stub_experiment(SubjectExperiment, skip: true) do |mock_rollout|
74
+ # assert_equal false, mock_rollout.skipped_for('_anything_')
75
+ # assert_equal :red, mock_rollout.variant_for('_anything_')
76
+ # assert_nil SubjectExperiment.run
77
+ # end
78
+ #
79
+ # By default the +MockRollout+ class will be used, which implements all of
80
+ # the functionality described above -- however, any rollout class can be
81
+ # used for the span of the provided block.
82
+ #
83
+ # stub_experiment(SubjectExperiment, rollout_class: MyCustomRollout) do
84
+ # # ...
85
+ # end
86
+ def stub_experiment(experiment_class, *variants, **options)
87
+ original_rollout = experiment_class.rollout
88
+
89
+ rollout_class = options.delete(:rollout_class) || MockRollout
90
+ rollout_options = { variant: variants }.merge(options)
91
+ experiment_class.rollout = rollout_class.new(experiment_class, **rollout_options)
92
+
93
+ _assert_nothing_raised_or_warn("stub_experiment") do
94
+ yield experiment_class.rollout
95
+ end
96
+ ensure
97
+ experiment_class.rollout = original_rollout
98
+ end
99
+
100
+ # Asserts that the number of experiments run matches the given number.
101
+ #
102
+ # def test_experiments
103
+ # assert_experiments 0
104
+ # MyExperiment.run
105
+ # assert_experiments 1
106
+ # MyExperiment.run
107
+ # assert_experiments 2
108
+ # end
109
+ #
110
+ # If a block is provided, that block should cause the specified number of
111
+ # experiments to be run.
112
+ #
113
+ # def test_experiments_again
114
+ # assert_experiments 1 do
115
+ # MyExperiment.run
116
+ # end
117
+ #
118
+ # assert_experiments 2 do
119
+ # MyExperiment.run
120
+ # MyExperiment.run
121
+ # end
122
+ # end
123
+ def assert_experiments(number, &block)
124
+ executed_count = executed_experiments.size
125
+ if block_given?
126
+ _assert_nothing_raised_or_warn("assert_experiments", &block)
127
+ executed_count = executed_experiments.size - executed_count
128
+ assert_equal number, executed_count, "#{number} experiment runs expected, but found #{executed_count}"
129
+ else
130
+ assert_equal number, executed_count
131
+ end
132
+ end
133
+
134
+ # Asserts that no experiments have been run.
135
+ #
136
+ # def test_experiments
137
+ # assert_no_experiments
138
+ # MyExperiment.run
139
+ # assert_experiments 1
140
+ # end
141
+ #
142
+ # If a block is passed, that block should not cause any emails to be sent.
143
+ #
144
+ # def test_experiments_again
145
+ # assert_no_experiments do
146
+ # # No experiments should be run from this block
147
+ # end
148
+ # end
149
+ #
150
+ # Note: This assertion is simply a shortcut for:
151
+ #
152
+ # assert_experiments(0, &block)
153
+ def assert_no_experiments(&block)
154
+ assert_experiments(0, &block)
155
+ end
156
+
157
+ # Asserts that a specific experiment has been run, optionally matching args
158
+ # and/or context.
159
+ #
160
+ # def test_experiment
161
+ # MyExperiment.run(id: 1)
162
+ # assert_experiment_with MyExperiment, context: { id: 1 }
163
+ # end
164
+ #
165
+ # def test_experiment_with_options
166
+ # MyExperiment.set(foo: :bar).run(id: 1)
167
+ # assert_experiment_with MyExperiment, options: { foo: "bar" }
168
+ # end
169
+ #
170
+ # def test_experiment_with_variant
171
+ # MyExperiment.set(variant: :red).run(id: 1)
172
+ # assert_experiment_with MyExperiment, variant: :red
173
+ # end
174
+ #
175
+ # If a block is passed, that block should cause the specified experiment to
176
+ # be run.
177
+ #
178
+ # def test_experiment_in_block
179
+ # assert_experiment_with MyExperiment, context: { id: 1 } do
180
+ # MyExperiment.run(id: 1)
181
+ # end
182
+ # end
183
+ def assert_experiment_with(experiment_class, context: nil, options: nil, variant: nil, &block)
184
+ expected = { context: context, options: options, variant: variant }.compact
185
+ experiments = executed_experiments
186
+ if block_given?
187
+ original_executed_experiments = experiments.dup
188
+ _assert_nothing_raised_or_warn("assert_experiment_with", &block)
189
+ experiments = executed_experiments - original_executed_experiments
190
+ end
191
+
192
+ match_potential = []
193
+ match_class = []
194
+ match_experiment = experiments.find do |experiment|
195
+ match_potential << experiment
196
+ if experiment.class == experiment_class
197
+ match_class << experiment
198
+ expected.all? do |key, value|
199
+ experiment.public_send(key) == value
200
+ end
201
+ end
202
+ end
203
+
204
+ message = +"No matching run found for #{experiment_class.name}"
205
+ message << " with #{expected.inspect}" if expected.any?
206
+ if match_potential.empty?
207
+ message << "\n\nNo experiment were run"
208
+ elsif match_class.empty?
209
+ message << "\n\nNo #{experiment_class.name} experiments were run, experiments run:"
210
+ message << "\n #{match_potential.map(&:class).join(", ")}"
211
+ else
212
+ message << "\n\nPotential matches:"
213
+ message << "\n #{match_class.join("\n ")}"
214
+ end
215
+
216
+ assert(match_experiment, message)
217
+ match_experiment
218
+ end
219
+
220
+ # Mock rollout class that can be used to stub out the variant assignment of
221
+ # an experiment.
222
+ #
223
+ # This is used by the ActiveExperiment::TestHelper in the +stub_experiment+
224
+ # method. It can be used directly, or through the helper when needing to
225
+ # stub out the rollout of an experiment in a test. It can also be inherited
226
+ # and customized.
227
+ class MockRollout < Rollouts::BaseRollout
228
+ def initialize(...)
229
+ @assigned = 0
230
+ super
231
+ end
232
+
233
+ def skipped_for(ex)
234
+ raise ArgumentError, "expecting a #{@experiment_class.name}" unless ex.is_a?(@experiment_class)
235
+
236
+ skip = opts[:skip]
237
+
238
+ # Accepts a callable in the :skip option.
239
+ return skip.call(ex) if skip.respond_to?(:call)
240
+
241
+ # Accepts a boolean in the :skip options, with a default of false.
242
+ !!skip
243
+ end
244
+
245
+ def variant_for(ex)
246
+ raise ArgumentError, "expecting a #{@experiment_class.name}" unless ex.is_a?(@experiment_class)
247
+
248
+ # Accepts an array in the :variant option.
249
+ variant = opts[:variant]
250
+ variant = variant[((@assigned += 1) - 1) % variant.size] if variant.is_a?(Array) && !variant.empty?
251
+
252
+ # Accepts a callable in the :variant option.
253
+ return variant.call(ex) if variant.respond_to?(:call)
254
+
255
+ # Accepts a symbol in the :variant option.
256
+ return variant if variant.is_a?(Symbol)
257
+
258
+ # Fall back to the default variant.
259
+ ex.try(:default_variant)
260
+ end
261
+
262
+ def opts
263
+ @rollout_options
264
+ end
265
+ end
266
+ end
267
+ end
@@ -0,0 +1,145 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveExperiment
4
+ # == Variant Registration
5
+ #
6
+ # Variants are registered using the +control+ and +variant+ methods within an
7
+ # experiment. The +control+ method is a convenience for registering a variant
8
+ # with the name +:control+ -- a convention used to describe the default
9
+ # variant.
10
+ #
11
+ # Registering a variant can be done by providing a name, and a block or a
12
+ # symbol referencing a method.
13
+ #
14
+ # class MyExperiment < ActiveExperiment::Base
15
+ # control { "control" } # defines a variant named :control
16
+ # variant :treatment, :treatment_method
17
+ #
18
+ # private
19
+ #
20
+ # def treatment_method
21
+ # "treatment"
22
+ # end
23
+ # end
24
+ #
25
+ # Subclassing experiments will inherit variants. Existing variants can be
26
+ # overridden or added to, and new variants can registered.
27
+ #
28
+ # class NewExperiment < MyExperiment
29
+ # control(override: true) { "new control" }
30
+ # variant(:treatment, add: true, prepend: true) { "new treatment" }
31
+ # variant(:new_variant) { "new variant" }
32
+ # end
33
+ #
34
+ # In the above example, the control is overridden, a new variant is added,
35
+ # and a new step is added to the treatment variant.
36
+ #
37
+ # When running an experiment, variants can be overridden by using the +on+
38
+ # method. This allows utilizing the scope of where the experiment is run to
39
+ # change the experiment behavior.
40
+ #
41
+ # NewExperiment.run do |experiment|
42
+ # experiment.on(:treatment) { "overridden treatment" }
43
+ # end
44
+ #
45
+ # By default, the "control" variant is assigned as the default variant, but
46
+ # any variant can be specified as the default. The concept of the control
47
+ # variant is only a convention.
48
+ #
49
+ # To specify a different default variant:
50
+ #
51
+ # class MyExperiment < ActiveExperiment::Base
52
+ # variant(:red) { "red" }
53
+ # variant(:blue) { "blue" }
54
+ #
55
+ # use_default_variant :blue
56
+ # end
57
+ #
58
+ # The default variant is assigned if the experiment is skipped or if no other
59
+ # variant has been resolved after asking the rollout -- when the rollout may
60
+ # not be working properly.
61
+ module Variants
62
+ extend ActiveSupport::Concern
63
+ include ActiveSupport::Callbacks
64
+
65
+ VARIANT_CHAIN_SUFFIX = "_variant"
66
+ private_constant :VARIANT_CHAIN_SUFFIX
67
+
68
+ STEP_CHAIN_SUFFIX = "_steps"
69
+ private_constant :STEP_CHAIN_SUFFIX
70
+
71
+ included do
72
+ class_attribute :variants, instance_writer: false, instance_predicate: false, default: {}
73
+ class_attribute :default_variant, instance_writer: false, instance_predicate: false, default: :control
74
+ end
75
+
76
+ # These methods will be included into any Active Experiment object, adding
77
+ # the the ability to set the default variant, defining variants and their
78
+ # behaviors and callback helpers.
79
+ module ClassMethods
80
+ private
81
+ def use_default_variant(variant)
82
+ variant = variant.to_sym
83
+ raise ArgumentError, "Unknown #{variant.inspect} variant" unless variants[variant]
84
+
85
+ self.default_variant = variant
86
+ end
87
+
88
+ def register_variant_callback(variant, *filters, override: false, add: false, **options, &block)
89
+ raise ArgumentError, "Provide either `override: true` or `add: true` but not both" if override && add
90
+
91
+ variant = variant.to_sym
92
+ if variants[variant].present?
93
+ unless override || add
94
+ raise ArgumentError, "The #{variant.inspect} variant is already registered. " \
95
+ "Provide `override: true` or `add: true` to make changes to it."
96
+ end
97
+ elsif override || add
98
+ raise ArgumentError, "Unable to override or add to unknown #{variant.inspect} variant"
99
+ end
100
+
101
+ self.variants = variants.dup unless singleton_class.method_defined?(:variants, false)
102
+
103
+ variants[variant] = callback_chain = :"#{variant}#{VARIANT_CHAIN_SUFFIX}"
104
+
105
+ unless add
106
+ define_variant_callbacks(callback_chain) # variant callback chain
107
+ define_variant_callbacks("#{callback_chain}#{STEP_CHAIN_SUFFIX}") # variant step chain
108
+ end
109
+
110
+ filters.push(block) if block.present?
111
+ filters.unshift(callback_chain) if filters.empty?
112
+ set_callback_with_target("#{callback_chain}#{STEP_CHAIN_SUFFIX}", *filters, **options) do |target, callback|
113
+ target.instance_variable_set(:@results, callback.call(target, nil))
114
+ end
115
+ end
116
+
117
+ def define_variant_callbacks(callback_chain)
118
+ define_callbacks(callback_chain)
119
+ private :"_#{callback_chain}_callbacks", :"_run_#{callback_chain}_callbacks"
120
+ end
121
+
122
+ def set_variant_callback(variant, type, *filters, &block)
123
+ raise ArgumentError, "Unknown `#{variant}` variant" unless variants[variant.to_sym]
124
+
125
+ set_callback("#{variant}#{VARIANT_CHAIN_SUFFIX}", type, *filters, &block)
126
+ end
127
+ end
128
+
129
+ # The names for all registered variants.
130
+ #
131
+ # This is most commonly used by rollouts, where knowing the variant names
132
+ # is important for determining which variant to assign.
133
+ def variant_names
134
+ variants.keys
135
+ end
136
+
137
+ private
138
+ def variant_step_chains
139
+ @variant_step_chains ||= variants.transform_values do |callback_chain|
140
+ chain_name = "#{callback_chain}#{STEP_CHAIN_SUFFIX}"
141
+ -> { run_callbacks(chain_name, :process_variant_steps) { @results } }
142
+ end
143
+ end
144
+ end
145
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "gem_version"
4
+
5
+ module ActiveExperiment
6
+ # Returns the currently loaded version of Active Experiment as a
7
+ # +Gem::Version+.
8
+ def self.version
9
+ gem_version
10
+ end
11
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "global_id"
4
+ require "active_support"
5
+ require "active_support/rails"
6
+ require "active_support/tagged_logging"
7
+
8
+ require "active_experiment/version"
9
+
10
+ module ActiveExperiment
11
+ Error = Class.new(StandardError)
12
+ ExecutionError = Class.new(Error)
13
+
14
+ extend ActiveSupport::Autoload
15
+
16
+ autoload :Base
17
+ autoload :Cache
18
+ autoload :ConfiguredExperiment
19
+ autoload :Executed
20
+ autoload :Rollouts
21
+ autoload :Capturable
22
+
23
+ autoload :TestCase
24
+ autoload :TestHelper
25
+
26
+ mattr_accessor :logger, default: ActiveSupport::TaggedLogging.new(ActiveSupport::Logger.new(STDOUT))
27
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This file is here to make the gem behave like a Rails core library...
4
+ require "active_experiment"
5
+
6
+ # But we'll also require the railtie if it looks like it's loading in a Rails
7
+ # app, to avoid having to require it manually in application.rb.
8
+ require "active_experiment/railtie" if defined?(Rails)
@@ -0,0 +1,12 @@
1
+ Description:
2
+ Generates a new experiment. Pass the experiment name, either CamelCased or
3
+ under_scored, with or without the experiment postfix. Optionally, provide a
4
+ list of variant names.
5
+
6
+ Examples:
7
+ `bin/rails generate experiment RedBlueExperiment red blue`
8
+
9
+ Creates the the following files:
10
+
11
+ Experiment: app/experiments/red_blue_experiment.rb
12
+ Test: test/experiments/red_blue_experiment_test.rb
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators/named_base"
4
+
5
+ module Rails # :nodoc:
6
+ module Generators # :nodoc:
7
+ class ExperimentGenerator < Rails::Generators::NamedBase # :nodoc:
8
+ class_option :parent, type: :string, default: "ApplicationExperiment", desc: "The parent class for the generated experiment"
9
+ class_option :skip_comments, type: :boolean, default: false, desc: "Omit helpful comments from generated files"
10
+
11
+ argument :variants, type: :array, default: %w[control treatment], banner: "variant variant"
12
+
13
+ check_class_collision suffix: "Experiment"
14
+
15
+ hook_for :test_framework
16
+
17
+ def self.default_generator_root
18
+ __dir__
19
+ end
20
+
21
+ def create_experiment_file
22
+ template "experiment.rb", File.join("app/experiments", class_path, "#{file_name}_experiment.rb")
23
+
24
+ in_root do
25
+ if behavior == :invoke && !File.exist?(application_experiment_file_name)
26
+ template "application_experiment.rb", application_experiment_file_name
27
+ end
28
+ end
29
+ end
30
+
31
+ private
32
+ def parent_class_name
33
+ options[:parent]
34
+ end
35
+
36
+ def variant_names
37
+ @variant_names ||= variants.map { |variant| variant.to_s.underscore }
38
+ end
39
+
40
+ def file_name
41
+ @_file_name ||= super.sub(/_experiment\z/i, "")
42
+ end
43
+
44
+ def application_experiment_file_name
45
+ @application_experiment_file_name ||= if mountable_engine?
46
+ "app/experiments/#{namespaced_path}/application_experiment.rb"
47
+ else
48
+ "app/experiments/application_experiment.rb"
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,4 @@
1
+ <% module_namespacing do -%>
2
+ class ApplicationExperiment < ActiveExperiment::Base
3
+ end
4
+ <% end -%>
@@ -0,0 +1,35 @@
1
+ <% module_namespacing do -%>
2
+ class <%= class_name %>Experiment < <%= parent_class_name.classify %>
3
+ <% variant_names.each do |variant| -%>
4
+ <% if variant == "control" -%>
5
+ <%= variant -%> { }
6
+ <% else -%>
7
+ variant(:<%= variant -%>) { }
8
+ <% end -%>
9
+ <% end -%>
10
+
11
+ <% unless options[:skip_comments] -%>
12
+ <% unless variant_names.include?("control") -%>
13
+ # Specify a default variant to use when the experiment is skipped:
14
+ #use_default_variant :<%= variant_names.first %>
15
+ #
16
+ <% end -%>
17
+ # Run this experiment by providing a context (current_user in this example),
18
+ # and optionally override the variant behaviors:
19
+ #
20
+ # <%= class_name %>Experiment.run(current_user) do |experiment|
21
+ <% variant_names.each do |variant| -%>
22
+ # experiment.on(:<%= variant -%>) { "overridden <%= variant -%>" }
23
+ <% end -%>
24
+ # end
25
+ #
26
+ # Each context (user) will consistently have the same variant assigned. More
27
+ # advanced logic can be provided to segment contexts into specific variants:
28
+ #
29
+ #segment :old_accounts, into: :<%= variant_names.last %>
30
+ #def old_accounts
31
+ # context.created_at < 1.year.ago
32
+ #end
33
+ <% end -%>
34
+ end
35
+ <% end -%>
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ # require "generators/rspec"
4
+
5
+ module Rspec # :nodoc:
6
+ module Generators # :nodoc:
7
+ class ExperimentGenerator < Rails::Generators::NamedBase # :nodoc:
8
+ source_root(File.expand_path("templates", __dir__))
9
+
10
+ def create_spec_file
11
+ template "experiment_spec.rb", File.join("spec/experiments", class_path, "#{file_name}_experiment_spec.rb")
12
+ end
13
+
14
+ private
15
+ def file_name
16
+ @_file_name ||= super.sub(/_experiment\z/i, "")
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,7 @@
1
+ require "rails_helper"
2
+
3
+ <% module_namespacing do -%>
4
+ RSpec.describe <%= class_name -%>Experiment, type: :experiment do
5
+ pending "add some examples to (or delete) #{__FILE__}"
6
+ end
7
+ <% end -%>
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators/test_unit"
4
+
5
+ module TestUnit # :nodoc:
6
+ module Generators # :nodoc:
7
+ class ExperimentGenerator < Base # :nodoc:
8
+ check_class_collision suffix: "ExperimentTest"
9
+ source_root(File.expand_path("templates", __dir__))
10
+
11
+ def create_test_file
12
+ template "experiment_test.rb", File.join("test/experiments", class_path, "#{file_name}_experiment_test.rb")
13
+ end
14
+
15
+ private
16
+ def file_name
17
+ @_file_name ||= super.sub(/_experiment\z/i, "")
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,9 @@
1
+ require "test_helper"
2
+
3
+ <% module_namespacing do -%>
4
+ class <%= class_name %>ExperimentTest < ActiveJob::TestCase
5
+ # test "the truth" do
6
+ # assert true
7
+ # end
8
+ end
9
+ <% end -%>