ab-split 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 (93) hide show
  1. checksums.yaml +7 -0
  2. data/.codeclimate.yml +30 -0
  3. data/.csslintrc +2 -0
  4. data/.eslintignore +1 -0
  5. data/.eslintrc +213 -0
  6. data/.github/FUNDING.yml +1 -0
  7. data/.github/ISSUE_TEMPLATE/bug_report.md +24 -0
  8. data/.rspec +1 -0
  9. data/.rubocop.yml +7 -0
  10. data/.rubocop_todo.yml +679 -0
  11. data/.travis.yml +60 -0
  12. data/Appraisals +19 -0
  13. data/CHANGELOG.md +696 -0
  14. data/CODE_OF_CONDUCT.md +74 -0
  15. data/CONTRIBUTING.md +62 -0
  16. data/Gemfile +7 -0
  17. data/LICENSE +22 -0
  18. data/README.md +955 -0
  19. data/Rakefile +9 -0
  20. data/ab-split.gemspec +44 -0
  21. data/gemfiles/4.2.gemfile +9 -0
  22. data/gemfiles/5.0.gemfile +9 -0
  23. data/gemfiles/5.1.gemfile +9 -0
  24. data/gemfiles/5.2.gemfile +9 -0
  25. data/gemfiles/6.0.gemfile +9 -0
  26. data/lib/split.rb +76 -0
  27. data/lib/split/algorithms/block_randomization.rb +23 -0
  28. data/lib/split/algorithms/weighted_sample.rb +18 -0
  29. data/lib/split/algorithms/whiplash.rb +38 -0
  30. data/lib/split/alternative.rb +191 -0
  31. data/lib/split/combined_experiments_helper.rb +37 -0
  32. data/lib/split/configuration.rb +255 -0
  33. data/lib/split/dashboard.rb +74 -0
  34. data/lib/split/dashboard/helpers.rb +45 -0
  35. data/lib/split/dashboard/pagination_helpers.rb +86 -0
  36. data/lib/split/dashboard/paginator.rb +16 -0
  37. data/lib/split/dashboard/public/dashboard-filtering.js +43 -0
  38. data/lib/split/dashboard/public/dashboard.js +24 -0
  39. data/lib/split/dashboard/public/jquery-1.11.1.min.js +4 -0
  40. data/lib/split/dashboard/public/reset.css +48 -0
  41. data/lib/split/dashboard/public/style.css +328 -0
  42. data/lib/split/dashboard/views/_controls.erb +18 -0
  43. data/lib/split/dashboard/views/_experiment.erb +155 -0
  44. data/lib/split/dashboard/views/_experiment_with_goal_header.erb +8 -0
  45. data/lib/split/dashboard/views/index.erb +26 -0
  46. data/lib/split/dashboard/views/layout.erb +27 -0
  47. data/lib/split/encapsulated_helper.rb +42 -0
  48. data/lib/split/engine.rb +15 -0
  49. data/lib/split/exceptions.rb +6 -0
  50. data/lib/split/experiment.rb +486 -0
  51. data/lib/split/experiment_catalog.rb +51 -0
  52. data/lib/split/extensions/string.rb +16 -0
  53. data/lib/split/goals_collection.rb +45 -0
  54. data/lib/split/helper.rb +165 -0
  55. data/lib/split/metric.rb +101 -0
  56. data/lib/split/persistence.rb +28 -0
  57. data/lib/split/persistence/cookie_adapter.rb +94 -0
  58. data/lib/split/persistence/dual_adapter.rb +85 -0
  59. data/lib/split/persistence/redis_adapter.rb +57 -0
  60. data/lib/split/persistence/session_adapter.rb +29 -0
  61. data/lib/split/redis_interface.rb +50 -0
  62. data/lib/split/trial.rb +117 -0
  63. data/lib/split/user.rb +69 -0
  64. data/lib/split/version.rb +7 -0
  65. data/lib/split/zscore.rb +57 -0
  66. data/spec/algorithms/block_randomization_spec.rb +32 -0
  67. data/spec/algorithms/weighted_sample_spec.rb +19 -0
  68. data/spec/algorithms/whiplash_spec.rb +24 -0
  69. data/spec/alternative_spec.rb +320 -0
  70. data/spec/combined_experiments_helper_spec.rb +57 -0
  71. data/spec/configuration_spec.rb +258 -0
  72. data/spec/dashboard/pagination_helpers_spec.rb +200 -0
  73. data/spec/dashboard/paginator_spec.rb +37 -0
  74. data/spec/dashboard_helpers_spec.rb +42 -0
  75. data/spec/dashboard_spec.rb +210 -0
  76. data/spec/encapsulated_helper_spec.rb +52 -0
  77. data/spec/experiment_catalog_spec.rb +53 -0
  78. data/spec/experiment_spec.rb +533 -0
  79. data/spec/goals_collection_spec.rb +80 -0
  80. data/spec/helper_spec.rb +1111 -0
  81. data/spec/metric_spec.rb +31 -0
  82. data/spec/persistence/cookie_adapter_spec.rb +106 -0
  83. data/spec/persistence/dual_adapter_spec.rb +194 -0
  84. data/spec/persistence/redis_adapter_spec.rb +90 -0
  85. data/spec/persistence/session_adapter_spec.rb +32 -0
  86. data/spec/persistence_spec.rb +34 -0
  87. data/spec/redis_interface_spec.rb +111 -0
  88. data/spec/spec_helper.rb +52 -0
  89. data/spec/split_spec.rb +43 -0
  90. data/spec/support/cookies_mock.rb +20 -0
  91. data/spec/trial_spec.rb +299 -0
  92. data/spec/user_spec.rb +87 -0
  93. metadata +322 -0
@@ -0,0 +1,80 @@
1
+ require 'spec_helper'
2
+ require 'split/goals_collection'
3
+ require 'time'
4
+
5
+ describe Split::GoalsCollection do
6
+ let(:experiment_name) { 'experiment_name' }
7
+
8
+ describe 'initialization' do
9
+ let(:goals_collection) {
10
+ Split::GoalsCollection.new('experiment_name', ['goal1', 'goal2'])
11
+ }
12
+
13
+ it "should have an experiment_name" do
14
+ expect(goals_collection.instance_variable_get(:@experiment_name)).
15
+ to eq('experiment_name')
16
+ end
17
+
18
+ it "should have a list of goals" do
19
+ expect(goals_collection.instance_variable_get(:@goals)).
20
+ to eq(['goal1', 'goal2'])
21
+ end
22
+ end
23
+
24
+ describe "#validate!" do
25
+ it "should't raise ArgumentError if @goals is nil?" do
26
+ goals_collection = Split::GoalsCollection.new('experiment_name')
27
+ expect { goals_collection.validate! }.not_to raise_error
28
+ end
29
+
30
+ it "should raise ArgumentError if @goals is not an Array" do
31
+ goals_collection = Split::GoalsCollection.
32
+ new('experiment_name', 'not an array')
33
+ expect { goals_collection.validate! }.to raise_error(ArgumentError)
34
+ end
35
+
36
+ it "should't raise ArgumentError if @goals is an array" do
37
+ goals_collection = Split::GoalsCollection.
38
+ new('experiment_name', ['an array'])
39
+ expect { goals_collection.validate! }.not_to raise_error
40
+ end
41
+ end
42
+
43
+ describe "#delete" do
44
+ let(:goals_key) { "#{experiment_name}:goals" }
45
+
46
+ it "should delete goals from redis" do
47
+ goals_collection = Split::GoalsCollection.new(experiment_name, ['goal1'])
48
+ goals_collection.save
49
+
50
+ goals_collection.delete
51
+ expect(Split.redis.exists(goals_key)).to be false
52
+ end
53
+ end
54
+
55
+ describe "#save" do
56
+ let(:goals_key) { "#{experiment_name}:goals" }
57
+
58
+ it "should return false if @goals is nil" do
59
+ goals_collection = Split::GoalsCollection.
60
+ new(experiment_name, nil)
61
+
62
+ expect(goals_collection.save).to be false
63
+ end
64
+
65
+ it "should save goals to redis if @goals is valid" do
66
+ goals = ['valid goal 1', 'valid goal 2']
67
+ collection = Split::GoalsCollection.new(experiment_name, goals)
68
+ collection.save
69
+
70
+ expect(Split.redis.lrange(goals_key, 0, -1)).to eq goals
71
+ end
72
+
73
+ it "should return @goals if @goals is valid" do
74
+ goals_collection = Split::GoalsCollection.
75
+ new(experiment_name, ['valid goal'])
76
+
77
+ expect(goals_collection.save).to eq(['valid goal'])
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,1111 @@
1
+ # frozen_string_literal: true
2
+ require 'spec_helper'
3
+
4
+ # TODO change some of these tests to use Rack::Test
5
+
6
+ describe Split::Helper do
7
+ include Split::Helper
8
+
9
+ let(:experiment) {
10
+ Split::ExperimentCatalog.find_or_create('link_color', 'blue', 'red')
11
+ }
12
+
13
+ describe "ab_test" do
14
+ it "should not raise an error when passed strings for alternatives" do
15
+ expect(lambda { ab_test('xyz', '1', '2', '3') }).not_to raise_error
16
+ end
17
+
18
+ it "should not raise an error when passed an array for alternatives" do
19
+ expect(lambda { ab_test('xyz', ['1', '2', '3']) }).not_to raise_error
20
+ end
21
+
22
+ it "should raise the appropriate error when passed integers for alternatives" do
23
+ expect(lambda { ab_test('xyz', 1, 2, 3) }).to raise_error(ArgumentError)
24
+ end
25
+
26
+ it "should raise the appropriate error when passed symbols for alternatives" do
27
+ expect(lambda { ab_test('xyz', :a, :b, :c) }).to raise_error(ArgumentError)
28
+ end
29
+
30
+ it "should not raise error when passed an array for goals" do
31
+ expect(lambda { ab_test({'link_color' => ["purchase", "refund"]}, 'blue', 'red') }).not_to raise_error
32
+ end
33
+
34
+ it "should not raise error when passed just one goal" do
35
+ expect(lambda { ab_test({'link_color' => "purchase"}, 'blue', 'red') }).not_to raise_error
36
+ end
37
+
38
+ it "raises an appropriate error when processing combined expirements" do
39
+ Split.configuration.experiments = {
40
+ :combined_exp_1 => {
41
+ :alternatives => [ { name: "control", percent: 50 }, { name: "test-alt", percent: 50 } ],
42
+ :metric => :my_metric,
43
+ :combined_experiments => [:combined_exp_1_sub_1]
44
+ }
45
+ }
46
+ Split::ExperimentCatalog.find_or_create('combined_exp_1')
47
+ expect(lambda { ab_test('combined_exp_1')}).to raise_error(Split::InvalidExperimentsFormatError )
48
+ end
49
+
50
+ it "should assign a random alternative to a new user when there are an equal number of alternatives assigned" do
51
+ ab_test('link_color', 'blue', 'red')
52
+ expect(['red', 'blue']).to include(ab_user['link_color'])
53
+ end
54
+
55
+ it "should increment the participation counter after assignment to a new user" do
56
+ previous_red_count = Split::Alternative.new('red', 'link_color').participant_count
57
+ previous_blue_count = Split::Alternative.new('blue', 'link_color').participant_count
58
+
59
+ ab_test('link_color', 'blue', 'red')
60
+
61
+ new_red_count = Split::Alternative.new('red', 'link_color').participant_count
62
+ new_blue_count = Split::Alternative.new('blue', 'link_color').participant_count
63
+
64
+ expect((new_red_count + new_blue_count)).to eq(previous_red_count + previous_blue_count + 1)
65
+ end
66
+
67
+ it 'should not increment the counter for an experiment that the user is not participating in' do
68
+ ab_test('link_color', 'blue', 'red')
69
+ e = Split::ExperimentCatalog.find_or_create('button_size', 'small', 'big')
70
+ expect(lambda {
71
+ # User shouldn't participate in this second experiment
72
+ ab_test('button_size', 'small', 'big')
73
+ }).not_to change { e.participant_count }
74
+ end
75
+
76
+ it 'should not increment the counter for an ended experiment' do
77
+ e = Split::ExperimentCatalog.find_or_create('button_size', 'small', 'big')
78
+ e.winner = 'small'
79
+ expect(lambda {
80
+ a = ab_test('button_size', 'small', 'big')
81
+ expect(a).to eq('small')
82
+ }).not_to change { e.participant_count }
83
+ end
84
+
85
+ it 'should not increment the counter for an not started experiment' do
86
+ expect(Split.configuration).to receive(:start_manually).and_return(true)
87
+ e = Split::ExperimentCatalog.find_or_create('button_size', 'small', 'big')
88
+ expect(lambda {
89
+ a = ab_test('button_size', 'small', 'big')
90
+ expect(a).to eq('small')
91
+ }).not_to change { e.participant_count }
92
+ end
93
+
94
+ it "should return the given alternative for an existing user" do
95
+ expect(ab_test('link_color', 'blue', 'red')).to eq ab_test('link_color', 'blue', 'red')
96
+ end
97
+
98
+ it 'should always return the winner if one is present' do
99
+ experiment.winner = "orange"
100
+
101
+ expect(ab_test('link_color', 'blue', 'red')).to eq('orange')
102
+ end
103
+
104
+ it "should allow the alternative to be forced by passing it in the params" do
105
+ # ?ab_test[link_color]=blue
106
+ @params = { 'ab_test' => { 'link_color' => 'blue' } }
107
+
108
+ alternative = ab_test('link_color', 'blue', 'red')
109
+ expect(alternative).to eq('blue')
110
+
111
+ alternative = ab_test('link_color', {'blue' => 1}, 'red' => 5)
112
+ expect(alternative).to eq('blue')
113
+
114
+ @params = { 'ab_test' => { 'link_color' => 'red' } }
115
+
116
+ alternative = ab_test('link_color', 'blue', 'red')
117
+ expect(alternative).to eq('red')
118
+
119
+ alternative = ab_test('link_color', {'blue' => 5}, 'red' => 1)
120
+ expect(alternative).to eq('red')
121
+ end
122
+
123
+ it "should not allow an arbitrary alternative" do
124
+ @params = { 'ab_test' => { 'link_color' => 'pink' } }
125
+ alternative = ab_test('link_color', 'blue')
126
+ expect(alternative).to eq('blue')
127
+ end
128
+
129
+ it "should not store the split when a param forced alternative" do
130
+ @params = { 'ab_test' => { 'link_color' => 'blue' } }
131
+ expect(ab_user).not_to receive(:[]=)
132
+ ab_test('link_color', 'blue', 'red')
133
+ end
134
+
135
+ it "SPLIT_DISABLE query parameter should also force the alternative (uses control)" do
136
+ @params = {'SPLIT_DISABLE' => 'true'}
137
+ alternative = ab_test('link_color', 'blue', 'red')
138
+ expect(alternative).to eq('blue')
139
+ alternative = ab_test('link_color', {'blue' => 1}, 'red' => 5)
140
+ expect(alternative).to eq('blue')
141
+ alternative = ab_test('link_color', 'red', 'blue')
142
+ expect(alternative).to eq('red')
143
+ alternative = ab_test('link_color', {'red' => 5}, 'blue' => 1)
144
+ expect(alternative).to eq('red')
145
+ end
146
+
147
+ it "should not store the split when Split generically disabled" do
148
+ @params = {'SPLIT_DISABLE' => 'true'}
149
+ expect(ab_user).not_to receive(:[]=)
150
+ ab_test('link_color', 'blue', 'red')
151
+ end
152
+
153
+ context "when store_override is set" do
154
+ before { Split.configuration.store_override = true }
155
+
156
+ it "should store the forced alternative" do
157
+ @params = { 'ab_test' => { 'link_color' => 'blue' } }
158
+ expect(ab_user).to receive(:[]=).with('link_color', 'blue')
159
+ ab_test('link_color', 'blue', 'red')
160
+ end
161
+ end
162
+
163
+ context "when on_trial_choose is set" do
164
+ before { Split.configuration.on_trial_choose = :some_method }
165
+ it "should call the method" do
166
+ expect(self).to receive(:some_method)
167
+ ab_test('link_color', 'blue', 'red')
168
+ end
169
+ end
170
+
171
+ it "should allow passing a block" do
172
+ alt = ab_test('link_color', 'blue', 'red')
173
+ ret = ab_test('link_color', 'blue', 'red') { |alternative| "shared/#{alternative}" }
174
+ expect(ret).to eq("shared/#{alt}")
175
+ end
176
+
177
+ it "should allow the share of visitors see an alternative to be specified" do
178
+ ab_test('link_color', {'blue' => 0.8}, {'red' => 20})
179
+ expect(['red', 'blue']).to include(ab_user['link_color'])
180
+ end
181
+
182
+ it "should allow alternative weighting interface as a single hash" do
183
+ ab_test('link_color', {'blue' => 0.01}, 'red' => 0.2)
184
+ experiment = Split::ExperimentCatalog.find('link_color')
185
+ expect(experiment.alternatives.map(&:name)).to eq(['blue', 'red'])
186
+ expect(experiment.alternatives.collect{|a| a.weight}).to match_array([0.01, 0.2])
187
+ end
188
+
189
+ it "should only let a user participate in one experiment at a time" do
190
+ link_color = ab_test('link_color', 'blue', 'red')
191
+ ab_test('button_size', 'small', 'big')
192
+ expect(ab_user['link_color']).to eq(link_color)
193
+ big = Split::Alternative.new('big', 'button_size')
194
+ expect(big.participant_count).to eq(0)
195
+ small = Split::Alternative.new('small', 'button_size')
196
+ expect(small.participant_count).to eq(0)
197
+ end
198
+
199
+ it "should let a user participate in many experiment with allow_multiple_experiments option" do
200
+ Split.configure do |config|
201
+ config.allow_multiple_experiments = true
202
+ end
203
+ link_color = ab_test('link_color', 'blue', 'red')
204
+ button_size = ab_test('button_size', 'small', 'big')
205
+ expect(ab_user['link_color']).to eq(link_color)
206
+ expect(ab_user['button_size']).to eq(button_size)
207
+ button_size_alt = Split::Alternative.new(button_size, 'button_size')
208
+ expect(button_size_alt.participant_count).to eq(1)
209
+ end
210
+
211
+ context "with allow_multiple_experiments = 'control'" do
212
+ it "should let a user participate in many experiment with one non-'control' alternative" do
213
+ Split.configure do |config|
214
+ config.allow_multiple_experiments = 'control'
215
+ end
216
+ groups = 100.times.map do |n|
217
+ ab_test("test#{n}".to_sym, {'control' => (100 - n)}, {"test#{n}-alt" => n})
218
+ end
219
+
220
+ experiments = ab_user.active_experiments
221
+ expect(experiments.size).to be > 1
222
+
223
+ count_control = experiments.values.count { |g| g == 'control' }
224
+ expect(count_control).to eq(experiments.size - 1)
225
+
226
+ count_alts = groups.count { |g| g != 'control' }
227
+ expect(count_alts).to eq(1)
228
+ end
229
+
230
+ context "when user already has experiment" do
231
+ let(:mock_user){ Split::User.new(self, {'test_0' => 'test-alt'}) }
232
+ before{
233
+ Split.configure do |config|
234
+ config.allow_multiple_experiments = 'control'
235
+ end
236
+ Split::ExperimentCatalog.find_or_initialize('test_0', 'control', 'test-alt').save
237
+ Split::ExperimentCatalog.find_or_initialize('test_1', 'control', 'test-alt').save
238
+ }
239
+
240
+ it "should restore previously selected alternative" do
241
+ expect(ab_user.active_experiments.size).to eq 1
242
+ expect(ab_test(:test_0, {'control' => 100}, {"test-alt" => 1})).to eq 'test-alt'
243
+ expect(ab_test(:test_0, {'control' => 1}, {"test-alt" => 100})).to eq 'test-alt'
244
+ end
245
+
246
+ it "lets override existing choice" do
247
+ pending "this requires user store reset on first call not depending on whelther it is current trial"
248
+ @params = { 'ab_test' => { 'test_1' => 'test-alt' } }
249
+
250
+ expect(ab_test(:test_0, {'control' => 0}, {"test-alt" => 100})).to eq 'control'
251
+ expect(ab_test(:test_1, {'control' => 100}, {"test-alt" => 1})).to eq 'test-alt'
252
+ end
253
+
254
+ end
255
+
256
+ end
257
+
258
+ it "should not over-write a finished key when an experiment is on a later version" do
259
+ experiment.increment_version
260
+ ab_user = { experiment.key => 'blue', experiment.finished_key => true }
261
+ finished_session = ab_user.dup
262
+ ab_test('link_color', 'blue', 'red')
263
+ expect(ab_user).to eq(finished_session)
264
+ end
265
+ end
266
+
267
+ describe 'metadata' do
268
+ before do
269
+ Split.configuration.experiments = {
270
+ :my_experiment => {
271
+ :alternatives => ["one", "two"],
272
+ :resettable => false,
273
+ :metadata => { 'one' => 'Meta1', 'two' => 'Meta2' }
274
+ }
275
+ }
276
+ end
277
+
278
+ it 'should be passed to helper block' do
279
+ @params = { 'ab_test' => { 'my_experiment' => 'one' } }
280
+ expect(ab_test('my_experiment')).to eq 'one'
281
+ expect(ab_test('my_experiment') do |alternative, meta|
282
+ meta
283
+ end).to eq('Meta1')
284
+ end
285
+
286
+ it 'should pass empty hash to helper block if library disabled' do
287
+ Split.configure do |config|
288
+ config.enabled = false
289
+ end
290
+
291
+ expect(ab_test('my_experiment')).to eq 'one'
292
+ expect(ab_test('my_experiment') do |_, meta|
293
+ meta
294
+ end).to eq({})
295
+ end
296
+ end
297
+
298
+ describe 'ab_finished' do
299
+ context 'for an experiment that the user participates in' do
300
+ before(:each) do
301
+ @experiment_name = 'link_color'
302
+ @alternatives = ['blue', 'red']
303
+ @experiment = Split::ExperimentCatalog.find_or_create(@experiment_name, *@alternatives)
304
+ @alternative_name = ab_test(@experiment_name, *@alternatives)
305
+ @previous_completion_count = Split::Alternative.new(@alternative_name, @experiment_name).completed_count
306
+ end
307
+
308
+ it 'should increment the counter for the completed alternative' do
309
+ ab_finished(@experiment_name)
310
+ new_completion_count = Split::Alternative.new(@alternative_name, @experiment_name).completed_count
311
+ expect(new_completion_count).to eq(@previous_completion_count + 1)
312
+ end
313
+
314
+ it "should set experiment's finished key if reset is false" do
315
+ ab_finished(@experiment_name, {:reset => false})
316
+ expect(ab_user[@experiment.key]).to eq(@alternative_name)
317
+ expect(ab_user[@experiment.finished_key]).to eq(true)
318
+ end
319
+
320
+ it 'should not increment the counter if reset is false and the experiment has been already finished' do
321
+ 2.times { ab_finished(@experiment_name, {:reset => false}) }
322
+ new_completion_count = Split::Alternative.new(@alternative_name, @experiment_name).completed_count
323
+ expect(new_completion_count).to eq(@previous_completion_count + 1)
324
+ end
325
+
326
+ it 'should not increment the counter for an ended experiment' do
327
+ e = Split::ExperimentCatalog.find_or_create('button_size', 'small', 'big')
328
+ e.winner = 'small'
329
+ a = ab_test('button_size', 'small', 'big')
330
+ expect(a).to eq('small')
331
+ expect(lambda {
332
+ ab_finished('button_size')
333
+ }).not_to change { Split::Alternative.new(a, 'button_size').completed_count }
334
+ end
335
+
336
+ it "should clear out the user's participation from their session" do
337
+ expect(ab_user[@experiment.key]).to eq(@alternative_name)
338
+ ab_finished(@experiment_name)
339
+ expect(ab_user.keys).to be_empty
340
+ end
341
+
342
+ it "should not clear out the users session if reset is false" do
343
+ expect(ab_user[@experiment.key]).to eq(@alternative_name)
344
+ ab_finished(@experiment_name, {:reset => false})
345
+ expect(ab_user[@experiment.key]).to eq(@alternative_name)
346
+ expect(ab_user[@experiment.finished_key]).to eq(true)
347
+ end
348
+
349
+ it "should reset the users session when experiment is not versioned" do
350
+ expect(ab_user[@experiment.key]).to eq(@alternative_name)
351
+ ab_finished(@experiment_name)
352
+ expect(ab_user.keys).to be_empty
353
+ end
354
+
355
+ it "should reset the users session when experiment is versioned" do
356
+ @experiment.increment_version
357
+ @alternative_name = ab_test(@experiment_name, *@alternatives)
358
+
359
+ expect(ab_user[@experiment.key]).to eq(@alternative_name)
360
+ ab_finished(@experiment_name)
361
+ expect(ab_user.keys).to be_empty
362
+ end
363
+
364
+ context "when on_trial_complete is set" do
365
+ before { Split.configuration.on_trial_complete = :some_method }
366
+ it "should call the method" do
367
+ expect(self).to receive(:some_method)
368
+ ab_finished(@experiment_name)
369
+ end
370
+
371
+ it "should not call the method without alternative" do
372
+ ab_user[@experiment.key] = nil
373
+ expect(self).not_to receive(:some_method)
374
+ ab_finished(@experiment_name)
375
+ end
376
+ end
377
+ end
378
+
379
+ context 'for an experiment that the user is excluded from' do
380
+ before do
381
+ alternative = ab_test('link_color', 'blue', 'red')
382
+ expect(Split::Alternative.new(alternative, 'link_color').participant_count).to eq(1)
383
+ alternative = ab_test('button_size', 'small', 'big')
384
+ expect(Split::Alternative.new(alternative, 'button_size').participant_count).to eq(0)
385
+ end
386
+
387
+ it 'should not increment the completed counter' do
388
+ # So, user should be participating in the link_color experiment and
389
+ # receive the control for button_size. As the user is not participating in
390
+ # the button size experiment, finishing it should not increase the
391
+ # completion count for that alternative.
392
+ expect(lambda {
393
+ ab_finished('button_size')
394
+ }).not_to change { Split::Alternative.new('small', 'button_size').completed_count }
395
+ end
396
+ end
397
+
398
+ context 'for an experiment that the user does not participate in' do
399
+ before do
400
+ Split::ExperimentCatalog.find_or_create(:not_started_experiment, 'control', 'alt')
401
+ end
402
+ it 'should not raise an exception' do
403
+ expect { ab_finished(:not_started_experiment) }.not_to raise_exception
404
+ end
405
+
406
+ it 'should not change the user state when reset is false' do
407
+ expect { ab_finished(:not_started_experiment, reset: false) }.not_to change { ab_user.keys}.from([])
408
+ end
409
+
410
+ it 'should not change the user state when reset is true' do
411
+ expect(self).not_to receive(:reset!)
412
+ ab_finished(:not_started_experiment)
413
+ end
414
+
415
+ it 'should not increment the completed counter' do
416
+ ab_finished(:not_started_experiment)
417
+ expect(Split::Alternative.new('control', :not_started_experiment).completed_count).to eq(0)
418
+ expect(Split::Alternative.new('alt', :not_started_experiment).completed_count).to eq(0)
419
+ end
420
+ end
421
+ end
422
+
423
+ context "finished with config" do
424
+ it "passes reset option" do
425
+ Split.configuration.experiments = {
426
+ :my_experiment => {
427
+ :alternatives => ["one", "two"],
428
+ :resettable => false,
429
+ }
430
+ }
431
+ alternative = ab_test(:my_experiment)
432
+ experiment = Split::ExperimentCatalog.find :my_experiment
433
+
434
+ ab_finished :my_experiment
435
+ expect(ab_user[experiment.key]).to eq(alternative)
436
+ expect(ab_user[experiment.finished_key]).to eq(true)
437
+ end
438
+ end
439
+
440
+ context "finished with metric name" do
441
+ before { Split.configuration.experiments = {} }
442
+ before { expect(Split::Alternative).to receive(:new).at_least(1).times.and_call_original }
443
+
444
+ def should_finish_experiment(experiment_name, should_finish=true)
445
+ alts = Split.configuration.experiments[experiment_name][:alternatives]
446
+ experiment = Split::ExperimentCatalog.find_or_create(experiment_name, *alts)
447
+ alt_name = ab_user[experiment.key] = alts.first
448
+ alt = double('alternative')
449
+ expect(alt).to receive(:name).at_most(1).times.and_return(alt_name)
450
+ expect(Split::Alternative).to receive(:new).at_most(1).times.with(alt_name, experiment_name.to_s).and_return(alt)
451
+ if should_finish
452
+ expect(alt).to receive(:increment_completion).at_most(1).times
453
+ else
454
+ expect(alt).not_to receive(:increment_completion)
455
+ end
456
+ end
457
+
458
+ it "completes the test" do
459
+ Split.configuration.experiments[:my_experiment] = {
460
+ :alternatives => [ "control_opt", "other_opt" ],
461
+ :metric => :my_metric
462
+ }
463
+ should_finish_experiment :my_experiment
464
+ ab_finished :my_metric
465
+ end
466
+
467
+ it "completes all relevant tests" do
468
+ Split.configuration.experiments = {
469
+ :exp_1 => {
470
+ :alternatives => [ "1-1", "1-2" ],
471
+ :metric => :my_metric
472
+ },
473
+ :exp_2 => {
474
+ :alternatives => [ "2-1", "2-2" ],
475
+ :metric => :another_metric
476
+ },
477
+ :exp_3 => {
478
+ :alternatives => [ "3-1", "3-2" ],
479
+ :metric => :my_metric
480
+ },
481
+ }
482
+ should_finish_experiment :exp_1
483
+ should_finish_experiment :exp_2, false
484
+ should_finish_experiment :exp_3
485
+ ab_finished :my_metric
486
+ end
487
+
488
+ it "passes reset option" do
489
+ Split.configuration.experiments = {
490
+ :my_exp => {
491
+ :alternatives => ["one", "two"],
492
+ :metric => :my_metric,
493
+ :resettable => false,
494
+ }
495
+ }
496
+ alternative_name = ab_test(:my_exp)
497
+ exp = Split::ExperimentCatalog.find :my_exp
498
+
499
+ ab_finished :my_metric
500
+ expect(ab_user[exp.key]).to eq(alternative_name)
501
+ expect(ab_user[exp.finished_key]).to be_truthy
502
+ end
503
+
504
+ it "passes through options" do
505
+ Split.configuration.experiments = {
506
+ :my_exp => {
507
+ :alternatives => ["one", "two"],
508
+ :metric => :my_metric,
509
+ }
510
+ }
511
+ alternative_name = ab_test(:my_exp)
512
+ exp = Split::ExperimentCatalog.find :my_exp
513
+
514
+ ab_finished :my_metric, :reset => false
515
+ expect(ab_user[exp.key]).to eq(alternative_name)
516
+ expect(ab_user[exp.finished_key]).to be_truthy
517
+ end
518
+ end
519
+
520
+ describe 'conversions' do
521
+ it 'should return a conversion rate for an alternative' do
522
+ alternative_name = ab_test('link_color', 'blue', 'red')
523
+
524
+ previous_convertion_rate = Split::Alternative.new(alternative_name, 'link_color').conversion_rate
525
+ expect(previous_convertion_rate).to eq(0.0)
526
+
527
+ ab_finished('link_color')
528
+
529
+ new_convertion_rate = Split::Alternative.new(alternative_name, 'link_color').conversion_rate
530
+ expect(new_convertion_rate).to eq(1.0)
531
+ end
532
+ end
533
+
534
+ describe 'active experiments' do
535
+ it 'should show an active test' do
536
+ alternative = ab_test('def', '4', '5', '6')
537
+ expect(active_experiments.count).to eq 1
538
+ expect(active_experiments.first[0]).to eq "def"
539
+ expect(active_experiments.first[1]).to eq alternative
540
+ end
541
+
542
+ it 'should show a finished test' do
543
+ alternative = ab_test('def', '4', '5', '6')
544
+ ab_finished('def', {:reset => false})
545
+ expect(active_experiments.count).to eq 1
546
+ expect(active_experiments.first[0]).to eq "def"
547
+ expect(active_experiments.first[1]).to eq alternative
548
+ end
549
+
550
+ it 'should show an active test when an experiment is on a later version' do
551
+ experiment.reset
552
+ expect(experiment.version).to eq(1)
553
+ ab_test('link_color', 'blue', 'red')
554
+ expect(active_experiments.count).to eq 1
555
+ expect(active_experiments.first[0]).to eq "link_color"
556
+ end
557
+
558
+ it 'should show versioned tests properly' do
559
+ 10.times { experiment.reset }
560
+
561
+ alternative = ab_test(experiment.name, 'blue', 'red')
562
+ ab_finished(experiment.name, reset: false)
563
+
564
+ expect(experiment.version).to eq(10)
565
+ expect(active_experiments.count).to eq 1
566
+ expect(active_experiments).to eq({'link_color' => alternative })
567
+ end
568
+
569
+ it 'should show multiple tests' do
570
+ Split.configure do |config|
571
+ config.allow_multiple_experiments = true
572
+ end
573
+ alternative = ab_test('def', '4', '5', '6')
574
+ another_alternative = ab_test('ghi', '7', '8', '9')
575
+ expect(active_experiments.count).to eq 2
576
+ expect(active_experiments['def']).to eq alternative
577
+ expect(active_experiments['ghi']).to eq another_alternative
578
+ end
579
+
580
+ it 'should not show tests with winners' do
581
+ Split.configure do |config|
582
+ config.allow_multiple_experiments = true
583
+ end
584
+ e = Split::ExperimentCatalog.find_or_create('def', '4', '5', '6')
585
+ e.winner = '4'
586
+ ab_test('def', '4', '5', '6')
587
+ another_alternative = ab_test('ghi', '7', '8', '9')
588
+ expect(active_experiments.count).to eq 1
589
+ expect(active_experiments.first[0]).to eq "ghi"
590
+ expect(active_experiments.first[1]).to eq another_alternative
591
+ end
592
+ end
593
+
594
+ describe 'when user is a robot' do
595
+ before(:each) do
596
+ @request = OpenStruct.new(:user_agent => 'Googlebot/2.1 (+http://www.google.com/bot.html)')
597
+ end
598
+
599
+ describe 'ab_test' do
600
+ it 'should return the control' do
601
+ alternative = ab_test('link_color', 'blue', 'red')
602
+ expect(alternative).to eq experiment.control.name
603
+ end
604
+
605
+ it 'should not create a experiment' do
606
+ ab_test('link_color', 'blue', 'red')
607
+ expect(Split::Experiment.new('link_color')).to be_a_new_record
608
+ end
609
+
610
+ it "should not increment the participation count" do
611
+
612
+ previous_red_count = Split::Alternative.new('red', 'link_color').participant_count
613
+ previous_blue_count = Split::Alternative.new('blue', 'link_color').participant_count
614
+
615
+ ab_test('link_color', 'blue', 'red')
616
+
617
+ new_red_count = Split::Alternative.new('red', 'link_color').participant_count
618
+ new_blue_count = Split::Alternative.new('blue', 'link_color').participant_count
619
+
620
+ expect((new_red_count + new_blue_count)).to eq(previous_red_count + previous_blue_count)
621
+ end
622
+ end
623
+
624
+ describe 'finished' do
625
+ it "should not increment the completed count" do
626
+ alternative_name = ab_test('link_color', 'blue', 'red')
627
+
628
+ previous_completion_count = Split::Alternative.new(alternative_name, 'link_color').completed_count
629
+
630
+ ab_finished('link_color')
631
+
632
+ new_completion_count = Split::Alternative.new(alternative_name, 'link_color').completed_count
633
+
634
+ expect(new_completion_count).to eq(previous_completion_count)
635
+ end
636
+ end
637
+ end
638
+
639
+ describe 'when providing custom ignore logic' do
640
+ context "using a proc to configure custom logic" do
641
+
642
+ before(:each) do
643
+ Split.configure do |c|
644
+ c.ignore_filter = proc{|request| true } # ignore everything
645
+ end
646
+ end
647
+
648
+ it "ignores the ab_test" do
649
+ ab_test('link_color', 'blue', 'red')
650
+
651
+ red_count = Split::Alternative.new('red', 'link_color').participant_count
652
+ blue_count = Split::Alternative.new('blue', 'link_color').participant_count
653
+ expect((red_count + blue_count)).to be(0)
654
+ end
655
+ end
656
+ end
657
+
658
+ shared_examples_for "a disabled test" do
659
+ describe 'ab_test' do
660
+ it 'should return the control' do
661
+ alternative = ab_test('link_color', 'blue', 'red')
662
+ expect(alternative).to eq experiment.control.name
663
+ end
664
+
665
+ it "should not increment the participation count" do
666
+ previous_red_count = Split::Alternative.new('red', 'link_color').participant_count
667
+ previous_blue_count = Split::Alternative.new('blue', 'link_color').participant_count
668
+
669
+ ab_test('link_color', 'blue', 'red')
670
+
671
+ new_red_count = Split::Alternative.new('red', 'link_color').participant_count
672
+ new_blue_count = Split::Alternative.new('blue', 'link_color').participant_count
673
+
674
+ expect((new_red_count + new_blue_count)).to eq(previous_red_count + previous_blue_count)
675
+ end
676
+ end
677
+
678
+ describe 'finished' do
679
+ it "should not increment the completed count" do
680
+ alternative_name = ab_test('link_color', 'blue', 'red')
681
+
682
+ previous_completion_count = Split::Alternative.new(alternative_name, 'link_color').completed_count
683
+
684
+ ab_finished('link_color')
685
+
686
+ new_completion_count = Split::Alternative.new(alternative_name, 'link_color').completed_count
687
+
688
+ expect(new_completion_count).to eq(previous_completion_count)
689
+ end
690
+ end
691
+ end
692
+
693
+ describe 'when ip address is ignored' do
694
+ context "individually" do
695
+ before(:each) do
696
+ @request = OpenStruct.new(:ip => '81.19.48.130')
697
+ Split.configure do |c|
698
+ c.ignore_ip_addresses << '81.19.48.130'
699
+ end
700
+ end
701
+
702
+ it_behaves_like "a disabled test"
703
+ end
704
+
705
+ context "for a range" do
706
+ before(:each) do
707
+ @request = OpenStruct.new(:ip => '81.19.48.129')
708
+ Split.configure do |c|
709
+ c.ignore_ip_addresses << /81\.19\.48\.[0-9]+/
710
+ end
711
+ end
712
+
713
+ it_behaves_like "a disabled test"
714
+ end
715
+
716
+ context "using both a range and a specific value" do
717
+ before(:each) do
718
+ @request = OpenStruct.new(:ip => '81.19.48.128')
719
+ Split.configure do |c|
720
+ c.ignore_ip_addresses << '81.19.48.130'
721
+ c.ignore_ip_addresses << /81\.19\.48\.[0-9]+/
722
+ end
723
+ end
724
+
725
+ it_behaves_like "a disabled test"
726
+ end
727
+
728
+ context "when ignored other address" do
729
+ before do
730
+ @request = OpenStruct.new(:ip => '1.1.1.1')
731
+ Split.configure do |c|
732
+ c.ignore_ip_addresses << '81.19.48.130'
733
+ end
734
+ end
735
+
736
+ it "works as usual" do
737
+ alternative_name = ab_test('link_color', 'red', 'blue')
738
+ expect{
739
+ ab_finished('link_color')
740
+ }.to change(Split::Alternative.new(alternative_name, 'link_color'), :completed_count).by(1)
741
+ end
742
+ end
743
+ end
744
+
745
+ describe 'when user is previewing' do
746
+ before(:each) do
747
+ @request = OpenStruct.new(headers: { 'x-purpose' => 'preview' })
748
+ end
749
+
750
+ it_behaves_like "a disabled test"
751
+ end
752
+
753
+ describe 'versioned experiments' do
754
+ it "should use version zero if no version is present" do
755
+ alternative_name = ab_test('link_color', 'blue', 'red')
756
+ expect(experiment.version).to eq(0)
757
+ expect(ab_user['link_color']).to eq(alternative_name)
758
+ end
759
+
760
+ it "should save the version of the experiment to the session" do
761
+ experiment.reset
762
+ expect(experiment.version).to eq(1)
763
+ alternative_name = ab_test('link_color', 'blue', 'red')
764
+ expect(ab_user['link_color:1']).to eq(alternative_name)
765
+ end
766
+
767
+ it "should load the experiment even if the version is not 0" do
768
+ experiment.reset
769
+ expect(experiment.version).to eq(1)
770
+ alternative_name = ab_test('link_color', 'blue', 'red')
771
+ expect(ab_user['link_color:1']).to eq(alternative_name)
772
+ return_alternative_name = ab_test('link_color', 'blue', 'red')
773
+ expect(return_alternative_name).to eq(alternative_name)
774
+ end
775
+
776
+ it "should reset the session of a user on an older version of the experiment" do
777
+ alternative_name = ab_test('link_color', 'blue', 'red')
778
+ expect(ab_user['link_color']).to eq(alternative_name)
779
+ alternative = Split::Alternative.new(alternative_name, 'link_color')
780
+ expect(alternative.participant_count).to eq(1)
781
+
782
+ experiment.reset
783
+ expect(experiment.version).to eq(1)
784
+ alternative = Split::Alternative.new(alternative_name, 'link_color')
785
+ expect(alternative.participant_count).to eq(0)
786
+
787
+ new_alternative_name = ab_test('link_color', 'blue', 'red')
788
+ expect(ab_user['link_color:1']).to eq(new_alternative_name)
789
+ new_alternative = Split::Alternative.new(new_alternative_name, 'link_color')
790
+ expect(new_alternative.participant_count).to eq(1)
791
+ end
792
+
793
+ it "should cleanup old versions of experiments from the session" do
794
+ alternative_name = ab_test('link_color', 'blue', 'red')
795
+ expect(ab_user['link_color']).to eq(alternative_name)
796
+ alternative = Split::Alternative.new(alternative_name, 'link_color')
797
+ expect(alternative.participant_count).to eq(1)
798
+
799
+ experiment.reset
800
+ expect(experiment.version).to eq(1)
801
+ alternative = Split::Alternative.new(alternative_name, 'link_color')
802
+ expect(alternative.participant_count).to eq(0)
803
+
804
+ new_alternative_name = ab_test('link_color', 'blue', 'red')
805
+ expect(ab_user['link_color:1']).to eq(new_alternative_name)
806
+ end
807
+
808
+ it "should only count completion of users on the current version" do
809
+ alternative_name = ab_test('link_color', 'blue', 'red')
810
+ expect(ab_user['link_color']).to eq(alternative_name)
811
+ alternative = Split::Alternative.new(alternative_name, 'link_color')
812
+
813
+ experiment.reset
814
+ expect(experiment.version).to eq(1)
815
+
816
+ ab_finished('link_color')
817
+ alternative = Split::Alternative.new(alternative_name, 'link_color')
818
+ expect(alternative.completed_count).to eq(0)
819
+ end
820
+ end
821
+
822
+ context 'when redis is not available' do
823
+ before(:each) do
824
+ expect(Split).to receive(:redis).at_most(5).times.and_raise(Errno::ECONNREFUSED.new)
825
+ end
826
+
827
+ context 'and db_failover config option is turned off' do
828
+ before(:each) do
829
+ Split.configure do |config|
830
+ config.db_failover = false
831
+ end
832
+ end
833
+
834
+ describe 'ab_test' do
835
+ it 'should raise an exception' do
836
+ expect(lambda { ab_test('link_color', 'blue', 'red') }).to raise_error(Errno::ECONNREFUSED)
837
+ end
838
+ end
839
+
840
+ describe 'finished' do
841
+ it 'should raise an exception' do
842
+ expect(lambda { ab_finished('link_color') }).to raise_error(Errno::ECONNREFUSED)
843
+ end
844
+ end
845
+
846
+ describe "disable split testing" do
847
+ before(:each) do
848
+ Split.configure do |config|
849
+ config.enabled = false
850
+ end
851
+ end
852
+
853
+ it "should not attempt to connect to redis" do
854
+ expect(lambda { ab_test('link_color', 'blue', 'red') }).not_to raise_error
855
+ end
856
+
857
+ it "should return control variable" do
858
+ expect(ab_test('link_color', 'blue', 'red')).to eq('blue')
859
+ expect(lambda { ab_finished('link_color') }).not_to raise_error
860
+ end
861
+ end
862
+ end
863
+
864
+ context 'and db_failover config option is turned on' do
865
+ before(:each) do
866
+ Split.configure do |config|
867
+ config.db_failover = true
868
+ end
869
+ end
870
+
871
+ describe 'ab_test' do
872
+ it 'should not raise an exception' do
873
+ expect(lambda { ab_test('link_color', 'blue', 'red') }).not_to raise_error
874
+ end
875
+
876
+ it 'should call db_failover_on_db_error proc with error as parameter' do
877
+ Split.configure do |config|
878
+ config.db_failover_on_db_error = proc do |error|
879
+ expect(error).to be_a(Errno::ECONNREFUSED)
880
+ end
881
+ end
882
+
883
+ expect(Split.configuration.db_failover_on_db_error).to receive(:call).and_call_original
884
+ ab_test('link_color', 'blue', 'red')
885
+ end
886
+
887
+ it 'should always use first alternative' do
888
+ expect(ab_test('link_color', 'blue', 'red')).to eq('blue')
889
+ expect(ab_test('link_color', {'blue' => 0.01}, 'red' => 0.2)).to eq('blue')
890
+ expect(ab_test('link_color', {'blue' => 0.8}, {'red' => 20})).to eq('blue')
891
+ expect(ab_test('link_color', 'blue', 'red') do |alternative|
892
+ "shared/#{alternative}"
893
+ end).to eq('shared/blue')
894
+ end
895
+
896
+ context 'and db_failover_allow_parameter_override config option is turned on' do
897
+ before(:each) do
898
+ Split.configure do |config|
899
+ config.db_failover_allow_parameter_override = true
900
+ end
901
+ end
902
+
903
+ context 'and given an override parameter' do
904
+ it 'should use given override instead of the first alternative' do
905
+ @params = { 'ab_test' => { 'link_color' => 'red' } }
906
+ expect(ab_test('link_color', 'blue', 'red')).to eq('red')
907
+ expect(ab_test('link_color', 'blue', 'red', 'green')).to eq('red')
908
+ expect(ab_test('link_color', {'blue' => 0.01}, 'red' => 0.2)).to eq('red')
909
+ expect(ab_test('link_color', {'blue' => 0.8}, {'red' => 20})).to eq('red')
910
+ expect(ab_test('link_color', 'blue', 'red') do |alternative|
911
+ "shared/#{alternative}"
912
+ end).to eq('shared/red')
913
+ end
914
+ end
915
+ end
916
+
917
+ context 'and preloaded config given' do
918
+ before do
919
+ Split.configuration.experiments[:link_color] = {
920
+ :alternatives => [ "blue", "red" ],
921
+ }
922
+ end
923
+
924
+ it "uses first alternative" do
925
+ expect(ab_test(:link_color)).to eq("blue")
926
+ end
927
+ end
928
+ end
929
+
930
+ describe 'finished' do
931
+ it 'should not raise an exception' do
932
+ expect(lambda { ab_finished('link_color') }).not_to raise_error
933
+ end
934
+
935
+ it 'should call db_failover_on_db_error proc with error as parameter' do
936
+ Split.configure do |config|
937
+ config.db_failover_on_db_error = proc do |error|
938
+ expect(error).to be_a(Errno::ECONNREFUSED)
939
+ end
940
+ end
941
+
942
+ expect(Split.configuration.db_failover_on_db_error).to receive(:call).and_call_original
943
+ ab_finished('link_color')
944
+ end
945
+ end
946
+ end
947
+ end
948
+
949
+ context "with preloaded config" do
950
+ before { Split.configuration.experiments = {}}
951
+
952
+ it "pulls options from config file" do
953
+ Split.configuration.experiments[:my_experiment] = {
954
+ :alternatives => [ "control_opt", "other_opt" ],
955
+ :goals => ["goal1", "goal2"]
956
+ }
957
+ ab_test :my_experiment
958
+ expect(Split::Experiment.new(:my_experiment).alternatives.map(&:name)).to eq([ "control_opt", "other_opt" ])
959
+ expect(Split::Experiment.new(:my_experiment).goals).to eq([ "goal1", "goal2" ])
960
+ end
961
+
962
+ it "can be called multiple times" do
963
+ Split.configuration.experiments[:my_experiment] = {
964
+ :alternatives => [ "control_opt", "other_opt" ],
965
+ :goals => ["goal1", "goal2"]
966
+ }
967
+ 5.times { ab_test :my_experiment }
968
+ experiment = Split::Experiment.new(:my_experiment)
969
+ expect(experiment.alternatives.map(&:name)).to eq([ "control_opt", "other_opt" ])
970
+ expect(experiment.goals).to eq([ "goal1", "goal2" ])
971
+ expect(experiment.participant_count).to eq(1)
972
+ end
973
+
974
+ it "accepts multiple goals" do
975
+ Split.configuration.experiments[:my_experiment] = {
976
+ :alternatives => [ "control_opt", "other_opt" ],
977
+ :goals => [ "goal1", "goal2", "goal3" ]
978
+ }
979
+ ab_test :my_experiment
980
+ experiment = Split::Experiment.new(:my_experiment)
981
+ expect(experiment.goals).to eq([ "goal1", "goal2", "goal3" ])
982
+ end
983
+
984
+ it "allow specifying goals to be optional" do
985
+ Split.configuration.experiments[:my_experiment] = {
986
+ :alternatives => [ "control_opt", "other_opt" ]
987
+ }
988
+ experiment = Split::Experiment.new(:my_experiment)
989
+ expect(experiment.goals).to eq([])
990
+ end
991
+
992
+ it "accepts multiple alternatives" do
993
+ Split.configuration.experiments[:my_experiment] = {
994
+ :alternatives => [ "control_opt", "second_opt", "third_opt" ],
995
+ }
996
+ ab_test :my_experiment
997
+ experiment = Split::Experiment.new(:my_experiment)
998
+ expect(experiment.alternatives.map(&:name)).to eq([ "control_opt", "second_opt", "third_opt" ])
999
+ end
1000
+
1001
+ it "accepts probability on alternatives" do
1002
+ Split.configuration.experiments[:my_experiment] = {
1003
+ :alternatives => [
1004
+ { :name => "control_opt", :percent => 67 },
1005
+ { :name => "second_opt", :percent => 10 },
1006
+ { :name => "third_opt", :percent => 23 },
1007
+ ],
1008
+ }
1009
+ ab_test :my_experiment
1010
+ experiment = Split::Experiment.new(:my_experiment)
1011
+ expect(experiment.alternatives.collect{|a| [a.name, a.weight]}).to eq([['control_opt', 0.67], ['second_opt', 0.1], ['third_opt', 0.23]])
1012
+ end
1013
+
1014
+ it "accepts probability on some alternatives" do
1015
+ Split.configuration.experiments[:my_experiment] = {
1016
+ :alternatives => [
1017
+ { :name => "control_opt", :percent => 34 },
1018
+ "second_opt",
1019
+ { :name => "third_opt", :percent => 23 },
1020
+ "fourth_opt",
1021
+ ],
1022
+ }
1023
+ ab_test :my_experiment
1024
+ experiment = Split::Experiment.new(:my_experiment)
1025
+ names_and_weights = experiment.alternatives.collect{|a| [a.name, a.weight]}
1026
+ expect(names_and_weights).to eq([['control_opt', 0.34], ['second_opt', 0.215], ['third_opt', 0.23], ['fourth_opt', 0.215]])
1027
+ expect(names_and_weights.inject(0){|sum, nw| sum + nw[1]}).to eq(1.0)
1028
+ end
1029
+
1030
+ it "allows name param without probability" do
1031
+ Split.configuration.experiments[:my_experiment] = {
1032
+ :alternatives => [
1033
+ { :name => "control_opt" },
1034
+ "second_opt",
1035
+ { :name => "third_opt", :percent => 64 },
1036
+ ],
1037
+ }
1038
+ ab_test :my_experiment
1039
+ experiment = Split::Experiment.new(:my_experiment)
1040
+ names_and_weights = experiment.alternatives.collect{|a| [a.name, a.weight]}
1041
+ expect(names_and_weights).to eq([['control_opt', 0.18], ['second_opt', 0.18], ['third_opt', 0.64]])
1042
+ expect(names_and_weights.inject(0){|sum, nw| sum + nw[1]}).to eq(1.0)
1043
+ end
1044
+
1045
+ it "fails gracefully if config is missing experiment" do
1046
+ Split.configuration.experiments = { :other_experiment => { :foo => "Bar" } }
1047
+ expect(lambda { ab_test :my_experiment }).to raise_error(Split::ExperimentNotFound)
1048
+ end
1049
+
1050
+ it "fails gracefully if config is missing" do
1051
+ expect(lambda { Split.configuration.experiments = nil }).to raise_error(Split::InvalidExperimentsFormatError)
1052
+ end
1053
+
1054
+ it "fails gracefully if config is missing alternatives" do
1055
+ Split.configuration.experiments[:my_experiment] = { :foo => "Bar" }
1056
+ expect(lambda { ab_test :my_experiment }).to raise_error(NoMethodError)
1057
+ end
1058
+ end
1059
+
1060
+ it 'should handle multiple experiments correctly' do
1061
+ experiment2 = Split::ExperimentCatalog.find_or_create('link_color2', 'blue', 'red')
1062
+ ab_test('link_color', 'blue', 'red')
1063
+ ab_test('link_color2', 'blue', 'red')
1064
+ ab_finished('link_color2')
1065
+
1066
+ experiment2.alternatives.each do |alt|
1067
+ expect(alt.unfinished_count).to eq(0)
1068
+ end
1069
+ end
1070
+
1071
+ context "with goals" do
1072
+ before do
1073
+ @experiment = {'link_color' => ["purchase", "refund"]}
1074
+ @alternatives = ['blue', 'red']
1075
+ @experiment_name, @goals = normalize_metric(@experiment)
1076
+ @goal1 = @goals[0]
1077
+ @goal2 = @goals[1]
1078
+ end
1079
+
1080
+ it "should normalize experiment" do
1081
+ expect(@experiment_name).to eq("link_color")
1082
+ expect(@goals).to eq(["purchase", "refund"])
1083
+ end
1084
+
1085
+ describe "ab_test" do
1086
+ it "should allow experiment goals interface as a single hash" do
1087
+ ab_test(@experiment, *@alternatives)
1088
+ experiment = Split::ExperimentCatalog.find('link_color')
1089
+ expect(experiment.goals).to eq(['purchase', "refund"])
1090
+ end
1091
+ end
1092
+
1093
+ describe "ab_finished" do
1094
+ before do
1095
+ @alternative_name = ab_test(@experiment, *@alternatives)
1096
+ end
1097
+
1098
+ it "should increment the counter for the specified-goal completed alternative" do
1099
+ expect(lambda {
1100
+ expect(lambda {
1101
+ ab_finished({"link_color" => ["purchase"]})
1102
+ }).not_to change {
1103
+ Split::Alternative.new(@alternative_name, @experiment_name).completed_count(@goal2)
1104
+ }
1105
+ }).to change {
1106
+ Split::Alternative.new(@alternative_name, @experiment_name).completed_count(@goal1)
1107
+ }.by(1)
1108
+ end
1109
+ end
1110
+ end
1111
+ end