split 0.5.0 → 0.6.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.
@@ -27,9 +27,25 @@ describe Split::Configuration do
27
27
  end
28
28
 
29
29
  it "should provide a default pattern for robots" do
30
- %w[Baidu Gigabot Googlebot libwww-perl lwp-trivial msnbot SiteUptime Slurp WordPress ZIBB ZyBorg].each do |robot|
30
+ %w[Baidu Gigabot Googlebot libwww-perl lwp-trivial msnbot SiteUptime Slurp WordPress ZIBB ZyBorg YandexBot AdsBot-Google Wget curl bitlybot facebookexternalhit spider].each do |robot|
31
31
  @config.robot_regex.should =~ robot
32
32
  end
33
+
34
+ @config.robot_regex.should =~ "EventMachine HttpClient"
35
+ @config.robot_regex.should =~ "libwww-perl/5.836"
36
+ @config.robot_regex.should =~ "Pingdom.com_bot_version_1.4_(http://www.pingdom.com)"
37
+
38
+ @config.robot_regex.should =~ " - "
39
+ end
40
+
41
+ it "should accept real UAs with the robot regexp" do
42
+ @config.robot_regex.should_not =~ "Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US; rv:1.9.1.4) Gecko/20091017 SeaMonkey/2.0"
43
+ @config.robot_regex.should_not =~ "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0; F-6.0SP2-20041109; .NET CLR 2.0.50727; .NET CLR 3.0.04506.648; .NET CLR 3.5.21022; .NET CLR 1.1.4322; InfoPath.3)"
44
+ end
45
+
46
+ it "should allow adding a bot to the bot list" do
47
+ @config.bots["newbot"] = "An amazing test bot"
48
+ @config.robot_regex.should =~ "newbot"
33
49
  end
34
50
 
35
51
  it "should use the session adapter for persistence by default" do
@@ -43,13 +59,85 @@ describe Split::Configuration do
43
59
  @config.metrics.should_not be_nil
44
60
  @config.metrics.keys.should == [:my_metric]
45
61
  end
46
-
62
+
47
63
  it "should allow loading of experiment using experment_for" do
48
64
  @config.experiments = {:my_experiment=>
49
65
  {:alternatives=>["control_opt", "other_opt"], :metric=>:my_metric}}
50
66
  @config.experiment_for(:my_experiment).should == {:alternatives=>["control_opt", ["other_opt"]]}
51
67
  end
52
-
68
+
69
+ context "when experiments are defined via YAML" do
70
+ context "as strings" do
71
+ context "in a basic configuration" do
72
+ before do
73
+ experiments_yaml = <<-eos
74
+ my_experiment:
75
+ alternatives:
76
+ - Control Opt
77
+ - Alt One
78
+ - Alt Two
79
+ resettable: false
80
+ eos
81
+ @config.experiments = YAML.load(experiments_yaml)
82
+ end
83
+
84
+ it 'should normalize experiments' do
85
+ @config.normalized_experiments.should == {:my_experiment=>{:alternatives=>["Control Opt", ["Alt One", "Alt Two"]]}}
86
+ end
87
+ end
88
+
89
+ context "in a complex configuration" do
90
+ before do
91
+ experiments_yaml = <<-eos
92
+ my_experiment:
93
+ alternatives:
94
+ - name: Control Opt
95
+ percent: 67
96
+ - name: Alt One
97
+ percent: 10
98
+ - name: Alt Two
99
+ percent: 23
100
+ resettable: false
101
+ metric: my_metric
102
+ another_experiment:
103
+ alternatives:
104
+ - a
105
+ - b
106
+ eos
107
+ @config.experiments = YAML.load(experiments_yaml)
108
+ end
109
+
110
+ it "should normalize experiments" do
111
+ @config.normalized_experiments.should == {:my_experiment=>{:alternatives=>[{"Control Opt"=>0.67},
112
+ [{"Alt One"=>0.1}, {"Alt Two"=>0.23}]]}, :another_experiment=>{:alternatives=>["a", ["b"]]}}
113
+ end
114
+
115
+ it "should recognize metrics" do
116
+ @config.metrics.should_not be_nil
117
+ @config.metrics.keys.should == [:my_metric]
118
+ end
119
+ end
120
+ end
121
+
122
+ context "as symbols" do
123
+ before do
124
+ experiments_yaml = <<-eos
125
+ :my_experiment:
126
+ :alternatives:
127
+ - Control Opt
128
+ - Alt One
129
+ - Alt Two
130
+ :resettable: false
131
+ eos
132
+ @config.experiments = YAML.load(experiments_yaml)
133
+ end
134
+
135
+ it "should normalize experiments" do
136
+ @config.normalized_experiments.should == {:my_experiment=>{:alternatives=>["Control Opt", ["Alt One", "Alt Two"]]}}
137
+ end
138
+ end
139
+ end
140
+
53
141
  it "should normalize experiments" do
54
142
  @config.experiments = {
55
143
  :my_experiment => {
@@ -60,7 +148,7 @@ describe Split::Configuration do
60
148
  ],
61
149
  }
62
150
  }
63
-
151
+
64
152
  @config.normalized_experiments.should == {:my_experiment=>{:alternatives=>[{"control_opt"=>0.67}, [{"second_opt"=>0.1}, {"third_opt"=>0.23}]]}}
65
153
  end
66
154
  end
@@ -5,6 +5,22 @@ require 'split/dashboard'
5
5
  describe Split::Dashboard do
6
6
  include Rack::Test::Methods
7
7
 
8
+ let(:link_color) {
9
+ Split::Experiment.find_or_create('link_color', 'blue', 'red')
10
+ }
11
+
12
+ def link(color)
13
+ Split::Alternative.new(color, 'link_color')
14
+ end
15
+
16
+ let(:red_link) {
17
+ link("red")
18
+ }
19
+
20
+ let(:blue_link) {
21
+ link("blue")
22
+ }
23
+
8
24
  def app
9
25
  @app ||= Split::Dashboard
10
26
  end
@@ -15,33 +31,33 @@ describe Split::Dashboard do
15
31
  end
16
32
 
17
33
  it "should reset an experiment" do
18
- experiment = Split::Experiment.find_or_create('link_color', 'blue', 'red')
34
+ experiment = link_color
19
35
 
20
- red = Split::Alternative.new('red', 'link_color')
21
- blue = Split::Alternative.new('blue', 'link_color')
22
- red.participant_count = 5
23
- blue.participant_count = 6
36
+ red_link.participant_count = 5
37
+ blue_link.participant_count = 7
38
+ experiment.winner = 'blue'
24
39
 
25
40
  post '/reset/link_color'
26
41
 
27
42
  last_response.should be_redirect
28
43
 
29
- new_red_count = Split::Alternative.new('red', 'link_color').participant_count
30
- new_blue_count = Split::Alternative.new('blue', 'link_color').participant_count
44
+ new_red_count = red_link.participant_count
45
+ new_blue_count = blue_link.participant_count
31
46
 
32
47
  new_blue_count.should eql(0)
33
48
  new_red_count.should eql(0)
49
+ experiment.winner.should be_nil
34
50
  end
35
51
 
36
52
  it "should delete an experiment" do
37
- experiment = Split::Experiment.find_or_create('link_color', 'blue', 'red')
53
+ experiment = link_color
38
54
  delete '/link_color'
39
55
  last_response.should be_redirect
40
56
  Split::Experiment.find('link_color').should be_nil
41
57
  end
42
58
 
43
59
  it "should mark an alternative as the winner" do
44
- experiment = Split::Experiment.find_or_create('link_color', 'blue', 'red')
60
+ experiment = link_color
45
61
  experiment.winner.should be_nil
46
62
 
47
63
  post '/link_color', :alternative => 'red'
@@ -53,7 +69,7 @@ describe Split::Dashboard do
53
69
  it "should display the start date" do
54
70
  experiment_start_time = Time.parse('2011-07-07')
55
71
  Time.stub(:now => experiment_start_time)
56
- experiment = Split::Experiment.find_or_create('link_color', 'blue', 'red')
72
+ experiment = link_color
57
73
 
58
74
  get '/'
59
75
 
@@ -63,7 +79,7 @@ describe Split::Dashboard do
63
79
  it "should handle experiments without a start date" do
64
80
  experiment_start_time = Time.parse('2011-07-07')
65
81
  Time.stub(:now => experiment_start_time)
66
- experiment = Split::Experiment.find_or_create('link_color', 'blue', 'red')
82
+ experiment = link_color
67
83
 
68
84
  Split.redis.hdel(:experiment_start_times, experiment.name)
69
85
 
@@ -4,9 +4,24 @@ require 'split/algorithms'
4
4
  require 'time'
5
5
 
6
6
  describe Split::Experiment do
7
+ def new_experiment(goals=[])
8
+ Split::Experiment.new('link_color', :alternatives => ['blue', 'red', 'green'], :goals => goals)
9
+ end
10
+
11
+ def alternative(color)
12
+ Split::Alternative.new(color, 'link_color')
13
+ end
14
+
15
+ let(:experiment) {
16
+ new_experiment
17
+ }
18
+
19
+ let(:blue) { alternative("blue") }
20
+ let(:green) { alternative("green") }
21
+
7
22
  context "with an experiment" do
8
- let(:experiment) { Split::Experiment.new('basket_text', :alternative_names => ['Basket', "Cart"]) }
9
-
23
+ let(:experiment) { Split::Experiment.new('basket_text', :alternatives => ['Basket', "Cart"]) }
24
+
10
25
  it "should have a name" do
11
26
  experiment.name.should eql('basket_text')
12
27
  end
@@ -14,15 +29,15 @@ describe Split::Experiment do
14
29
  it "should have alternatives" do
15
30
  experiment.alternatives.length.should be 2
16
31
  end
17
-
32
+
18
33
  it "should have alternatives with correct names" do
19
34
  experiment.alternatives.collect{|a| a.name}.should == ['Basket', 'Cart']
20
35
  end
21
-
36
+
22
37
  it "should be resettable by default" do
23
38
  experiment.resettable.should be_true
24
39
  end
25
-
40
+
26
41
  it "should save to redis" do
27
42
  experiment.save
28
43
  Split.redis.exists('basket_text').should be true
@@ -35,10 +50,10 @@ describe Split::Experiment do
35
50
 
36
51
  Split::Experiment.find('basket_text').start_time.should == experiment_start_time
37
52
  end
38
-
53
+
39
54
  it "should save the selected algorithm to redis" do
40
55
  experiment_algorithm = Split::Algorithms::Whiplash
41
- experiment.algorithm = experiment_algorithm
56
+ experiment.algorithm = experiment_algorithm
42
57
  experiment.save
43
58
 
44
59
  Split::Experiment.find('basket_text').algorithm.should == experiment_algorithm
@@ -60,7 +75,7 @@ describe Split::Experiment do
60
75
  Split.redis.exists('basket_text').should be true
61
76
  Split.redis.lrange('basket_text', 0, -1).should eql(['Basket', "Cart"])
62
77
  end
63
-
78
+
64
79
  describe 'new record?' do
65
80
  it "should know if it hasn't been saved yet" do
66
81
  experiment.new_record?.should be_true
@@ -71,18 +86,19 @@ describe Split::Experiment do
71
86
  experiment.new_record?.should be_false
72
87
  end
73
88
  end
74
-
89
+
75
90
  describe 'find' do
76
91
  it "should return an existing experiment" do
77
92
  experiment.save
78
- Split::Experiment.find('basket_text').name.should eql('basket_text')
93
+ experiment = Split::Experiment.find('basket_text')
94
+ experiment.name.should eql('basket_text')
79
95
  end
80
96
 
81
97
  it "should return an existing experiment" do
82
98
  Split::Experiment.find('non_existent_experiment').should be_nil
83
99
  end
84
100
  end
85
-
101
+
86
102
  describe 'control' do
87
103
  it 'should be the first alternative' do
88
104
  experiment.save
@@ -90,80 +106,74 @@ describe Split::Experiment do
90
106
  end
91
107
  end
92
108
  end
109
+
93
110
  describe 'initialization' do
94
111
  it "should set the algorithm when passed as an option to the initializer" do
95
- experiment = Split::Experiment.new('basket_text', :alternative_names => ['Basket', "Cart"], :algorithm => Split::Algorithms::Whiplash)
112
+ experiment = Split::Experiment.new('basket_text', :alternatives => ['Basket', "Cart"], :algorithm => Split::Algorithms::Whiplash)
96
113
  experiment.algorithm.should == Split::Algorithms::Whiplash
97
114
  end
98
-
115
+
99
116
  it "should be possible to make an experiment not resettable" do
100
- experiment = Split::Experiment.new('basket_text', :alternative_names => ['Basket', "Cart"], :resettable => false)
101
- experiment.resettable.should be_false
117
+ experiment = Split::Experiment.new('basket_text', :alternatives => ['Basket', "Cart"], :resettable => false)
118
+ experiment.resettable.should be_false
102
119
  end
103
120
  end
104
-
121
+
105
122
  describe 'persistent configuration' do
106
-
123
+
107
124
  it "should persist resettable in redis" do
108
- experiment = Split::Experiment.new('basket_text', :alternative_names => ['Basket', "Cart"], :resettable => false)
125
+ experiment = Split::Experiment.new('basket_text', :alternatives => ['Basket', "Cart"], :resettable => false)
109
126
  experiment.save
110
-
127
+
111
128
  e = Split::Experiment.find('basket_text')
112
129
  e.should == experiment
113
130
  e.resettable.should be_false
114
-
131
+
115
132
  end
116
-
133
+
117
134
  it "should persist algorithm in redis" do
118
- experiment = Split::Experiment.new('basket_text', :alternative_names => ['Basket', "Cart"], :algorithm => Split::Algorithms::Whiplash)
135
+ experiment = Split::Experiment.new('basket_text', :alternatives => ['Basket', "Cart"], :algorithm => Split::Algorithms::Whiplash)
119
136
  experiment.save
120
-
137
+
121
138
  e = Split::Experiment.find('basket_text')
122
139
  e.should == experiment
123
140
  e.algorithm.should == Split::Algorithms::Whiplash
124
141
  end
125
142
  end
126
143
 
127
-
128
-
129
-
130
144
  describe 'deleting' do
131
145
  it 'should delete itself' do
132
- experiment = Split::Experiment.new('basket_text', :alternative_names => [ 'Basket', "Cart"])
146
+ experiment = Split::Experiment.new('basket_text', :alternatives => [ 'Basket', "Cart"])
133
147
  experiment.save
134
148
 
135
149
  experiment.delete
136
- Split.redis.exists('basket_text').should be false
137
- Split::Experiment.find('basket_text').should be_nil
150
+ Split.redis.exists('link_color').should be false
151
+ Split::Experiment.find('link_color').should be_nil
138
152
  end
139
153
 
140
154
  it "should increment the version" do
141
- experiment = Split::Experiment.find_or_create('link_color', 'blue', 'red', 'green')
142
155
  experiment.version.should eql(0)
143
156
  experiment.delete
144
157
  experiment.version.should eql(1)
145
158
  end
146
159
  end
147
160
 
161
+
148
162
  describe 'winner' do
149
163
  it "should have no winner initially" do
150
- experiment = Split::Experiment.find_or_create('link_color', 'blue', 'red')
151
164
  experiment.winner.should be_nil
152
165
  end
153
166
 
154
167
  it "should allow you to specify a winner" do
155
- experiment = Split::Experiment.find_or_create('link_color', 'blue', 'red')
168
+ experiment.save
156
169
  experiment.winner = 'red'
157
-
158
- experiment = Split::Experiment.find_or_create('link_color', 'blue', 'red')
159
170
  experiment.winner.name.should == 'red'
160
171
  end
161
172
  end
162
173
 
163
174
  describe 'reset' do
175
+ before { experiment.save }
164
176
  it 'should reset all alternatives' do
165
- experiment = Split::Experiment.find_or_create('link_color', 'blue', 'red', 'green')
166
- green = Split::Alternative.new('green', 'link_color')
167
177
  experiment.winner = 'green'
168
178
 
169
179
  experiment.next_alternative.name.should eql('green')
@@ -171,14 +181,11 @@ describe Split::Experiment do
171
181
 
172
182
  experiment.reset
173
183
 
174
- reset_green = Split::Alternative.new('green', 'link_color')
175
- reset_green.participant_count.should eql(0)
176
- reset_green.completed_count.should eql(0)
184
+ green.participant_count.should eql(0)
185
+ green.completed_count.should eql(0)
177
186
  end
178
187
 
179
188
  it 'should reset the winner' do
180
- experiment = Split::Experiment.find_or_create('link_color', 'blue', 'red', 'green')
181
- green = Split::Alternative.new('green', 'link_color')
182
189
  experiment.winner = 'green'
183
190
 
184
191
  experiment.next_alternative.name.should eql('green')
@@ -190,29 +197,28 @@ describe Split::Experiment do
190
197
  end
191
198
 
192
199
  it "should increment the version" do
193
- experiment = Split::Experiment.find_or_create('link_color', 'blue', 'red', 'green')
194
200
  experiment.version.should eql(0)
195
201
  experiment.reset
196
202
  experiment.version.should eql(1)
197
203
  end
198
204
  end
199
-
205
+
200
206
  describe 'algorithm' do
201
207
  let(:experiment) { Split::Experiment.find_or_create('link_color', 'blue', 'red', 'green') }
202
-
208
+
203
209
  it 'should use the default algorithm if none is specified' do
204
210
  experiment.algorithm.should == Split.configuration.algorithm
205
211
  end
206
-
212
+
207
213
  it 'should use the user specified algorithm for this experiment if specified' do
208
214
  experiment.algorithm = Split::Algorithms::Whiplash
209
215
  experiment.algorithm.should == Split::Algorithms::Whiplash
210
216
  end
211
217
  end
212
-
218
+
213
219
  describe 'next_alternative' do
214
220
  let(:experiment) { Split::Experiment.find_or_create('link_color', 'blue', 'red', 'green') }
215
-
221
+
216
222
  it "should always return the winner if one exists" do
217
223
  green = Split::Alternative.new('green', 'link_color')
218
224
  experiment.winner = 'green'
@@ -220,42 +226,42 @@ describe Split::Experiment do
220
226
  experiment.next_alternative.name.should eql('green')
221
227
  green.increment_participation
222
228
 
223
- experiment = Split::Experiment.find_or_create('link_color', 'blue', 'red', 'green')
224
229
  experiment.next_alternative.name.should eql('green')
225
230
  end
226
-
231
+
227
232
  it "should use the specified algorithm if a winner does not exist" do
228
233
  Split.configuration.algorithm.should_receive(:choose_alternative).and_return(Split::Alternative.new('green', 'link_color'))
229
234
  experiment.next_alternative.name.should eql('green')
230
235
  end
231
236
  end
232
-
237
+
233
238
  describe 'single alternative' do
234
239
  let(:experiment) { Split::Experiment.find_or_create('link_color', 'blue') }
235
-
240
+
236
241
  it "should always return the color blue" do
237
242
  experiment.next_alternative.name.should eql('blue')
238
- end
243
+ end
239
244
  end
240
245
 
241
246
  describe 'changing an existing experiment' do
247
+ def same_but_different_alternative
248
+ Split::Experiment.find_or_create('link_color', 'blue', 'yellow', 'orange')
249
+ end
250
+
242
251
  it "should reset an experiment if it is loaded with different alternatives" do
243
- experiment = Split::Experiment.find_or_create('link_color', 'blue', 'red', 'green')
244
- blue = Split::Alternative.new('blue', 'link_color')
252
+ experiment.save
245
253
  blue.participant_count = 5
246
- blue.save
247
- same_experiment = Split::Experiment.find_or_create('link_color', 'blue', 'yellow', 'orange')
254
+ same_experiment = same_but_different_alternative
248
255
  same_experiment.alternatives.map(&:name).should eql(['blue', 'yellow', 'orange'])
249
- new_blue = Split::Alternative.new('blue', 'link_color')
250
- new_blue.participant_count.should eql(0)
256
+ blue.participant_count.should eql(0)
251
257
  end
252
258
 
253
259
  it "should only reset once" do
254
- experiment = Split::Experiment.find_or_create('link_color', 'blue', 'red', 'green')
260
+ experiment.save
255
261
  experiment.version.should eql(0)
256
- same_experiment = Split::Experiment.find_or_create('link_color', 'blue', 'yellow', 'orange')
262
+ same_experiment = same_but_different_alternative
257
263
  same_experiment.version.should eql(1)
258
- same_experiment_again = Split::Experiment.find_or_create('link_color', 'blue', 'yellow', 'orange')
264
+ same_experiment_again = same_but_different_alternative
259
265
  same_experiment_again.version.should eql(1)
260
266
  end
261
267
  end
@@ -268,18 +274,55 @@ describe Split::Experiment do
268
274
  end
269
275
 
270
276
  describe 'specifying weights' do
271
- it "should work for a new experiment" do
272
- experiment = Split::Experiment.find_or_create('link_color', {'blue' => 1}, {'red' => 2 })
277
+ let(:experiment_with_weight) {
278
+ Split::Experiment.find_or_create('link_color', {'blue' => 1}, {'red' => 2 })
279
+ }
273
280
 
274
- experiment.alternatives.map(&:weight).should == [1, 2]
281
+ it "should work for a new experiment" do
282
+ experiment_with_weight.alternatives.map(&:weight).should == [1, 2]
275
283
  end
276
284
 
277
285
  it "should work for an existing experiment" do
278
- experiment = Split::Experiment.find_or_create('link_color', 'blue', 'red')
279
286
  experiment.save
287
+ experiment_with_weight.alternatives.map(&:weight).should == [1, 2]
288
+ end
289
+ end
280
290
 
281
- same_experiment = Split::Experiment.find_or_create('link_color', {'blue' => 1}, {'red' => 2 })
282
- same_experiment.alternatives.map(&:weight).should == [1, 2]
291
+ describe "specifying goals" do
292
+ let(:experiment) {
293
+ new_experiment(["purchase"])
294
+ }
295
+
296
+ context "saving experiment" do
297
+ def same_but_different_goals
298
+ Split::Experiment.find_or_create({'link_color' => ["purchase", "refund"]}, 'blue', 'red', 'green')
299
+ end
300
+
301
+ before { experiment.save }
302
+
303
+ it "can find existing experiment" do
304
+ Split::Experiment.find("link_color").name.should eql("link_color")
305
+ end
306
+
307
+ it "should reset an experiment if it is loaded with different goals" do
308
+ same_experiment = same_but_different_goals
309
+ Split::Experiment.find("link_color").goals.should == ["purchase", "refund"]
310
+ end
311
+
312
+ end
313
+
314
+ it "should have goals" do
315
+ experiment.goals.should eql(["purchase"])
316
+ end
317
+
318
+ context "find or create experiment" do
319
+ it "should have correct goals" do
320
+ experiment = Split::Experiment.find_or_create({'link_color3' => ["purchase", "refund"]}, 'blue', 'red', 'green')
321
+ experiment.goals.should == ["purchase", "refund"]
322
+ experiment = Split::Experiment.find_or_create('link_color3', 'blue', 'red', 'green')
323
+ experiment.goals.should == []
324
+ end
283
325
  end
284
326
  end
327
+
285
328
  end