split 3.3.0 → 4.0.0.pre

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. checksums.yaml +4 -4
  2. data/.eslintrc +1 -1
  3. data/.github/FUNDING.yml +1 -0
  4. data/.github/ISSUE_TEMPLATE/bug_report.md +24 -0
  5. data/.rspec +1 -0
  6. data/.rubocop.yml +71 -1044
  7. data/.rubocop_todo.yml +226 -0
  8. data/.travis.yml +18 -39
  9. data/Appraisals +4 -0
  10. data/CHANGELOG.md +110 -0
  11. data/CODE_OF_CONDUCT.md +3 -3
  12. data/Gemfile +2 -0
  13. data/README.md +58 -23
  14. data/Rakefile +2 -0
  15. data/gemfiles/{4.2.gemfile → 6.0.gemfile} +1 -1
  16. data/lib/split.rb +16 -3
  17. data/lib/split/algorithms/block_randomization.rb +2 -0
  18. data/lib/split/algorithms/weighted_sample.rb +2 -1
  19. data/lib/split/algorithms/whiplash.rb +3 -2
  20. data/lib/split/alternative.rb +4 -3
  21. data/lib/split/cache.rb +28 -0
  22. data/lib/split/combined_experiments_helper.rb +3 -2
  23. data/lib/split/configuration.rb +15 -14
  24. data/lib/split/dashboard.rb +19 -1
  25. data/lib/split/dashboard/helpers.rb +3 -2
  26. data/lib/split/dashboard/pagination_helpers.rb +4 -4
  27. data/lib/split/dashboard/paginator.rb +1 -0
  28. data/lib/split/dashboard/public/dashboard.js +10 -0
  29. data/lib/split/dashboard/public/style.css +5 -0
  30. data/lib/split/dashboard/views/_controls.erb +13 -0
  31. data/lib/split/dashboard/views/layout.erb +1 -1
  32. data/lib/split/encapsulated_helper.rb +3 -2
  33. data/lib/split/engine.rb +7 -4
  34. data/lib/split/exceptions.rb +1 -0
  35. data/lib/split/experiment.rb +98 -65
  36. data/lib/split/experiment_catalog.rb +1 -3
  37. data/lib/split/extensions/string.rb +1 -0
  38. data/lib/split/goals_collection.rb +2 -0
  39. data/lib/split/helper.rb +30 -10
  40. data/lib/split/metric.rb +2 -1
  41. data/lib/split/persistence.rb +4 -2
  42. data/lib/split/persistence/cookie_adapter.rb +1 -0
  43. data/lib/split/persistence/dual_adapter.rb +54 -12
  44. data/lib/split/persistence/redis_adapter.rb +5 -0
  45. data/lib/split/persistence/session_adapter.rb +1 -0
  46. data/lib/split/redis_interface.rb +9 -28
  47. data/lib/split/trial.rb +25 -17
  48. data/lib/split/user.rb +19 -3
  49. data/lib/split/version.rb +2 -4
  50. data/lib/split/zscore.rb +1 -0
  51. data/spec/alternative_spec.rb +1 -1
  52. data/spec/cache_spec.rb +88 -0
  53. data/spec/configuration_spec.rb +1 -14
  54. data/spec/dashboard/pagination_helpers_spec.rb +3 -1
  55. data/spec/dashboard_helpers_spec.rb +2 -2
  56. data/spec/dashboard_spec.rb +78 -17
  57. data/spec/encapsulated_helper_spec.rb +2 -2
  58. data/spec/experiment_spec.rb +116 -12
  59. data/spec/goals_collection_spec.rb +1 -1
  60. data/spec/helper_spec.rb +191 -112
  61. data/spec/persistence/cookie_adapter_spec.rb +1 -1
  62. data/spec/persistence/dual_adapter_spec.rb +160 -68
  63. data/spec/persistence/redis_adapter_spec.rb +9 -0
  64. data/spec/redis_interface_spec.rb +0 -69
  65. data/spec/spec_helper.rb +5 -6
  66. data/spec/trial_spec.rb +65 -19
  67. data/spec/user_spec.rb +28 -0
  68. data/split.gemspec +9 -9
  69. metadata +34 -28
@@ -52,7 +52,7 @@ describe Split::Persistence::CookieAdapter do
52
52
  it "puts multiple experiments in a single cookie" do
53
53
  subject["foo"] = "FOO"
54
54
  subject["bar"] = "BAR"
55
- expect(context.response.headers["Set-Cookie"]).to match(/\Asplit=%7B%22foo%22%3A%22FOO%22%2C%22bar%22%3A%22BAR%22%7D; path=\/; expires=[a-zA-Z]{3}, \d{2} [a-zA-Z]{3} \d{4} \d{2}:\d{2}:\d{2} -0000\Z/)
55
+ expect(context.response.headers["Set-Cookie"]).to match(/\Asplit=%7B%22foo%22%3A%22FOO%22%2C%22bar%22%3A%22BAR%22%7D; path=\/; expires=[a-zA-Z]{3}, \d{2} [a-zA-Z]{3} \d{4} \d{2}:\d{2}:\d{2} [A-Z]{3}\Z/)
56
56
  end
57
57
 
58
58
  it "ensure other added cookies are not overriden" do
@@ -1,102 +1,194 @@
1
1
  # frozen_string_literal: true
2
- require "spec_helper"
2
+
3
+ require 'spec_helper'
3
4
 
4
5
  describe Split::Persistence::DualAdapter do
6
+ let(:context) { 'some context' }
5
7
 
6
- let(:context){ "some context" }
7
-
8
- let(:just_adapter){ Class.new }
9
- let(:selected_adapter_instance){ double }
10
- let(:selected_adapter){
11
- c = Class.new
12
- expect(c).to receive(:new){ selected_adapter_instance }
13
- c
14
- }
15
- let(:not_selected_adapter){
16
- c = Class.new
17
- expect(c).not_to receive(:new)
18
- c
19
- }
20
-
21
- shared_examples_for "forwarding calls" do
22
- it "#[]=" do
23
- expect(selected_adapter_instance).to receive(:[]=).with('my_key', 'my_value')
24
- expect_any_instance_of(not_selected_adapter).not_to receive(:[]=)
25
- subject["my_key"] = "my_value"
26
- end
8
+ let(:logged_in_adapter_instance) { double }
9
+ let(:logged_in_adapter) do
10
+ Class.new.tap { |c| allow(c).to receive(:new) { logged_in_adapter_instance } }
11
+ end
12
+ let(:logged_out_adapter_instance) { double }
13
+ let(:logged_out_adapter) do
14
+ Class.new.tap { |c| allow(c).to receive(:new) { logged_out_adapter_instance } }
15
+ end
27
16
 
28
- it "#[]" do
29
- expect(selected_adapter_instance).to receive(:[]).with('my_key'){'my_value'}
30
- expect_any_instance_of(not_selected_adapter).not_to receive(:[])
31
- expect(subject["my_key"]).to eq('my_value')
32
- end
17
+ context 'when fallback_to_logged_out_adapter is false' do
18
+ context 'when logged in' do
19
+ subject do
20
+ described_class.with_config(
21
+ logged_in: lambda { |context| true },
22
+ logged_in_adapter: logged_in_adapter,
23
+ logged_out_adapter: logged_out_adapter,
24
+ fallback_to_logged_out_adapter: false
25
+ ).new(context)
26
+ end
27
+
28
+ it '#[]=' do
29
+ expect(logged_in_adapter_instance).to receive(:[]=).with('my_key', 'my_value')
30
+ expect_any_instance_of(logged_out_adapter).not_to receive(:[]=)
31
+ subject['my_key'] = 'my_value'
32
+ end
33
+
34
+ it '#[]' do
35
+ expect(logged_in_adapter_instance).to receive(:[]).with('my_key') { 'my_value' }
36
+ expect_any_instance_of(logged_out_adapter).not_to receive(:[])
37
+ expect(subject['my_key']).to eq('my_value')
38
+ end
33
39
 
34
- it "#delete" do
35
- expect(selected_adapter_instance).to receive(:delete).with('my_key'){'my_value'}
36
- expect_any_instance_of(not_selected_adapter).not_to receive(:delete)
37
- expect(subject.delete("my_key")).to eq('my_value')
40
+ it '#delete' do
41
+ expect(logged_in_adapter_instance).to receive(:delete).with('my_key') { 'my_value' }
42
+ expect_any_instance_of(logged_out_adapter).not_to receive(:delete)
43
+ expect(subject.delete('my_key')).to eq('my_value')
44
+ end
45
+
46
+ it '#keys' do
47
+ expect(logged_in_adapter_instance).to receive(:keys) { ['my_value'] }
48
+ expect_any_instance_of(logged_out_adapter).not_to receive(:keys)
49
+ expect(subject.keys).to eq(['my_value'])
50
+ end
38
51
  end
39
52
 
40
- it "#keys" do
41
- expect(selected_adapter_instance).to receive(:keys){'my_value'}
42
- expect_any_instance_of(not_selected_adapter).not_to receive(:keys)
43
- expect(subject.keys).to eq('my_value')
53
+ context 'when logged out' do
54
+ subject do
55
+ described_class.with_config(
56
+ logged_in: lambda { |context| false },
57
+ logged_in_adapter: logged_in_adapter,
58
+ logged_out_adapter: logged_out_adapter,
59
+ fallback_to_logged_out_adapter: false
60
+ ).new(context)
61
+ end
62
+
63
+ it '#[]=' do
64
+ expect_any_instance_of(logged_in_adapter).not_to receive(:[]=)
65
+ expect(logged_out_adapter_instance).to receive(:[]=).with('my_key', 'my_value')
66
+ subject['my_key'] = 'my_value'
67
+ end
68
+
69
+ it '#[]' do
70
+ expect_any_instance_of(logged_in_adapter).not_to receive(:[])
71
+ expect(logged_out_adapter_instance).to receive(:[]).with('my_key') { 'my_value' }
72
+ expect(subject['my_key']).to eq('my_value')
73
+ end
74
+
75
+ it '#delete' do
76
+ expect_any_instance_of(logged_in_adapter).not_to receive(:delete)
77
+ expect(logged_out_adapter_instance).to receive(:delete).with('my_key') { 'my_value' }
78
+ expect(subject.delete('my_key')).to eq('my_value')
79
+ end
80
+
81
+ it '#keys' do
82
+ expect_any_instance_of(logged_in_adapter).not_to receive(:keys)
83
+ expect(logged_out_adapter_instance).to receive(:keys) { ['my_value', 'my_value2'] }
84
+ expect(subject.keys).to eq(['my_value', 'my_value2'])
85
+ end
44
86
  end
45
87
  end
46
88
 
47
- context "when logged in" do
48
- subject {
49
- described_class.with_config(
50
- logged_in: lambda { |context| true },
51
- logged_in_adapter: selected_adapter,
52
- logged_out_adapter: not_selected_adapter
89
+ context 'when fallback_to_logged_out_adapter is true' do
90
+ context 'when logged in' do
91
+ subject do
92
+ described_class.with_config(
93
+ logged_in: lambda { |context| true },
94
+ logged_in_adapter: logged_in_adapter,
95
+ logged_out_adapter: logged_out_adapter,
96
+ fallback_to_logged_out_adapter: true
53
97
  ).new(context)
54
- }
98
+ end
55
99
 
56
- it_should_behave_like "forwarding calls"
57
- end
100
+ it '#[]=' do
101
+ expect(logged_in_adapter_instance).to receive(:[]=).with('my_key', 'my_value')
102
+ expect(logged_out_adapter_instance).to receive(:[]=).with('my_key', 'my_value')
103
+ expect(logged_out_adapter_instance).to receive(:[]).with('my_key') { nil }
104
+ subject['my_key'] = 'my_value'
105
+ end
106
+
107
+ it '#[]' do
108
+ expect(logged_in_adapter_instance).to receive(:[]).with('my_key') { 'my_value' }
109
+ expect_any_instance_of(logged_out_adapter).not_to receive(:[])
110
+ expect(subject['my_key']).to eq('my_value')
111
+ end
112
+
113
+ it '#delete' do
114
+ expect(logged_in_adapter_instance).to receive(:delete).with('my_key') { 'my_value' }
115
+ expect(logged_out_adapter_instance).to receive(:delete).with('my_key') { 'my_value' }
116
+ expect(subject.delete('my_key')).to eq('my_value')
117
+ end
118
+
119
+ it '#keys' do
120
+ expect(logged_in_adapter_instance).to receive(:keys) { ['my_value'] }
121
+ expect(logged_out_adapter_instance).to receive(:keys) { ['my_value', 'my_value2'] }
122
+ expect(subject.keys).to eq(['my_value', 'my_value2'])
123
+ end
124
+ end
58
125
 
59
- context "when not logged in" do
60
- subject {
61
- described_class.with_config(
62
- logged_in: lambda { |context| false },
63
- logged_in_adapter: not_selected_adapter,
64
- logged_out_adapter: selected_adapter
126
+ context 'when logged out' do
127
+ subject do
128
+ described_class.with_config(
129
+ logged_in: lambda { |context| false },
130
+ logged_in_adapter: logged_in_adapter,
131
+ logged_out_adapter: logged_out_adapter,
132
+ fallback_to_logged_out_adapter: true
65
133
  ).new(context)
66
- }
134
+ end
135
+
136
+ it '#[]=' do
137
+ expect_any_instance_of(logged_in_adapter).not_to receive(:[]=)
138
+ expect(logged_out_adapter_instance).to receive(:[]=).with('my_key', 'my_value')
139
+ expect(logged_out_adapter_instance).to receive(:[]).with('my_key') { nil }
140
+ subject['my_key'] = 'my_value'
141
+ end
142
+
143
+ it '#[]' do
144
+ expect_any_instance_of(logged_in_adapter).not_to receive(:[])
145
+ expect(logged_out_adapter_instance).to receive(:[]).with('my_key') { 'my_value' }
146
+ expect(subject['my_key']).to eq('my_value')
147
+ end
148
+
149
+ it '#delete' do
150
+ expect(logged_in_adapter_instance).to receive(:delete).with('my_key') { 'my_value' }
151
+ expect(logged_out_adapter_instance).to receive(:delete).with('my_key') { 'my_value' }
152
+ expect(subject.delete('my_key')).to eq('my_value')
153
+ end
67
154
 
68
- it_should_behave_like "forwarding calls"
155
+ it '#keys' do
156
+ expect(logged_in_adapter_instance).to receive(:keys) { ['my_value'] }
157
+ expect(logged_out_adapter_instance).to receive(:keys) { ['my_value', 'my_value2'] }
158
+ expect(subject.keys).to eq(['my_value', 'my_value2'])
159
+ end
160
+ end
69
161
  end
70
162
 
71
- describe "when errors in config" do
72
- before{
73
- described_class.config.clear
74
- }
75
- let(:some_proc){ ->{} }
76
- it "when no logged in adapter" do
163
+ describe 'when errors in config' do
164
+ before { described_class.config.clear }
165
+ let(:some_proc) { ->{} }
166
+
167
+ it 'when no logged in adapter' do
77
168
  expect{
78
169
  described_class.with_config(
79
170
  logged_in: some_proc,
80
- logged_out_adapter: just_adapter
81
- ).new(context)
171
+ logged_out_adapter: logged_out_adapter
172
+ ).new(context)
82
173
  }.to raise_error(StandardError, /:logged_in_adapter/)
83
174
  end
84
- it "when no logged out adapter" do
175
+
176
+ it 'when no logged out adapter' do
85
177
  expect{
86
178
  described_class.with_config(
87
179
  logged_in: some_proc,
88
- logged_in_adapter: just_adapter
89
- ).new(context)
180
+ logged_in_adapter: logged_in_adapter
181
+ ).new(context)
90
182
  }.to raise_error(StandardError, /:logged_out_adapter/)
91
183
  end
92
- it "when no logged in detector" do
184
+
185
+ it 'when no logged in detector' do
93
186
  expect{
94
187
  described_class.with_config(
95
- logged_in_adapter: just_adapter,
96
- logged_out_adapter: just_adapter
97
- ).new(context)
188
+ logged_in_adapter: logged_in_adapter,
189
+ logged_out_adapter: logged_out_adapter
190
+ ).new(context)
98
191
  }.to raise_error(StandardError, /:logged_in$/)
99
192
  end
100
193
  end
101
-
102
194
  end
@@ -60,6 +60,15 @@ describe Split::Persistence::RedisAdapter do
60
60
  end
61
61
  end
62
62
 
63
+ describe '#find' do
64
+ before { Split::Persistence::RedisAdapter.with_config(:lookup_by => proc{'frag'}, :namespace => 'a_namespace') }
65
+
66
+ it "should create and user from a given key" do
67
+ adapter = Split::Persistence::RedisAdapter.find(2)
68
+ expect(adapter.redis_key).to eq("a_namespace:2")
69
+ end
70
+ end
71
+
63
72
  context 'functional tests' do
64
73
  before { Split::Persistence::RedisAdapter.with_config(:lookup_by => 'lookup') }
65
74
 
@@ -29,75 +29,6 @@ describe Split::RedisInterface do
29
29
  end
30
30
  end
31
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
32
  describe '#add_to_set' do
102
33
  subject(:add_to_set) do
103
34
  interface.add_to_set(set_name, 'something')
@@ -13,17 +13,16 @@ require 'yaml'
13
13
 
14
14
  Dir['./spec/support/*.rb'].each { |f| require f }
15
15
 
16
- require "fakeredis"
17
-
18
- G_fakeredis = Redis.new
19
-
20
16
  module GlobalSharedContext
21
17
  extend RSpec::SharedContext
22
18
  let(:mock_user){ Split::User.new(double(session: {})) }
19
+
23
20
  before(:each) do
24
21
  Split.configuration = Split::Configuration.new
25
- Split.redis = G_fakeredis
26
- Split.redis.flushall
22
+ Split.redis = Redis.new
23
+ Split.redis.select(10)
24
+ Split.redis.flushdb
25
+ Split::Cache.clear
27
26
  @ab_user = mock_user
28
27
  params = nil
29
28
  end
@@ -176,6 +176,14 @@ describe Split::Trial do
176
176
 
177
177
  expect_alternative(trial, 'basket')
178
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
179
187
  end
180
188
 
181
189
  context "when user is a new participant" do
@@ -190,42 +198,68 @@ describe Split::Trial do
190
198
  expect(trial.alternative.name).to_not be_empty
191
199
  Split.configuration.on_trial_choose = nil
192
200
  end
201
+
202
+ it "assigns user to an alternative" do
203
+ trial.choose! context
204
+
205
+ expect(alternatives).to include(user[experiment.name])
206
+ end
207
+
208
+ context "when cohorting is disabled" do
209
+ before(:each) { allow(experiment).to receive(:cohorting_disabled?).and_return(true) }
210
+
211
+ it "picks the control and does not run on_trial callbacks" do
212
+ Split.configuration.on_trial = :on_trial_callback
213
+
214
+ expect(experiment).to_not receive(:next_alternative)
215
+ expect(context).not_to receive(:on_trial_callback)
216
+ expect_alternative(trial, 'basket')
217
+
218
+ Split.configuration.enabled = true
219
+ Split.configuration.on_trial = nil
220
+ end
221
+
222
+ it "user is not assigned an alternative" do
223
+ trial.choose! context
224
+
225
+ expect(user[experiment]).to eq(nil)
226
+ end
227
+ end
193
228
  end
194
229
  end
195
230
 
196
231
  describe "#complete!" do
197
- let(:trial) { Split::Trial.new(:user => user, :experiment => experiment) }
198
232
  context 'when there are no goals' do
233
+ let(:trial) { Split::Trial.new(:user => user, :experiment => experiment) }
199
234
  it 'should complete the trial' do
200
235
  trial.choose!
201
236
  old_completed_count = trial.alternative.completed_count
202
237
  trial.complete!
203
- expect(trial.alternative.completed_count).to be(old_completed_count+1)
238
+ expect(trial.alternative.completed_count).to eq(old_completed_count + 1)
204
239
  end
205
240
  end
206
241
 
207
- context 'when there are many goals' do
208
- let(:goals) { ['first', 'second'] }
242
+ context "when there are many goals" do
243
+ let(:goals) { [ "goal1", "goal2" ] }
209
244
  let(:trial) { Split::Trial.new(:user => user, :experiment => experiment, :goals => goals) }
210
- shared_examples_for "goal completion" do
211
- it 'should not complete the trial' do
212
- trial.choose!
213
- old_completed_count = trial.alternative.completed_count
214
- trial.complete!(goal)
215
- expect(trial.alternative.completed_count).to_not be(old_completed_count+1)
216
- end
217
- end
218
245
 
219
- describe 'Array of Goals' do
220
- let(:goal) { [goals.first] }
221
- it_behaves_like 'goal completion'
246
+ it "increments the completed count corresponding to the goals" do
247
+ trial.choose!
248
+ old_completed_counts = goals.map{ |goal| [goal, trial.alternative.completed_count(goal)] }.to_h
249
+ trial.complete!
250
+ goals.each { | goal | expect(trial.alternative.completed_count(goal)).to eq(old_completed_counts[goal] + 1) }
222
251
  end
252
+ end
223
253
 
224
- describe 'String of Goal' do
225
- let(:goal) { goals.first }
226
- it_behaves_like 'goal completion'
254
+ context "when there is 1 goal of type string" do
255
+ let(:goal) { "goal" }
256
+ let(:trial) { Split::Trial.new(:user => user, :experiment => experiment, :goals => goal) }
257
+ it "increments the completed count corresponding to the goal" do
258
+ trial.choose!
259
+ old_completed_count = trial.alternative.completed_count(goal)
260
+ trial.complete!
261
+ expect(trial.alternative.completed_count(goal)).to eq(old_completed_count + 1)
227
262
  end
228
-
229
263
  end
230
264
  end
231
265
 
@@ -275,5 +309,17 @@ describe Split::Trial do
275
309
  trial.choose!
276
310
  end
277
311
  end
312
+
313
+ context 'when experiment has winner' do
314
+ let(:trial) do
315
+ experiment.winner = 'cart'
316
+ Split::Trial.new(:user => user, :experiment => experiment)
317
+ end
318
+
319
+ it 'does not store' do
320
+ expect(user).to_not receive("[]=")
321
+ trial.choose!
322
+ end
323
+ end
278
324
  end
279
325
  end