ab-split 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require "spec_helper"
|
3
|
+
|
4
|
+
describe Split::Persistence do
|
5
|
+
|
6
|
+
subject { Split::Persistence }
|
7
|
+
|
8
|
+
describe ".adapter" do
|
9
|
+
context "when the persistence config is a symbol" do
|
10
|
+
it "should return the appropriate adapter for the symbol" do
|
11
|
+
expect(Split.configuration).to receive(:persistence).twice.and_return(:cookie)
|
12
|
+
expect(subject.adapter).to eq(Split::Persistence::CookieAdapter)
|
13
|
+
end
|
14
|
+
|
15
|
+
it "should return an adapter whose class is present in Split::Persistence::ADAPTERS" do
|
16
|
+
expect(Split.configuration).to receive(:persistence).twice.and_return(:cookie)
|
17
|
+
expect(Split::Persistence::ADAPTERS.values).to include(subject.adapter)
|
18
|
+
end
|
19
|
+
|
20
|
+
it "should raise if the adapter cannot be found" do
|
21
|
+
expect(Split.configuration).to receive(:persistence).twice.and_return(:something_weird)
|
22
|
+
expect { subject.adapter }.to raise_error(Split::InvalidPersistenceAdapterError)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
context "when the persistence config is a class" do
|
26
|
+
let(:custom_adapter_class) { MyCustomAdapterClass = Class.new }
|
27
|
+
it "should return that class" do
|
28
|
+
expect(Split.configuration).to receive(:persistence).twice.and_return(custom_adapter_class)
|
29
|
+
expect(subject.adapter).to eq(MyCustomAdapterClass)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
@@ -0,0 +1,111 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Split::RedisInterface do
|
4
|
+
let(:list_name) { 'list_name' }
|
5
|
+
let(:set_name) { 'set_name' }
|
6
|
+
let(:interface) { described_class.new }
|
7
|
+
|
8
|
+
describe '#persist_list' do
|
9
|
+
subject(:persist_list) do
|
10
|
+
interface.persist_list(list_name, %w(a b c d))
|
11
|
+
end
|
12
|
+
|
13
|
+
specify do
|
14
|
+
expect(persist_list).to eq %w(a b c d)
|
15
|
+
expect(Split.redis.lindex(list_name, 0)).to eq 'a'
|
16
|
+
expect(Split.redis.lindex(list_name, 1)).to eq 'b'
|
17
|
+
expect(Split.redis.lindex(list_name, 2)).to eq 'c'
|
18
|
+
expect(Split.redis.lindex(list_name, 3)).to eq 'd'
|
19
|
+
expect(Split.redis.llen(list_name)).to eq 4
|
20
|
+
end
|
21
|
+
|
22
|
+
context 'list is overwritten but not deleted' do
|
23
|
+
specify do
|
24
|
+
expect(persist_list).to eq %w(a b c d)
|
25
|
+
interface.persist_list(list_name, ['z'])
|
26
|
+
expect(Split.redis.lindex(list_name, 0)).to eq 'z'
|
27
|
+
expect(Split.redis.llen(list_name)).to eq 1
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
describe '#add_to_list' do
|
33
|
+
subject(:add_to_list) do
|
34
|
+
interface.add_to_list(list_name, 'y')
|
35
|
+
interface.add_to_list(list_name, 'z')
|
36
|
+
end
|
37
|
+
|
38
|
+
specify do
|
39
|
+
add_to_list
|
40
|
+
expect(Split.redis.lindex(list_name, 0)).to eq 'y'
|
41
|
+
expect(Split.redis.lindex(list_name, 1)).to eq 'z'
|
42
|
+
expect(Split.redis.llen(list_name)).to eq 2
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
describe '#set_list_index' do
|
47
|
+
subject(:set_list_index) do
|
48
|
+
interface.add_to_list(list_name, 'y')
|
49
|
+
interface.add_to_list(list_name, 'z')
|
50
|
+
interface.set_list_index(list_name, 0, 'a')
|
51
|
+
end
|
52
|
+
|
53
|
+
specify do
|
54
|
+
set_list_index
|
55
|
+
expect(Split.redis.lindex(list_name, 0)).to eq 'a'
|
56
|
+
expect(Split.redis.lindex(list_name, 1)).to eq 'z'
|
57
|
+
expect(Split.redis.llen(list_name)).to eq 2
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
describe '#list_length' do
|
62
|
+
subject(:list_length) do
|
63
|
+
interface.add_to_list(list_name, 'y')
|
64
|
+
interface.add_to_list(list_name, 'z')
|
65
|
+
interface.list_length(list_name)
|
66
|
+
end
|
67
|
+
|
68
|
+
specify do
|
69
|
+
expect(list_length).to eq 2
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
describe '#remove_last_item_from_list' do
|
74
|
+
subject(:remove_last_item_from_list) do
|
75
|
+
interface.add_to_list(list_name, 'y')
|
76
|
+
interface.add_to_list(list_name, 'z')
|
77
|
+
interface.remove_last_item_from_list(list_name)
|
78
|
+
end
|
79
|
+
|
80
|
+
specify do
|
81
|
+
remove_last_item_from_list
|
82
|
+
expect(Split.redis.lindex(list_name, 0)).to eq 'y'
|
83
|
+
expect(Split.redis.llen(list_name)).to eq 1
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
describe '#make_list_length' do
|
88
|
+
subject(:make_list_length) do
|
89
|
+
interface.add_to_list(list_name, 'y')
|
90
|
+
interface.add_to_list(list_name, 'z')
|
91
|
+
interface.make_list_length(list_name, 1)
|
92
|
+
end
|
93
|
+
|
94
|
+
specify do
|
95
|
+
make_list_length
|
96
|
+
expect(Split.redis.lindex(list_name, 0)).to eq 'y'
|
97
|
+
expect(Split.redis.llen(list_name)).to eq 1
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
describe '#add_to_set' do
|
102
|
+
subject(:add_to_set) do
|
103
|
+
interface.add_to_set(set_name, 'something')
|
104
|
+
end
|
105
|
+
|
106
|
+
specify do
|
107
|
+
add_to_set
|
108
|
+
expect(Split.redis.sismember(set_name, 'something')).to be true
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
ENV['RACK_ENV'] = "test"
|
3
|
+
|
4
|
+
require 'rubygems'
|
5
|
+
require 'bundler/setup'
|
6
|
+
|
7
|
+
require 'simplecov'
|
8
|
+
SimpleCov.start
|
9
|
+
|
10
|
+
require 'split'
|
11
|
+
require 'ostruct'
|
12
|
+
require 'yaml'
|
13
|
+
|
14
|
+
Dir['./spec/support/*.rb'].each { |f| require f }
|
15
|
+
|
16
|
+
require "fakeredis"
|
17
|
+
|
18
|
+
G_fakeredis = Redis.new
|
19
|
+
|
20
|
+
module GlobalSharedContext
|
21
|
+
extend RSpec::SharedContext
|
22
|
+
let(:mock_user){ Split::User.new(double(session: {})) }
|
23
|
+
before(:each) do
|
24
|
+
Split.configuration = Split::Configuration.new
|
25
|
+
Split.redis = G_fakeredis
|
26
|
+
Split.redis.flushall
|
27
|
+
@ab_user = mock_user
|
28
|
+
params = nil
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
RSpec.configure do |config|
|
33
|
+
config.order = 'random'
|
34
|
+
config.include GlobalSharedContext
|
35
|
+
end
|
36
|
+
|
37
|
+
def session
|
38
|
+
@session ||= {}
|
39
|
+
end
|
40
|
+
|
41
|
+
def params
|
42
|
+
@params ||= {}
|
43
|
+
end
|
44
|
+
|
45
|
+
def request(ua = 'Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_6; de-de) AppleWebKit/533.20.25 (KHTML, like Gecko) Version/5.0.4 Safari/533.20.27')
|
46
|
+
@request ||= begin
|
47
|
+
r = OpenStruct.new
|
48
|
+
r.user_agent = ua
|
49
|
+
r.ip = '192.168.1.1'
|
50
|
+
r
|
51
|
+
end
|
52
|
+
end
|
data/spec/split_spec.rb
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'spec_helper'
|
3
|
+
|
4
|
+
RSpec.describe Split do
|
5
|
+
|
6
|
+
around(:each) do |ex|
|
7
|
+
old_env, old_redis = [ENV.delete('REDIS_URL'), Split.redis]
|
8
|
+
ex.run
|
9
|
+
ENV['REDIS_URL'] = old_env
|
10
|
+
Split.redis = old_redis
|
11
|
+
end
|
12
|
+
|
13
|
+
describe '#redis=' do
|
14
|
+
it 'accepts a url string' do
|
15
|
+
Split.redis = 'redis://localhost:6379'
|
16
|
+
expect(Split.redis).to be_a(Redis)
|
17
|
+
|
18
|
+
client = Split.redis.connection
|
19
|
+
expect(client[:host]).to eq("localhost")
|
20
|
+
expect(client[:port]).to eq(6379)
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'accepts an options hash' do
|
24
|
+
Split.redis = {host: 'localhost', port: 6379, db: 12}
|
25
|
+
expect(Split.redis).to be_a(Redis)
|
26
|
+
|
27
|
+
client = Split.redis.connection
|
28
|
+
expect(client[:host]).to eq("localhost")
|
29
|
+
expect(client[:port]).to eq(6379)
|
30
|
+
expect(client[:db]).to eq(12)
|
31
|
+
end
|
32
|
+
|
33
|
+
it 'accepts a valid Redis instance' do
|
34
|
+
other_redis = Redis.new(url: "redis://localhost:6379")
|
35
|
+
Split.redis = other_redis
|
36
|
+
expect(Split.redis).to eq(other_redis)
|
37
|
+
end
|
38
|
+
|
39
|
+
it 'raises an ArgumentError when server cannot be determined' do
|
40
|
+
expect { Split.redis = Object.new }.to raise_error(ArgumentError)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
class CookiesMock
|
3
|
+
|
4
|
+
def initialize
|
5
|
+
@cookies = {}
|
6
|
+
end
|
7
|
+
|
8
|
+
def []=(key, value)
|
9
|
+
@cookies[key] = value[:value]
|
10
|
+
end
|
11
|
+
|
12
|
+
def [](key)
|
13
|
+
@cookies[key]
|
14
|
+
end
|
15
|
+
|
16
|
+
def delete(key)
|
17
|
+
@cookies.delete(key)
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
data/spec/trial_spec.rb
ADDED
@@ -0,0 +1,299 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'spec_helper'
|
3
|
+
require 'split/trial'
|
4
|
+
|
5
|
+
describe Split::Trial do
|
6
|
+
let(:user) { mock_user }
|
7
|
+
let(:alternatives) { ['basket', 'cart'] }
|
8
|
+
let(:experiment) do
|
9
|
+
Split::Experiment.new('basket_text', :alternatives => alternatives).save
|
10
|
+
end
|
11
|
+
|
12
|
+
it "should be initializeable" do
|
13
|
+
experiment = double('experiment')
|
14
|
+
alternative = double('alternative', :kind_of? => Split::Alternative)
|
15
|
+
trial = Split::Trial.new(:experiment => experiment, :alternative => alternative)
|
16
|
+
expect(trial.experiment).to eq(experiment)
|
17
|
+
expect(trial.alternative).to eq(alternative)
|
18
|
+
end
|
19
|
+
|
20
|
+
describe "alternative" do
|
21
|
+
it "should use the alternative if specified" do
|
22
|
+
alternative = double('alternative', :kind_of? => Split::Alternative)
|
23
|
+
trial = Split::Trial.new(:experiment => double('experiment'),
|
24
|
+
:alternative => alternative, :user => user)
|
25
|
+
expect(trial).not_to receive(:choose)
|
26
|
+
expect(trial.alternative).to eq(alternative)
|
27
|
+
end
|
28
|
+
|
29
|
+
it "should load the alternative when the alternative name is set" do
|
30
|
+
experiment = Split::Experiment.new('basket_text', :alternatives => ['basket', 'cart'])
|
31
|
+
experiment.save
|
32
|
+
|
33
|
+
trial = Split::Trial.new(:experiment => experiment, :alternative => 'basket')
|
34
|
+
expect(trial.alternative.name).to eq('basket')
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
describe "metadata" do
|
39
|
+
let(:metadata) { Hash[alternatives.map { |k| [k, "Metadata for #{k}"] }] }
|
40
|
+
let(:experiment) do
|
41
|
+
Split::Experiment.new('basket_text', :alternatives => alternatives, :metadata => metadata).save
|
42
|
+
end
|
43
|
+
|
44
|
+
it 'has metadata on each trial' do
|
45
|
+
trial = Split::Trial.new(:experiment => experiment, :user => user, :metadata => metadata['cart'],
|
46
|
+
:override => 'cart')
|
47
|
+
expect(trial.metadata).to eq(metadata['cart'])
|
48
|
+
end
|
49
|
+
|
50
|
+
it 'has metadata on each trial from the experiment' do
|
51
|
+
trial = Split::Trial.new(:experiment => experiment, :user => user)
|
52
|
+
trial.choose!
|
53
|
+
expect(trial.metadata).to eq(metadata[trial.alternative.name])
|
54
|
+
expect(trial.metadata).to match(/#{trial.alternative.name}/)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
describe "#choose!" do
|
59
|
+
let(:context) { double(on_trial_callback: 'test callback') }
|
60
|
+
let(:trial) do
|
61
|
+
Split::Trial.new(:user => user, :experiment => experiment)
|
62
|
+
end
|
63
|
+
|
64
|
+
shared_examples_for 'a trial with callbacks' do
|
65
|
+
it 'does not run if on_trial callback is not respondable' do
|
66
|
+
Split.configuration.on_trial = :foo
|
67
|
+
allow(context).to receive(:respond_to?).with(:foo, true).and_return false
|
68
|
+
expect(context).to_not receive(:foo)
|
69
|
+
trial.choose! context
|
70
|
+
end
|
71
|
+
it 'runs on_trial callback' do
|
72
|
+
Split.configuration.on_trial = :on_trial_callback
|
73
|
+
expect(context).to receive(:on_trial_callback)
|
74
|
+
trial.choose! context
|
75
|
+
end
|
76
|
+
it 'does not run nil on_trial callback' do
|
77
|
+
Split.configuration.on_trial = nil
|
78
|
+
expect(context).not_to receive(:on_trial_callback)
|
79
|
+
trial.choose! context
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def expect_alternative(trial, alternative_name)
|
84
|
+
3.times do
|
85
|
+
trial.choose! context
|
86
|
+
expect(alternative_name).to include(trial.alternative.name)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
context "when override is present" do
|
91
|
+
let(:override) { 'cart' }
|
92
|
+
let(:trial) do
|
93
|
+
Split::Trial.new(:user => user, :experiment => experiment, :override => override)
|
94
|
+
end
|
95
|
+
|
96
|
+
it_behaves_like 'a trial with callbacks'
|
97
|
+
|
98
|
+
it "picks the override" do
|
99
|
+
expect(experiment).to_not receive(:next_alternative)
|
100
|
+
expect_alternative(trial, override)
|
101
|
+
end
|
102
|
+
|
103
|
+
context "when alternative doesn't exist" do
|
104
|
+
let(:override) { nil }
|
105
|
+
it 'falls back on next_alternative' do
|
106
|
+
expect(experiment).to receive(:next_alternative).and_call_original
|
107
|
+
expect_alternative(trial, alternatives)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
context "when disabled option is true" do
|
113
|
+
let(:trial) do
|
114
|
+
Split::Trial.new(:user => user, :experiment => experiment, :disabled => true)
|
115
|
+
end
|
116
|
+
|
117
|
+
it "picks the control", :aggregate_failures do
|
118
|
+
Split.configuration.on_trial = :on_trial_callback
|
119
|
+
expect(experiment).to_not receive(:next_alternative)
|
120
|
+
|
121
|
+
expect(context).not_to receive(:on_trial_callback)
|
122
|
+
|
123
|
+
expect_alternative(trial, 'basket')
|
124
|
+
Split.configuration.on_trial = nil
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
context "when Split is globally disabled" do
|
129
|
+
it "picks the control and does not run on_trial callbacks", :aggregate_failures do
|
130
|
+
Split.configuration.enabled = false
|
131
|
+
Split.configuration.on_trial = :on_trial_callback
|
132
|
+
|
133
|
+
expect(experiment).to_not receive(:next_alternative)
|
134
|
+
expect(context).not_to receive(:on_trial_callback)
|
135
|
+
expect_alternative(trial, 'basket')
|
136
|
+
|
137
|
+
Split.configuration.enabled = true
|
138
|
+
Split.configuration.on_trial = nil
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
context "when experiment has winner" do
|
143
|
+
let(:trial) do
|
144
|
+
Split::Trial.new(:user => user, :experiment => experiment)
|
145
|
+
end
|
146
|
+
|
147
|
+
it_behaves_like 'a trial with callbacks'
|
148
|
+
|
149
|
+
it "picks the winner" do
|
150
|
+
experiment.winner = 'cart'
|
151
|
+
expect(experiment).to_not receive(:next_alternative)
|
152
|
+
|
153
|
+
expect_alternative(trial, 'cart')
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
context "when exclude is true" do
|
158
|
+
let(:trial) do
|
159
|
+
Split::Trial.new(:user => user, :experiment => experiment, :exclude => true)
|
160
|
+
end
|
161
|
+
|
162
|
+
it_behaves_like 'a trial with callbacks'
|
163
|
+
|
164
|
+
it "picks the control" do
|
165
|
+
expect(experiment).to_not receive(:next_alternative)
|
166
|
+
expect_alternative(trial, 'basket')
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
context "when user is already participating" do
|
171
|
+
it_behaves_like 'a trial with callbacks'
|
172
|
+
|
173
|
+
it "picks the same alternative" do
|
174
|
+
user[experiment.key] = 'basket'
|
175
|
+
expect(experiment).to_not receive(:next_alternative)
|
176
|
+
|
177
|
+
expect_alternative(trial, 'basket')
|
178
|
+
end
|
179
|
+
|
180
|
+
context "when alternative is not found" do
|
181
|
+
it "falls back on next_alternative" do
|
182
|
+
user[experiment.key] = 'notfound'
|
183
|
+
expect(experiment).to receive(:next_alternative).and_call_original
|
184
|
+
expect_alternative(trial, alternatives)
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
context "when user is a new participant" do
|
190
|
+
it "picks a new alternative and runs on_trial_choose callback", :aggregate_failures do
|
191
|
+
Split.configuration.on_trial_choose = :on_trial_choose_callback
|
192
|
+
|
193
|
+
expect(experiment).to receive(:next_alternative).and_call_original
|
194
|
+
expect(context).to receive(:on_trial_choose_callback)
|
195
|
+
|
196
|
+
trial.choose! context
|
197
|
+
|
198
|
+
expect(trial.alternative.name).to_not be_empty
|
199
|
+
Split.configuration.on_trial_choose = nil
|
200
|
+
end
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
describe "#complete!" do
|
205
|
+
let(:trial) { Split::Trial.new(:user => user, :experiment => experiment) }
|
206
|
+
context 'when there are no goals' do
|
207
|
+
it 'should complete the trial' do
|
208
|
+
trial.choose!
|
209
|
+
old_completed_count = trial.alternative.completed_count
|
210
|
+
trial.complete!
|
211
|
+
expect(trial.alternative.completed_count).to be(old_completed_count+1)
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
context 'when there are many goals' do
|
216
|
+
let(:goals) { ['first', 'second'] }
|
217
|
+
let(:trial) { Split::Trial.new(:user => user, :experiment => experiment, :goals => goals) }
|
218
|
+
shared_examples_for "goal completion" do
|
219
|
+
it 'should not complete the trial' do
|
220
|
+
trial.choose!
|
221
|
+
old_completed_count = trial.alternative.completed_count
|
222
|
+
trial.complete!(goal)
|
223
|
+
expect(trial.alternative.completed_count).to_not be(old_completed_count+1)
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
describe 'Array of Goals' do
|
228
|
+
let(:goal) { [goals.first] }
|
229
|
+
it_behaves_like 'goal completion'
|
230
|
+
end
|
231
|
+
|
232
|
+
describe 'String of Goal' do
|
233
|
+
let(:goal) { goals.first }
|
234
|
+
it_behaves_like 'goal completion'
|
235
|
+
end
|
236
|
+
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
describe "alternative recording" do
|
241
|
+
before(:each) { Split.configuration.store_override = false }
|
242
|
+
|
243
|
+
context "when override is present" do
|
244
|
+
it "stores when store_override is true" do
|
245
|
+
trial = Split::Trial.new(:user => user, :experiment => experiment, :override => 'basket')
|
246
|
+
|
247
|
+
Split.configuration.store_override = true
|
248
|
+
expect(user).to receive("[]=")
|
249
|
+
trial.choose!
|
250
|
+
expect(trial.alternative.participant_count).to eq(1)
|
251
|
+
end
|
252
|
+
|
253
|
+
it "does not store when store_override is false" do
|
254
|
+
trial = Split::Trial.new(:user => user, :experiment => experiment, :override => 'basket')
|
255
|
+
|
256
|
+
expect(user).to_not receive("[]=")
|
257
|
+
trial.choose!
|
258
|
+
end
|
259
|
+
end
|
260
|
+
|
261
|
+
context "when disabled is present" do
|
262
|
+
it "stores when store_override is true" do
|
263
|
+
trial = Split::Trial.new(:user => user, :experiment => experiment, :disabled => true)
|
264
|
+
|
265
|
+
Split.configuration.store_override = true
|
266
|
+
expect(user).to receive("[]=")
|
267
|
+
trial.choose!
|
268
|
+
end
|
269
|
+
|
270
|
+
it "does not store when store_override is false" do
|
271
|
+
trial = Split::Trial.new(:user => user, :experiment => experiment, :disabled => true)
|
272
|
+
|
273
|
+
expect(user).to_not receive("[]=")
|
274
|
+
trial.choose!
|
275
|
+
end
|
276
|
+
end
|
277
|
+
|
278
|
+
context "when exclude is present" do
|
279
|
+
it "does not store" do
|
280
|
+
trial = Split::Trial.new(:user => user, :experiment => experiment, :exclude => true)
|
281
|
+
|
282
|
+
expect(user).to_not receive("[]=")
|
283
|
+
trial.choose!
|
284
|
+
end
|
285
|
+
end
|
286
|
+
|
287
|
+
context 'when experiment has winner' do
|
288
|
+
let(:trial) do
|
289
|
+
experiment.winner = 'cart'
|
290
|
+
Split::Trial.new(:user => user, :experiment => experiment)
|
291
|
+
end
|
292
|
+
|
293
|
+
it 'does not store' do
|
294
|
+
expect(user).to_not receive("[]=")
|
295
|
+
trial.choose!
|
296
|
+
end
|
297
|
+
end
|
298
|
+
end
|
299
|
+
end
|