ab-split 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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