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.
- checksums.yaml +7 -0
- data/.codeclimate.yml +30 -0
- data/.csslintrc +2 -0
- data/.eslintignore +1 -0
- data/.eslintrc +213 -0
- data/.github/FUNDING.yml +1 -0
- data/.github/ISSUE_TEMPLATE/bug_report.md +24 -0
- data/.rspec +1 -0
- data/.rubocop.yml +7 -0
- data/.rubocop_todo.yml +679 -0
- data/.travis.yml +60 -0
- data/Appraisals +19 -0
- data/CHANGELOG.md +696 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/CONTRIBUTING.md +62 -0
- data/Gemfile +7 -0
- data/LICENSE +22 -0
- data/README.md +955 -0
- data/Rakefile +9 -0
- data/ab-split.gemspec +44 -0
- data/gemfiles/4.2.gemfile +9 -0
- data/gemfiles/5.0.gemfile +9 -0
- data/gemfiles/5.1.gemfile +9 -0
- data/gemfiles/5.2.gemfile +9 -0
- data/gemfiles/6.0.gemfile +9 -0
- data/lib/split.rb +76 -0
- data/lib/split/algorithms/block_randomization.rb +23 -0
- data/lib/split/algorithms/weighted_sample.rb +18 -0
- data/lib/split/algorithms/whiplash.rb +38 -0
- data/lib/split/alternative.rb +191 -0
- data/lib/split/combined_experiments_helper.rb +37 -0
- data/lib/split/configuration.rb +255 -0
- data/lib/split/dashboard.rb +74 -0
- data/lib/split/dashboard/helpers.rb +45 -0
- data/lib/split/dashboard/pagination_helpers.rb +86 -0
- data/lib/split/dashboard/paginator.rb +16 -0
- data/lib/split/dashboard/public/dashboard-filtering.js +43 -0
- data/lib/split/dashboard/public/dashboard.js +24 -0
- data/lib/split/dashboard/public/jquery-1.11.1.min.js +4 -0
- data/lib/split/dashboard/public/reset.css +48 -0
- data/lib/split/dashboard/public/style.css +328 -0
- data/lib/split/dashboard/views/_controls.erb +18 -0
- data/lib/split/dashboard/views/_experiment.erb +155 -0
- data/lib/split/dashboard/views/_experiment_with_goal_header.erb +8 -0
- data/lib/split/dashboard/views/index.erb +26 -0
- data/lib/split/dashboard/views/layout.erb +27 -0
- data/lib/split/encapsulated_helper.rb +42 -0
- data/lib/split/engine.rb +15 -0
- data/lib/split/exceptions.rb +6 -0
- data/lib/split/experiment.rb +486 -0
- data/lib/split/experiment_catalog.rb +51 -0
- data/lib/split/extensions/string.rb +16 -0
- data/lib/split/goals_collection.rb +45 -0
- data/lib/split/helper.rb +165 -0
- data/lib/split/metric.rb +101 -0
- data/lib/split/persistence.rb +28 -0
- data/lib/split/persistence/cookie_adapter.rb +94 -0
- data/lib/split/persistence/dual_adapter.rb +85 -0
- data/lib/split/persistence/redis_adapter.rb +57 -0
- data/lib/split/persistence/session_adapter.rb +29 -0
- data/lib/split/redis_interface.rb +50 -0
- data/lib/split/trial.rb +117 -0
- data/lib/split/user.rb +69 -0
- data/lib/split/version.rb +7 -0
- data/lib/split/zscore.rb +57 -0
- data/spec/algorithms/block_randomization_spec.rb +32 -0
- data/spec/algorithms/weighted_sample_spec.rb +19 -0
- data/spec/algorithms/whiplash_spec.rb +24 -0
- data/spec/alternative_spec.rb +320 -0
- data/spec/combined_experiments_helper_spec.rb +57 -0
- data/spec/configuration_spec.rb +258 -0
- data/spec/dashboard/pagination_helpers_spec.rb +200 -0
- data/spec/dashboard/paginator_spec.rb +37 -0
- data/spec/dashboard_helpers_spec.rb +42 -0
- data/spec/dashboard_spec.rb +210 -0
- data/spec/encapsulated_helper_spec.rb +52 -0
- data/spec/experiment_catalog_spec.rb +53 -0
- data/spec/experiment_spec.rb +533 -0
- data/spec/goals_collection_spec.rb +80 -0
- data/spec/helper_spec.rb +1111 -0
- data/spec/metric_spec.rb +31 -0
- data/spec/persistence/cookie_adapter_spec.rb +106 -0
- data/spec/persistence/dual_adapter_spec.rb +194 -0
- data/spec/persistence/redis_adapter_spec.rb +90 -0
- data/spec/persistence/session_adapter_spec.rb +32 -0
- data/spec/persistence_spec.rb +34 -0
- data/spec/redis_interface_spec.rb +111 -0
- data/spec/spec_helper.rb +52 -0
- data/spec/split_spec.rb +43 -0
- data/spec/support/cookies_mock.rb +20 -0
- data/spec/trial_spec.rb +299 -0
- data/spec/user_spec.rb +87 -0
- 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
|
data/spec/helper_spec.rb
ADDED
@@ -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
|