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,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
|