split 3.3.0 → 4.0.0.pre

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 (69) hide show
  1. checksums.yaml +4 -4
  2. data/.eslintrc +1 -1
  3. data/.github/FUNDING.yml +1 -0
  4. data/.github/ISSUE_TEMPLATE/bug_report.md +24 -0
  5. data/.rspec +1 -0
  6. data/.rubocop.yml +71 -1044
  7. data/.rubocop_todo.yml +226 -0
  8. data/.travis.yml +18 -39
  9. data/Appraisals +4 -0
  10. data/CHANGELOG.md +110 -0
  11. data/CODE_OF_CONDUCT.md +3 -3
  12. data/Gemfile +2 -0
  13. data/README.md +58 -23
  14. data/Rakefile +2 -0
  15. data/gemfiles/{4.2.gemfile → 6.0.gemfile} +1 -1
  16. data/lib/split.rb +16 -3
  17. data/lib/split/algorithms/block_randomization.rb +2 -0
  18. data/lib/split/algorithms/weighted_sample.rb +2 -1
  19. data/lib/split/algorithms/whiplash.rb +3 -2
  20. data/lib/split/alternative.rb +4 -3
  21. data/lib/split/cache.rb +28 -0
  22. data/lib/split/combined_experiments_helper.rb +3 -2
  23. data/lib/split/configuration.rb +15 -14
  24. data/lib/split/dashboard.rb +19 -1
  25. data/lib/split/dashboard/helpers.rb +3 -2
  26. data/lib/split/dashboard/pagination_helpers.rb +4 -4
  27. data/lib/split/dashboard/paginator.rb +1 -0
  28. data/lib/split/dashboard/public/dashboard.js +10 -0
  29. data/lib/split/dashboard/public/style.css +5 -0
  30. data/lib/split/dashboard/views/_controls.erb +13 -0
  31. data/lib/split/dashboard/views/layout.erb +1 -1
  32. data/lib/split/encapsulated_helper.rb +3 -2
  33. data/lib/split/engine.rb +7 -4
  34. data/lib/split/exceptions.rb +1 -0
  35. data/lib/split/experiment.rb +98 -65
  36. data/lib/split/experiment_catalog.rb +1 -3
  37. data/lib/split/extensions/string.rb +1 -0
  38. data/lib/split/goals_collection.rb +2 -0
  39. data/lib/split/helper.rb +30 -10
  40. data/lib/split/metric.rb +2 -1
  41. data/lib/split/persistence.rb +4 -2
  42. data/lib/split/persistence/cookie_adapter.rb +1 -0
  43. data/lib/split/persistence/dual_adapter.rb +54 -12
  44. data/lib/split/persistence/redis_adapter.rb +5 -0
  45. data/lib/split/persistence/session_adapter.rb +1 -0
  46. data/lib/split/redis_interface.rb +9 -28
  47. data/lib/split/trial.rb +25 -17
  48. data/lib/split/user.rb +19 -3
  49. data/lib/split/version.rb +2 -4
  50. data/lib/split/zscore.rb +1 -0
  51. data/spec/alternative_spec.rb +1 -1
  52. data/spec/cache_spec.rb +88 -0
  53. data/spec/configuration_spec.rb +1 -14
  54. data/spec/dashboard/pagination_helpers_spec.rb +3 -1
  55. data/spec/dashboard_helpers_spec.rb +2 -2
  56. data/spec/dashboard_spec.rb +78 -17
  57. data/spec/encapsulated_helper_spec.rb +2 -2
  58. data/spec/experiment_spec.rb +116 -12
  59. data/spec/goals_collection_spec.rb +1 -1
  60. data/spec/helper_spec.rb +191 -112
  61. data/spec/persistence/cookie_adapter_spec.rb +1 -1
  62. data/spec/persistence/dual_adapter_spec.rb +160 -68
  63. data/spec/persistence/redis_adapter_spec.rb +9 -0
  64. data/spec/redis_interface_spec.rb +0 -69
  65. data/spec/spec_helper.rb +5 -6
  66. data/spec/trial_spec.rb +65 -19
  67. data/spec/user_spec.rb +28 -0
  68. data/split.gemspec +9 -9
  69. metadata +34 -28
@@ -212,20 +212,6 @@ describe Split::Configuration do
212
212
  expect(@config.normalized_experiments).to eq({:my_experiment=>{:alternatives=>[{"control_opt"=>0.67}, [{"second_opt"=>0.1}, {"third_opt"=>0.23}]]}})
213
213
  end
214
214
 
215
- context 'redis_url configuration [DEPRECATED]' do
216
- it 'should warn on set and assign to #redis' do
217
- expect(@config).to receive(:warn).with(/\[DEPRECATED\]/) { nil }
218
- @config.redis_url = 'example_url'
219
- expect(@config.redis).to eq('example_url')
220
- end
221
-
222
- it 'should warn on get and return #redis' do
223
- expect(@config).to receive(:warn).with(/\[DEPRECATED\]/) { nil }
224
- @config.redis = 'example_url'
225
- expect(@config.redis_url).to eq('example_url')
226
- end
227
- end
228
-
229
215
  context "redis configuration" do
230
216
  it "should default to local redis server" do
231
217
  expect(@config.redis).to eq("redis://localhost:6379")
@@ -240,6 +226,7 @@ describe Split::Configuration do
240
226
  it "should use the ENV variable" do
241
227
  ENV['REDIS_URL'] = "env_redis_url"
242
228
  expect(Split::Configuration.new.redis).to eq("env_redis_url")
229
+ ENV.delete('REDIS_URL')
243
230
  end
244
231
  end
245
232
  end
@@ -10,7 +10,9 @@ describe Split::DashboardPaginationHelpers do
10
10
  context 'when params empty' do
11
11
  let(:params) { Hash[] }
12
12
 
13
- it 'returns 10' do
13
+ it 'returns the default (10)' do
14
+ default_per_page = Split.configuration.dashboard_pagination_default_per_page
15
+ expect(pagination_per).to eql default_per_page
14
16
  expect(pagination_per).to eql 10
15
17
  end
16
18
  end
@@ -27,11 +27,11 @@ describe Split::DashboardHelpers do
27
27
 
28
28
  describe '#round' do
29
29
  it 'can round number strings' do
30
- expect(round('3.1415')).to eq BigDecimal.new('3.14')
30
+ expect(round('3.1415')).to eq BigDecimal('3.14')
31
31
  end
32
32
 
33
33
  it 'can round number strings for precsion' do
34
- expect(round('3.1415', 1)).to eq BigDecimal.new('3.1')
34
+ expect(round('3.1415', 1)).to eq BigDecimal('3.1')
35
35
  end
36
36
 
37
37
  it 'can handle invalid number strings' do
@@ -6,8 +6,16 @@ require 'split/dashboard'
6
6
  describe Split::Dashboard do
7
7
  include Rack::Test::Methods
8
8
 
9
+ class TestDashboard < Split::Dashboard
10
+ include Split::Helper
11
+
12
+ get '/my_experiment' do
13
+ ab_test(params[:experiment], 'blue', 'red')
14
+ end
15
+ end
16
+
9
17
  def app
10
- @app ||= Split::Dashboard
18
+ @app ||= TestDashboard
11
19
  end
12
20
 
13
21
  def link(color)
@@ -29,6 +37,10 @@ describe Split::Dashboard do
29
37
  let(:red_link) { link("red") }
30
38
  let(:blue_link) { link("blue") }
31
39
 
40
+ before(:each) do
41
+ Split.configuration.beta_probability_simulations = 1
42
+ end
43
+
32
44
  it "should respond to /" do
33
45
  get '/'
34
46
  expect(last_response).to be_ok
@@ -74,17 +86,49 @@ describe Split::Dashboard do
74
86
  end
75
87
 
76
88
  describe "force alternative" do
77
- let!(:user) do
78
- Split::User.new(@app, { experiment.name => 'a' })
79
- end
89
+ context "initial version" do
90
+ let!(:user) do
91
+ Split::User.new(@app, { experiment.name => 'red' })
92
+ end
80
93
 
81
- before do
82
- allow(Split::User).to receive(:new).and_return(user)
94
+ before do
95
+ allow(Split::User).to receive(:new).and_return(user)
96
+ end
97
+
98
+ it "should set current user's alternative" do
99
+ blue_link.participant_count = 7
100
+ post "/force_alternative?experiment=#{experiment.name}", alternative: "blue"
101
+
102
+ get "/my_experiment?experiment=#{experiment.name}"
103
+ expect(last_response.body).to include("blue")
104
+ end
105
+
106
+ it "should not modify an existing user" do
107
+ blue_link.participant_count = 7
108
+ post "/force_alternative?experiment=#{experiment.name}", alternative: "blue"
109
+
110
+ expect(user[experiment.key]).to eq("red")
111
+ expect(blue_link.participant_count).to eq(7)
112
+ end
83
113
  end
84
114
 
85
- it "should set current user's alternative" do
86
- post "/force_alternative?experiment=#{experiment.name}", alternative: "b"
87
- expect(user[experiment.name]).to eq("b")
115
+ context "incremented version" do
116
+ let!(:user) do
117
+ experiment.increment_version
118
+ Split::User.new(@app, { "#{experiment.name}:#{experiment.version}" => 'red' })
119
+ end
120
+
121
+ before do
122
+ allow(Split::User).to receive(:new).and_return(user)
123
+ end
124
+
125
+ it "should set current user's alternative" do
126
+ blue_link.participant_count = 7
127
+ post "/force_alternative?experiment=#{experiment.name}", alternative: "blue"
128
+
129
+ get "/my_experiment?experiment=#{experiment.name}"
130
+ expect(last_response.body).to include("blue")
131
+ end
88
132
  end
89
133
  end
90
134
 
@@ -120,7 +164,7 @@ describe Split::Dashboard do
120
164
  it "removes winner" do
121
165
  post "/reopen?experiment=#{experiment.name}"
122
166
 
123
- expect(experiment).to_not have_winner
167
+ expect(Split::ExperimentCatalog.find(experiment.name)).to_not have_winner
124
168
  end
125
169
 
126
170
  it "keeps existing stats" do
@@ -135,6 +179,28 @@ describe Split::Dashboard do
135
179
  end
136
180
  end
137
181
 
182
+ describe "update cohorting" do
183
+ it "calls enable of cohorting when action is enable" do
184
+ post "/update_cohorting?experiment=#{experiment.name}", { "cohorting_action": "enable" }
185
+
186
+ expect(experiment.cohorting_disabled?).to eq false
187
+ end
188
+
189
+ it "calls disable of cohorting when action is disable" do
190
+ post "/update_cohorting?experiment=#{experiment.name}", { "cohorting_action": "disable" }
191
+
192
+ expect(experiment.cohorting_disabled?).to eq true
193
+ end
194
+
195
+ it "calls neither enable or disable cohorting when passed invalid action" do
196
+ previous_value = experiment.cohorting_disabled?
197
+
198
+ post "/update_cohorting?experiment=#{experiment.name}", { "cohorting_action": "other" }
199
+
200
+ expect(experiment.cohorting_disabled?).to eq previous_value
201
+ end
202
+ end
203
+
138
204
  it "should reset an experiment" do
139
205
  red_link.participant_count = 5
140
206
  blue_link.participant_count = 7
@@ -167,19 +233,14 @@ describe Split::Dashboard do
167
233
  end
168
234
 
169
235
  it "should display the start date" do
170
- experiment_start_time = Time.parse('2011-07-07')
171
- expect(Time).to receive(:now).at_least(:once).and_return(experiment_start_time)
172
- experiment
236
+ experiment.start
173
237
 
174
238
  get '/'
175
239
 
176
- expect(last_response.body).to include('<small>2011-07-07</small>')
240
+ expect(last_response.body).to include("<small>#{experiment.start_time.strftime('%Y-%m-%d')}</small>")
177
241
  end
178
242
 
179
243
  it "should handle experiments without a start date" do
180
- experiment_start_time = Time.parse('2011-07-07')
181
- expect(Time).to receive(:now).at_least(:once).and_return(experiment_start_time)
182
-
183
244
  Split.redis.hdel(:experiment_start_times, experiment.name)
184
245
 
185
246
  get '/'
@@ -21,7 +21,7 @@ describe Split::EncapsulatedHelper do
21
21
  end
22
22
 
23
23
  it "calls the block with selected alternative" do
24
- expect{|block| ab_test('link_color', 'red', 'red', &block) }.to yield_with_args('red', nil)
24
+ expect{|block| ab_test('link_color', 'red', 'red', &block) }.to yield_with_args('red', {})
25
25
  end
26
26
 
27
27
  context "inside a view" do
@@ -33,7 +33,7 @@ describe Split::EncapsulatedHelper do
33
33
  static <%= alt %>
34
34
  <% end %>
35
35
  ERB
36
- expect(template.result(binding)).to match /foo static \d/
36
+ expect(template.result(binding)).to match(/foo static \d/)
37
37
  end
38
38
 
39
39
  end
@@ -37,7 +37,7 @@ describe Split::Experiment do
37
37
 
38
38
  it "should save to redis" do
39
39
  experiment.save
40
- expect(Split.redis.exists('basket_text')).to be true
40
+ expect(Split.redis.exists?('basket_text')).to be true
41
41
  end
42
42
 
43
43
  it "should save the start time to redis" do
@@ -85,8 +85,8 @@ describe Split::Experiment do
85
85
  it "should not create duplicates when saving multiple times" do
86
86
  experiment.save
87
87
  experiment.save
88
- expect(Split.redis.exists('basket_text')).to be true
89
- expect(Split.redis.lrange('basket_text', 0, -1)).to eq(['Basket', "Cart"])
88
+ expect(Split.redis.exists?('basket_text')).to be true
89
+ expect(Split.redis.lrange('basket_text', 0, -1)).to eq(['{"Basket":1}', '{"Cart":1}'])
90
90
  end
91
91
 
92
92
  describe 'new record?' do
@@ -118,6 +118,23 @@ describe Split::Experiment do
118
118
  experiment = Split::Experiment.new('basket_text', :alternatives => ['Basket', "Cart"], :resettable => false)
119
119
  expect(experiment.resettable).to be_falsey
120
120
  end
121
+
122
+ context 'from configuration' do
123
+ let(:experiment_name) { :my_experiment }
124
+ let(:experiments) do
125
+ {
126
+ experiment_name => {
127
+ :alternatives => ['Control Opt', 'Alt one']
128
+ }
129
+ }
130
+ end
131
+
132
+ before { Split.configuration.experiments = experiments }
133
+
134
+ it 'assigns default values to the experiment' do
135
+ expect(Split::Experiment.new(experiment_name).resettable).to eq(true)
136
+ end
137
+ end
121
138
  end
122
139
 
123
140
  describe 'persistent configuration' do
@@ -134,10 +151,23 @@ describe Split::Experiment do
134
151
 
135
152
  describe '#metadata' do
136
153
  let(:experiment) { Split::Experiment.new('basket_text', :alternatives => ['Basket', "Cart"], :algorithm => Split::Algorithms::Whiplash, :metadata => meta) }
154
+ let(:meta) { { a: 'b' }}
155
+
156
+ before do
157
+ experiment.save
158
+ end
159
+
160
+ it "should delete the key when metadata is removed" do
161
+ experiment.metadata = nil
162
+ experiment.save
163
+
164
+ expect(Split.redis.exists?(experiment.metadata_key)).to be_falsey
165
+ end
166
+
137
167
  context 'simple hash' do
138
168
  let(:meta) { { 'basket' => 'a', 'cart' => 'b' } }
169
+
139
170
  it "should persist metadata in redis" do
140
- experiment.save
141
171
  e = Split::ExperimentCatalog.find('basket_text')
142
172
  expect(e).to eq(experiment)
143
173
  expect(e.metadata).to eq(meta)
@@ -147,7 +177,6 @@ describe Split::Experiment do
147
177
  context 'nested hash' do
148
178
  let(:meta) { { 'basket' => { 'one' => 'two' }, 'cart' => 'b' } }
149
179
  it "should persist metadata in redis" do
150
- experiment.save
151
180
  e = Split::ExperimentCatalog.find('basket_text')
152
181
  expect(e).to eq(experiment)
153
182
  expect(e.metadata).to eq(meta)
@@ -180,7 +209,7 @@ describe Split::Experiment do
180
209
  experiment.save
181
210
 
182
211
  experiment.delete
183
- expect(Split.redis.exists('link_color')).to be false
212
+ expect(Split.redis.exists?('link_color')).to be false
184
213
  expect(Split::ExperimentCatalog.find('link_color')).to be_nil
185
214
  end
186
215
 
@@ -206,19 +235,59 @@ describe Split::Experiment do
206
235
  experiment.delete
207
236
  expect(experiment.start_time).to be_nil
208
237
  end
209
- end
210
238
 
239
+ it "should default cohorting back to false" do
240
+ experiment.disable_cohorting
241
+ expect(experiment.cohorting_disabled?).to eq(true)
242
+ experiment.delete
243
+ expect(experiment.cohorting_disabled?).to eq(false)
244
+ end
245
+ end
211
246
 
212
247
  describe 'winner' do
213
248
  it "should have no winner initially" do
214
249
  expect(experiment.winner).to be_nil
215
250
  end
251
+ end
216
252
 
217
- it "should allow you to specify a winner" do
253
+ describe 'winner=' do
254
+ it 'should allow you to specify a winner' do
218
255
  experiment.save
219
256
  experiment.winner = 'red'
220
257
  expect(experiment.winner.name).to eq('red')
221
258
  end
259
+
260
+ it 'should call the on_experiment_winner_choose hook' do
261
+ expect(Split.configuration.on_experiment_winner_choose).to receive(:call)
262
+ experiment.winner = 'green'
263
+ end
264
+
265
+ context 'when has_winner state is memoized' do
266
+ before { expect(experiment).to_not have_winner }
267
+
268
+ it 'should keep has_winner state consistent' do
269
+ experiment.winner = 'red'
270
+ expect(experiment).to have_winner
271
+ end
272
+ end
273
+ end
274
+
275
+ describe 'reset_winner' do
276
+ before { experiment.winner = 'green' }
277
+
278
+ it 'should reset the winner' do
279
+ experiment.reset_winner
280
+ expect(experiment.winner).to be_nil
281
+ end
282
+
283
+ context 'when has_winner state is memoized' do
284
+ before { expect(experiment).to have_winner }
285
+
286
+ it 'should keep has_winner state consistent' do
287
+ experiment.reset_winner
288
+ expect(experiment).to_not have_winner
289
+ end
290
+ end
222
291
  end
223
292
 
224
293
  describe 'has_winner?' do
@@ -235,6 +304,12 @@ describe Split::Experiment do
235
304
  expect(experiment).to_not have_winner
236
305
  end
237
306
  end
307
+
308
+ it 'memoizes has_winner state' do
309
+ expect(experiment).to receive(:winner).once
310
+ expect(experiment).to_not have_winner
311
+ expect(experiment).to_not have_winner
312
+ end
238
313
  end
239
314
 
240
315
  describe 'reset' do
@@ -335,6 +410,22 @@ describe Split::Experiment do
335
410
  end
336
411
  end
337
412
 
413
+ describe "#cohorting_disabled?" do
414
+ it "returns false when nothing has been configured" do
415
+ expect(experiment.cohorting_disabled?).to eq false
416
+ end
417
+
418
+ it "returns true when enable_cohorting is performed" do
419
+ experiment.enable_cohorting
420
+ expect(experiment.cohorting_disabled?).to eq false
421
+ end
422
+
423
+ it "returns false when nothing has been configured" do
424
+ experiment.disable_cohorting
425
+ expect(experiment.cohorting_disabled?).to eq true
426
+ end
427
+ end
428
+
338
429
  describe 'changing an existing experiment' do
339
430
  def same_but_different_alternative
340
431
  Split::ExperimentCatalog.find_or_create('link_color', 'blue', 'yellow', 'orange')
@@ -357,6 +448,21 @@ describe Split::Experiment do
357
448
  expect(same_experiment_again.version).to eq(1)
358
449
  end
359
450
 
451
+ context "when metadata is changed" do
452
+ it "should increase version" do
453
+ experiment.save
454
+ experiment.metadata = { 'foo' => 'bar' }
455
+
456
+ expect { experiment.save }.to change { experiment.version }.by(1)
457
+ end
458
+
459
+ it "does not increase version" do
460
+ experiment.metadata = nil
461
+ experiment.save
462
+ expect { experiment.save }.to change { experiment.version }.by(0)
463
+ end
464
+ end
465
+
360
466
  context 'when experiment configuration is changed' do
361
467
  let(:reset_manually) { false }
362
468
 
@@ -414,9 +520,7 @@ describe Split::Experiment do
414
520
  }
415
521
 
416
522
  context "saving experiment" do
417
- def same_but_different_goals
418
- Split::ExperimentCatalog.find_or_create({'link_color' => ["purchase", "refund"]}, 'blue', 'red', 'green')
419
- end
523
+ let(:same_but_different_goals) { Split::ExperimentCatalog.find_or_create({'link_color' => ["purchase", "refund"]}, 'blue', 'red', 'green') }
420
524
 
421
525
  before { experiment.save }
422
526
 
@@ -425,7 +529,7 @@ describe Split::Experiment do
425
529
  end
426
530
 
427
531
  it "should reset an experiment if it is loaded with different goals" do
428
- same_experiment = same_but_different_goals
532
+ same_but_different_goals
429
533
  expect(Split::ExperimentCatalog.find("link_color").goals).to eq(["purchase", "refund"])
430
534
  end
431
535
 
@@ -48,7 +48,7 @@ describe Split::GoalsCollection do
48
48
  goals_collection.save
49
49
 
50
50
  goals_collection.delete
51
- expect(Split.redis.exists(goals_key)).to be false
51
+ expect(Split.redis.exists?(goals_key)).to be false
52
52
  end
53
53
  end
54
54
 
@@ -183,8 +183,7 @@ describe Split::Helper do
183
183
  ab_test('link_color', {'blue' => 0.01}, 'red' => 0.2)
184
184
  experiment = Split::ExperimentCatalog.find('link_color')
185
185
  expect(experiment.alternatives.map(&:name)).to eq(['blue', 'red'])
186
- # TODO: persist alternative weights
187
- # expect(experiment.alternatives.collect{|a| a.weight}).to eq([0.01, 0.2])
186
+ expect(experiment.alternatives.collect{|a| a.weight}).to match_array([0.01, 0.2])
188
187
  end
189
188
 
190
189
  it "should only let a user participate in one experiment at a time" do
@@ -230,13 +229,15 @@ describe Split::Helper do
230
229
 
231
230
  context "when user already has experiment" do
232
231
  let(:mock_user){ Split::User.new(self, {'test_0' => 'test-alt'}) }
233
- before{
232
+
233
+ before do
234
234
  Split.configure do |config|
235
235
  config.allow_multiple_experiments = 'control'
236
236
  end
237
+
237
238
  Split::ExperimentCatalog.find_or_initialize('test_0', 'control', 'test-alt').save
238
239
  Split::ExperimentCatalog.find_or_initialize('test_1', 'control', 'test-alt').save
239
- }
240
+ end
240
241
 
241
242
  it "should restore previously selected alternative" do
242
243
  expect(ab_user.active_experiments.size).to eq 1
@@ -244,6 +245,16 @@ describe Split::Helper do
244
245
  expect(ab_test(:test_0, {'control' => 1}, {"test-alt" => 100})).to eq 'test-alt'
245
246
  end
246
247
 
248
+ it "should select the correct alternatives after experiment resets" do
249
+ experiment = Split::ExperimentCatalog.find(:test_0)
250
+ experiment.reset
251
+ mock_user[experiment.key] = 'test-alt'
252
+
253
+ expect(ab_user.active_experiments.size).to eq 1
254
+ expect(ab_test(:test_0, {'control' => 100}, {"test-alt" => 1})).to eq 'test-alt'
255
+ expect(ab_test(:test_0, {'control' => 0}, {"test-alt" => 100})).to eq 'test-alt'
256
+ end
257
+
247
258
  it "lets override existing choice" do
248
259
  pending "this requires user store reset on first call not depending on whelther it is current trial"
249
260
  @params = { 'ab_test' => { 'test_1' => 'test-alt' } }
@@ -266,129 +277,187 @@ describe Split::Helper do
266
277
  end
267
278
 
268
279
  describe 'metadata' do
269
- before do
270
- Split.configuration.experiments = {
271
- :my_experiment => {
272
- :alternatives => ["one", "two"],
273
- :resettable => false,
274
- :metadata => { 'one' => 'Meta1', 'two' => 'Meta2' }
280
+ context 'is defined' do
281
+ before do
282
+ Split.configuration.experiments = {
283
+ :my_experiment => {
284
+ :alternatives => ["one", "two"],
285
+ :resettable => false,
286
+ :metadata => { 'one' => 'Meta1', 'two' => 'Meta2' }
287
+ }
275
288
  }
276
- }
277
- end
289
+ end
278
290
 
279
- it 'should be passed to helper block' do
280
- @params = { 'ab_test' => { 'my_experiment' => 'one' } }
281
- expect(ab_test('my_experiment')).to eq 'one'
282
- expect(ab_test('my_experiment') do |alternative, meta|
283
- meta
284
- end).to eq('Meta1')
291
+ it 'should be passed to helper block' do
292
+ @params = { 'ab_test' => { 'my_experiment' => 'two' } }
293
+ expect(ab_test('my_experiment')).to eq 'two'
294
+ expect(ab_test('my_experiment') do |alternative, meta|
295
+ meta
296
+ end).to eq('Meta2')
297
+ end
298
+
299
+ it 'should pass control metadata helper block if library disabled' do
300
+ Split.configure do |config|
301
+ config.enabled = false
302
+ end
303
+
304
+ expect(ab_test('my_experiment')).to eq 'one'
305
+ expect(ab_test('my_experiment') do |_, meta|
306
+ meta
307
+ end).to eq('Meta1')
308
+ end
285
309
  end
286
310
 
287
- it 'should pass empty hash to helper block if library disabled' do
288
- Split.configure do |config|
289
- config.enabled = false
311
+ context 'is not defined' do
312
+ before do
313
+ Split.configuration.experiments = {
314
+ :my_experiment => {
315
+ :alternatives => ["one", "two"],
316
+ :resettable => false,
317
+ :metadata => nil
318
+ }
319
+ }
290
320
  end
291
321
 
292
- expect(ab_test('my_experiment')).to eq 'one'
293
- expect(ab_test('my_experiment') do |_, meta|
294
- meta
295
- end).to eq({})
322
+ it 'should be passed to helper block' do
323
+ expect(ab_test('my_experiment') do |alternative, meta|
324
+ meta
325
+ end).to eq({})
326
+ end
327
+
328
+ it 'should pass control metadata helper block if library disabled' do
329
+ Split.configure do |config|
330
+ config.enabled = false
331
+ end
332
+
333
+ expect(ab_test('my_experiment') do |_, meta|
334
+ meta
335
+ end).to eq({})
336
+ end
296
337
  end
297
338
  end
298
339
 
299
340
  describe 'ab_finished' 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
341
+ context 'for an experiment that the user participates in' do
342
+ before(:each) do
343
+ @experiment_name = 'link_color'
344
+ @alternatives = ['blue', 'red']
345
+ @experiment = Split::ExperimentCatalog.find_or_create(@experiment_name, *@alternatives)
346
+ @alternative_name = ab_test(@experiment_name, *@alternatives)
347
+ @previous_completion_count = Split::Alternative.new(@alternative_name, @experiment_name).completed_count
348
+ end
307
349
 
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
350
+ it 'should increment the counter for the completed alternative' do
351
+ ab_finished(@experiment_name)
352
+ new_completion_count = Split::Alternative.new(@alternative_name, @experiment_name).completed_count
353
+ expect(new_completion_count).to eq(@previous_completion_count + 1)
354
+ end
313
355
 
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
356
+ it "should set experiment's finished key if reset is false" do
357
+ ab_finished(@experiment_name, {:reset => false})
358
+ expect(ab_user[@experiment.key]).to eq(@alternative_name)
359
+ expect(ab_user[@experiment.finished_key]).to eq(true)
360
+ end
319
361
 
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
362
+ it 'should not increment the counter if reset is false and the experiment has been already finished' do
363
+ 2.times { ab_finished(@experiment_name, {:reset => false}) }
364
+ new_completion_count = Split::Alternative.new(@alternative_name, @experiment_name).completed_count
365
+ expect(new_completion_count).to eq(@previous_completion_count + 1)
366
+ end
325
367
 
326
- it 'should not increment the counter for an experiment that the user is not participating in' do
327
- ab_test('button_size', 'small', 'big')
368
+ it 'should not increment the counter for an ended experiment' do
369
+ e = Split::ExperimentCatalog.find_or_create('button_size', 'small', 'big')
370
+ e.winner = 'small'
371
+ a = ab_test('button_size', 'small', 'big')
372
+ expect(a).to eq('small')
373
+ expect(lambda {
374
+ ab_finished('button_size')
375
+ }).not_to change { Split::Alternative.new(a, 'button_size').completed_count }
376
+ end
328
377
 
329
- # So, user should be participating in the link_color experiment and
330
- # receive the control for button_size. As the user is not participating in
331
- # the button size experiment, finishing it should not increase the
332
- # completion count for that alternative.
333
- expect(lambda {
334
- ab_finished('button_size')
335
- }).not_to change { Split::Alternative.new('small', 'button_size').completed_count }
336
- end
378
+ it "should clear out the user's participation from their session" do
379
+ expect(ab_user[@experiment.key]).to eq(@alternative_name)
380
+ ab_finished(@experiment_name)
381
+ expect(ab_user.keys).to be_empty
382
+ end
337
383
 
338
- it 'should not increment the counter for an ended experiment' do
339
- e = Split::ExperimentCatalog.find_or_create('button_size', 'small', 'big')
340
- e.winner = 'small'
341
- a = ab_test('button_size', 'small', 'big')
342
- expect(a).to eq('small')
343
- expect(lambda {
344
- ab_finished('button_size')
345
- }).not_to change { Split::Alternative.new(a, 'button_size').completed_count }
346
- end
384
+ it "should not clear out the users session if reset is false" do
385
+ expect(ab_user[@experiment.key]).to eq(@alternative_name)
386
+ ab_finished(@experiment_name, {:reset => false})
387
+ expect(ab_user[@experiment.key]).to eq(@alternative_name)
388
+ expect(ab_user[@experiment.finished_key]).to eq(true)
389
+ end
347
390
 
348
- it "should clear out the user's participation from their session" do
349
- expect(ab_user[@experiment.key]).to eq(@alternative_name)
350
- ab_finished(@experiment_name)
351
- expect(ab_user.keys).to be_empty
352
- end
391
+ it "should reset the users session when experiment is not versioned" do
392
+ expect(ab_user[@experiment.key]).to eq(@alternative_name)
393
+ ab_finished(@experiment_name)
394
+ expect(ab_user.keys).to be_empty
395
+ end
353
396
 
354
- it "should not clear out the users session if reset is false" do
355
- expect(ab_user[@experiment.key]).to eq(@alternative_name)
356
- ab_finished(@experiment_name, {:reset => false})
357
- expect(ab_user[@experiment.key]).to eq(@alternative_name)
358
- expect(ab_user[@experiment.finished_key]).to eq(true)
359
- end
397
+ it "should reset the users session when experiment is versioned" do
398
+ @experiment.increment_version
399
+ @alternative_name = ab_test(@experiment_name, *@alternatives)
360
400
 
361
- it "should reset the users session when experiment is not versioned" do
362
- expect(ab_user[@experiment.key]).to eq(@alternative_name)
363
- ab_finished(@experiment_name)
364
- expect(ab_user.keys).to be_empty
365
- end
401
+ expect(ab_user[@experiment.key]).to eq(@alternative_name)
402
+ ab_finished(@experiment_name)
403
+ expect(ab_user.keys).to be_empty
404
+ end
366
405
 
367
- it "should reset the users session when experiment is versioned" do
368
- @experiment.increment_version
369
- @alternative_name = ab_test(@experiment_name, *@alternatives)
406
+ context "when on_trial_complete is set" do
407
+ before { Split.configuration.on_trial_complete = :some_method }
408
+ it "should call the method" do
409
+ expect(self).to receive(:some_method)
410
+ ab_finished(@experiment_name)
411
+ end
370
412
 
371
- expect(ab_user[@experiment.key]).to eq(@alternative_name)
372
- ab_finished(@experiment_name)
373
- expect(ab_user.keys).to be_empty
413
+ it "should not call the method without alternative" do
414
+ ab_user[@experiment.key] = nil
415
+ expect(self).not_to receive(:some_method)
416
+ ab_finished(@experiment_name)
417
+ end
418
+ end
374
419
  end
375
420
 
376
- it "should do nothing where the experiment was not started by this user" do
377
- ab_user = nil
378
- expect(lambda { ab_finished('some_experiment_not_started_by_the_user') }).not_to raise_exception
421
+ context 'for an experiment that the user is excluded from' do
422
+ before do
423
+ alternative = ab_test('link_color', 'blue', 'red')
424
+ expect(Split::Alternative.new(alternative, 'link_color').participant_count).to eq(1)
425
+ alternative = ab_test('button_size', 'small', 'big')
426
+ expect(Split::Alternative.new(alternative, 'button_size').participant_count).to eq(0)
427
+ end
428
+
429
+ it 'should not increment the completed counter' do
430
+ # So, user should be participating in the link_color experiment and
431
+ # receive the control for button_size. As the user is not participating in
432
+ # the button size experiment, finishing it should not increase the
433
+ # completion count for that alternative.
434
+ expect(lambda {
435
+ ab_finished('button_size')
436
+ }).not_to change { Split::Alternative.new('small', 'button_size').completed_count }
437
+ end
379
438
  end
380
439
 
381
- context "when on_trial_complete is set" do
382
- before { Split.configuration.on_trial_complete = :some_method }
383
- it "should call the method" do
384
- expect(self).to receive(:some_method)
385
- ab_finished(@experiment_name)
440
+ context 'for an experiment that the user does not participate in' do
441
+ before do
442
+ Split::ExperimentCatalog.find_or_create(:not_started_experiment, 'control', 'alt')
443
+ end
444
+ it 'should not raise an exception' do
445
+ expect { ab_finished(:not_started_experiment) }.not_to raise_exception
386
446
  end
387
447
 
388
- it "should not call the method without alternative" do
389
- ab_user[@experiment.key] = nil
390
- expect(self).not_to receive(:some_method)
391
- ab_finished(@experiment_name)
448
+ it 'should not change the user state when reset is false' do
449
+ expect { ab_finished(:not_started_experiment, reset: false) }.not_to change { ab_user.keys}.from([])
450
+ end
451
+
452
+ it 'should not change the user state when reset is true' do
453
+ expect(self).not_to receive(:reset!)
454
+ ab_finished(:not_started_experiment)
455
+ end
456
+
457
+ it 'should not increment the completed counter' do
458
+ ab_finished(:not_started_experiment)
459
+ expect(Split::Alternative.new('control', :not_started_experiment).completed_count).to eq(0)
460
+ expect(Split::Alternative.new('alt', :not_started_experiment).completed_count).to eq(0)
392
461
  end
393
462
  end
394
463
  end
@@ -528,6 +597,17 @@ describe Split::Helper do
528
597
  expect(active_experiments.first[0]).to eq "link_color"
529
598
  end
530
599
 
600
+ it 'should show versioned tests properly' do
601
+ 10.times { experiment.reset }
602
+
603
+ alternative = ab_test(experiment.name, 'blue', 'red')
604
+ ab_finished(experiment.name, reset: false)
605
+
606
+ expect(experiment.version).to eq(10)
607
+ expect(active_experiments.count).to eq 1
608
+ expect(active_experiments).to eq({'link_color' => alternative })
609
+ end
610
+
531
611
  it 'should show multiple tests' do
532
612
  Split.configure do |config|
533
613
  config.allow_multiple_experiments = true
@@ -545,7 +625,7 @@ describe Split::Helper do
545
625
  end
546
626
  e = Split::ExperimentCatalog.find_or_create('def', '4', '5', '6')
547
627
  e.winner = '4'
548
- alternative = ab_test('def', '4', '5', '6')
628
+ ab_test('def', '4', '5', '6')
549
629
  another_alternative = ab_test('ghi', '7', '8', '9')
550
630
  expect(active_experiments.count).to eq 1
551
631
  expect(active_experiments.first[0]).to eq "ghi"
@@ -564,6 +644,11 @@ describe Split::Helper do
564
644
  expect(alternative).to eq experiment.control.name
565
645
  end
566
646
 
647
+ it 'should not create a experiment' do
648
+ ab_test('link_color', 'blue', 'red')
649
+ expect(Split::Experiment.new('link_color')).to be_a_new_record
650
+ end
651
+
567
652
  it "should not increment the participation count" do
568
653
 
569
654
  previous_red_count = Split::Alternative.new('red', 'link_color').participant_count
@@ -1016,8 +1101,8 @@ describe Split::Helper do
1016
1101
 
1017
1102
  it 'should handle multiple experiments correctly' do
1018
1103
  experiment2 = Split::ExperimentCatalog.find_or_create('link_color2', 'blue', 'red')
1019
- alternative_name = ab_test('link_color', 'blue', 'red')
1020
- alternative_name2 = ab_test('link_color2', 'blue', 'red')
1104
+ ab_test('link_color', 'blue', 'red')
1105
+ ab_test('link_color2', 'blue', 'red')
1021
1106
  ab_finished('link_color2')
1022
1107
 
1023
1108
  experiment2.alternatives.each do |alt|
@@ -1053,15 +1138,9 @@ describe Split::Helper do
1053
1138
  end
1054
1139
 
1055
1140
  it "should increment the counter for the specified-goal completed alternative" do
1056
- expect(lambda {
1057
- expect(lambda {
1058
- ab_finished({"link_color" => ["purchase"]})
1059
- }).not_to change {
1060
- Split::Alternative.new(@alternative_name, @experiment_name).completed_count(@goal2)
1061
- }
1062
- }).to change {
1063
- Split::Alternative.new(@alternative_name, @experiment_name).completed_count(@goal1)
1064
- }.by(1)
1141
+ expect{ ab_finished({"link_color" => ["purchase"]}) }
1142
+ .to change{ Split::Alternative.new(@alternative_name, @experiment_name).completed_count(@goal2) }.by(0)
1143
+ .and change{ Split::Alternative.new(@alternative_name, @experiment_name).completed_count(@goal1) }.by(1)
1065
1144
  end
1066
1145
  end
1067
1146
  end