liveqa 1.8.3 → 1.9.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 +4 -4
- data/.rubocop.yml +8 -2
- data/.travis.yml +0 -1
- data/LICENCE +21 -0
- data/README.md +10 -117
- data/lib/liveqa/api_resource.rb +7 -3
- data/lib/liveqa/batch.rb +18 -0
- data/lib/liveqa/config.rb +13 -21
- data/lib/liveqa/event.rb +1 -1
- data/lib/liveqa/formated_logger.rb +45 -0
- data/lib/liveqa/message.rb +3 -3
- data/lib/liveqa/plugins/rack/middleware.rb +3 -3
- data/lib/liveqa/plugins/sidekiq/server_middleware.rb +1 -1
- data/lib/liveqa/processor/async.rb +48 -0
- data/lib/liveqa/processor/batch.rb +81 -0
- data/lib/liveqa/processor/worker.rb +66 -0
- data/lib/liveqa/util.rb +3 -3
- data/lib/liveqa/version.rb +1 -1
- data/lib/liveqa.rb +66 -33
- data/liveqa.gemspec +3 -3
- data/spec/lib/liveqa/api_resource_spec.rb +8 -8
- data/spec/lib/liveqa/batch_spec.rb +14 -0
- data/spec/lib/liveqa/config_spec.rb +9 -15
- data/spec/lib/liveqa/formated_logger_spec.rb +99 -0
- data/spec/lib/liveqa/processor/async_spec.rb +19 -0
- data/spec/lib/liveqa/processor/batch_spec.rb +48 -0
- data/spec/lib/liveqa/processor/worker_spec.rb +25 -0
- data/spec/lib/liveqa_spec.rb +133 -40
- metadata +26 -16
- data/lib/liveqa/async_handlers/base.rb +0 -15
- data/lib/liveqa/async_handlers/sidekiq.rb +0 -33
- data/spec/lib/liveqa/async_handlers/base_spec.rb +0 -29
- data/spec/lib/liveqa/async_handlers/sidekiq_spec.rb +0 -40
data/lib/liveqa.rb
CHANGED
@@ -5,12 +5,11 @@ require 'ostruct'
|
|
5
5
|
require 'json'
|
6
6
|
require 'time'
|
7
7
|
require 'cgi'
|
8
|
-
|
9
|
-
# Async
|
10
|
-
require 'liveqa/async_handlers/base'
|
8
|
+
require 'logger'
|
11
9
|
|
12
10
|
# Base
|
13
11
|
require 'liveqa/version'
|
12
|
+
require 'liveqa/formated_logger'
|
14
13
|
require 'liveqa/library_name'
|
15
14
|
require 'liveqa/util'
|
16
15
|
require 'liveqa/config'
|
@@ -21,6 +20,11 @@ require 'liveqa/api_resource'
|
|
21
20
|
require 'liveqa/store'
|
22
21
|
require 'liveqa/message'
|
23
22
|
|
23
|
+
# Processor
|
24
|
+
require 'liveqa/processor/async'
|
25
|
+
require 'liveqa/processor/worker'
|
26
|
+
require 'liveqa/processor/batch'
|
27
|
+
|
24
28
|
# Operations
|
25
29
|
require 'liveqa/api_operation/save'
|
26
30
|
require 'liveqa/api_operation/delete'
|
@@ -30,6 +34,7 @@ require 'liveqa/event'
|
|
30
34
|
require 'liveqa/group'
|
31
35
|
require 'liveqa/identity'
|
32
36
|
require 'liveqa/watcher'
|
37
|
+
require 'liveqa/batch'
|
33
38
|
|
34
39
|
# Plugins
|
35
40
|
require 'liveqa/plugins'
|
@@ -39,6 +44,11 @@ require 'liveqa/plugins'
|
|
39
44
|
module LiveQA
|
40
45
|
class << self
|
41
46
|
|
47
|
+
EVENTS_CREATE_ACTION = 'events:create'.freeze
|
48
|
+
GROUPS_UPDATE_ACTION = 'groups:update'.freeze
|
49
|
+
IDENTITIES_UPDATE_ACTION = 'identities:update'.freeze
|
50
|
+
WATCHERS_CREATE_ACTION = 'watchers:create'.freeze
|
51
|
+
|
42
52
|
##
|
43
53
|
# @return [LiveQA::Config] configurations
|
44
54
|
attr_reader :configurations
|
@@ -65,8 +75,8 @@ module LiveQA
|
|
65
75
|
# @param [Hash] payload to be send
|
66
76
|
# @param [Hash] options for the request
|
67
77
|
#
|
68
|
-
# @return [
|
69
|
-
def track(name, payload = {},
|
78
|
+
# @return [Boolean] status of the request
|
79
|
+
def track(name, payload = {}, options = {})
|
70
80
|
return true unless configurations.enabled
|
71
81
|
|
72
82
|
payload[:type] = 'track'
|
@@ -75,12 +85,15 @@ module LiveQA
|
|
75
85
|
|
76
86
|
payload = Message.extended.merge(payload)
|
77
87
|
|
78
|
-
if configurations.
|
79
|
-
return
|
88
|
+
if configurations.async
|
89
|
+
return processor.enqueue(
|
90
|
+
action: EVENTS_CREATE_ACTION,
|
91
|
+
payload: payload,
|
92
|
+
options: options.select { |k, _v| %i[space_name environment_name].include?(k) },
|
93
|
+
)
|
80
94
|
end
|
81
95
|
|
82
|
-
event = Event.create(payload,
|
83
|
-
|
96
|
+
event = Event.create(payload, options)
|
84
97
|
event.successful?
|
85
98
|
end
|
86
99
|
|
@@ -91,8 +104,8 @@ module LiveQA
|
|
91
104
|
# @param [Hash] payload to be send
|
92
105
|
# @param [Hash] options for the request
|
93
106
|
#
|
94
|
-
# @return [
|
95
|
-
def identify(user_id, payload = {},
|
107
|
+
# @return [Boolean] status of the request
|
108
|
+
def identify(user_id, payload = {}, options = {})
|
96
109
|
return true unless configurations.enabled
|
97
110
|
|
98
111
|
payload[:type] = 'identify'
|
@@ -101,12 +114,15 @@ module LiveQA
|
|
101
114
|
|
102
115
|
payload = Message.extended.merge(payload)
|
103
116
|
|
104
|
-
if configurations.
|
105
|
-
return
|
117
|
+
if configurations.async
|
118
|
+
return processor.enqueue(
|
119
|
+
action: EVENTS_CREATE_ACTION,
|
120
|
+
payload: payload,
|
121
|
+
options: options.select { |k, _v| %i[space_name environment_name].include?(k) },
|
122
|
+
)
|
106
123
|
end
|
107
124
|
|
108
|
-
event = Event.create(payload,
|
109
|
-
|
125
|
+
event = Event.create(payload, options)
|
110
126
|
event.successful?
|
111
127
|
end
|
112
128
|
|
@@ -117,18 +133,22 @@ module LiveQA
|
|
117
133
|
# @param [Hash] payload to be send
|
118
134
|
# @param [Hash] options for the request
|
119
135
|
#
|
120
|
-
# @return [
|
121
|
-
def set_group(group_id, payload = {},
|
136
|
+
# @return [Boolean] status of the request
|
137
|
+
def set_group(group_id, payload = {}, options = {})
|
122
138
|
return true unless configurations.enabled
|
123
139
|
|
124
140
|
payload = Message.base.merge(payload)
|
125
141
|
|
126
|
-
if configurations.
|
127
|
-
return
|
142
|
+
if configurations.async
|
143
|
+
return processor.enqueue(
|
144
|
+
action: GROUPS_UPDATE_ACTION,
|
145
|
+
id: group_id,
|
146
|
+
payload: payload,
|
147
|
+
options: options.select { |k, _v| %i[space_name environment_name].include?(k) },
|
148
|
+
)
|
128
149
|
end
|
129
150
|
|
130
|
-
group = Group.update(group_id, payload,
|
131
|
-
|
151
|
+
group = Group.update(group_id, payload, options)
|
132
152
|
group.successful?
|
133
153
|
end
|
134
154
|
|
@@ -139,18 +159,22 @@ module LiveQA
|
|
139
159
|
# @param [Hash] payload to be send
|
140
160
|
# @param [Hash] options for the request
|
141
161
|
#
|
142
|
-
# @return [
|
143
|
-
def set_identity(user_id, payload = {},
|
162
|
+
# @return [Boolean] status of the request
|
163
|
+
def set_identity(user_id, payload = {}, options = {})
|
144
164
|
return true unless configurations.enabled
|
145
165
|
|
146
166
|
payload = Message.base.merge(payload)
|
147
167
|
|
148
|
-
if configurations.
|
149
|
-
return
|
168
|
+
if configurations.async
|
169
|
+
return processor.enqueue(
|
170
|
+
action: IDENTITIES_UPDATE_ACTION,
|
171
|
+
id: user_id,
|
172
|
+
payload: payload,
|
173
|
+
options: options.select { |k, _v| %i[space_name environment_name].include?(k) },
|
174
|
+
)
|
150
175
|
end
|
151
176
|
|
152
|
-
identity = Identity.update(user_id, payload,
|
153
|
-
|
177
|
+
identity = Identity.update(user_id, payload, options)
|
154
178
|
identity.successful?
|
155
179
|
end
|
156
180
|
|
@@ -161,8 +185,8 @@ module LiveQA
|
|
161
185
|
# @param [Hash] payload to be send
|
162
186
|
# @param [Hash] options for the request
|
163
187
|
#
|
164
|
-
# @return [
|
165
|
-
def watch(id_or_name, payload = {},
|
188
|
+
# @return [Boolean] status of the request
|
189
|
+
def watch(id_or_name, payload = {}, options = {})
|
166
190
|
return true unless configurations.enabled
|
167
191
|
|
168
192
|
payload[:template_flow] = id_or_name
|
@@ -170,14 +194,23 @@ module LiveQA
|
|
170
194
|
|
171
195
|
payload.delete(:session_tracker_id) if payload.delete(:without_session)
|
172
196
|
|
173
|
-
if configurations.
|
174
|
-
return
|
197
|
+
if configurations.async
|
198
|
+
return processor.enqueue(
|
199
|
+
action: WATCHERS_CREATE_ACTION,
|
200
|
+
payload: payload,
|
201
|
+
options: options.select { |k, _v| %i[space_name environment_name].include?(k) },
|
202
|
+
)
|
175
203
|
end
|
176
204
|
|
177
|
-
watcher = Watcher.create(payload,
|
178
|
-
|
205
|
+
watcher = Watcher.create(payload, options)
|
179
206
|
watcher.successful?
|
180
207
|
end
|
181
208
|
|
209
|
+
private
|
210
|
+
|
211
|
+
def processor
|
212
|
+
@processor ||= Processor::Async.new
|
213
|
+
end
|
214
|
+
|
182
215
|
end
|
183
216
|
end
|
data/liveqa.gemspec
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
lib = File.expand_path('
|
1
|
+
lib = File.expand_path('lib', __dir__)
|
2
2
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
3
3
|
require 'liveqa/version'
|
4
4
|
require 'liveqa/library_name'
|
@@ -19,8 +19,8 @@ Gem::Specification.new do |s|
|
|
19
19
|
s.test_files = `git ls-files -- spec/*`.split("\n")
|
20
20
|
|
21
21
|
s.add_development_dependency 'faker', '~> 1.8.4'
|
22
|
-
s.add_development_dependency 'rake', '>= 0.9.0'
|
23
22
|
s.add_development_dependency 'pry'
|
23
|
+
s.add_development_dependency 'rake', '>= 0.9.0'
|
24
24
|
s.add_development_dependency 'rspec', '~> 3.5'
|
25
|
-
s.add_development_dependency 'rubocop', '= 0.
|
25
|
+
s.add_development_dependency 'rubocop', '= 0.58.2'
|
26
26
|
end
|
@@ -12,7 +12,7 @@ describe LiveQA::APIResource do
|
|
12
12
|
let(:expected_payload) {{
|
13
13
|
method: :post,
|
14
14
|
url: 'http://localhost:4003/test',
|
15
|
-
payload: '
|
15
|
+
payload: match(''),
|
16
16
|
use_ssl: false,
|
17
17
|
headers: {
|
18
18
|
accept: 'application/json',
|
@@ -20,7 +20,7 @@ describe LiveQA::APIResource do
|
|
20
20
|
x_account_token: 'acc_xx',
|
21
21
|
x_space_name: 'LiveQA',
|
22
22
|
x_environment_name: 'test'
|
23
|
-
}
|
23
|
+
},
|
24
24
|
}}
|
25
25
|
|
26
26
|
after { resource }
|
@@ -32,7 +32,7 @@ describe LiveQA::APIResource do
|
|
32
32
|
let(:expected_payload) {{
|
33
33
|
method: :post,
|
34
34
|
url: 'http://localhost:4003/test',
|
35
|
-
payload: "
|
35
|
+
payload: match("\"test\":true"),
|
36
36
|
use_ssl: false,
|
37
37
|
headers: {
|
38
38
|
accept: 'application/json',
|
@@ -51,8 +51,8 @@ describe LiveQA::APIResource do
|
|
51
51
|
context 'with body and get' do
|
52
52
|
let(:expected_payload) {{
|
53
53
|
method: :get,
|
54
|
-
url:
|
55
|
-
payload: "
|
54
|
+
url: match("http://localhost:4003/test\\?test=true"),
|
55
|
+
payload: match("\"test\":true"),
|
56
56
|
use_ssl: false,
|
57
57
|
headers: {
|
58
58
|
accept: 'application/json',
|
@@ -71,8 +71,8 @@ describe LiveQA::APIResource do
|
|
71
71
|
context 'with overwrite headers tokens' do
|
72
72
|
let(:expected_payload) {{
|
73
73
|
method: :post,
|
74
|
-
url: 'http://localhost:4003/test',
|
75
|
-
payload: '
|
74
|
+
url: match('http://localhost:4003/test'),
|
75
|
+
payload: match(''),
|
76
76
|
use_ssl: false,
|
77
77
|
headers: {
|
78
78
|
accept: 'application/json',
|
@@ -101,7 +101,7 @@ describe LiveQA::APIResource do
|
|
101
101
|
context 'with Net::HTTPBadRequest' do
|
102
102
|
let(:response) { double('response', code: '400', code_type: Net::HTTPBadRequest, body: {}.to_json, message: 'failed') }
|
103
103
|
|
104
|
-
it { expect { resource }.
|
104
|
+
it { expect { resource }.to raise_error(LiveQA::RequestError) }
|
105
105
|
end
|
106
106
|
end
|
107
107
|
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe LiveQA::Batch do
|
4
|
+
|
5
|
+
describe '.create' do
|
6
|
+
let(:response) { double('LiveQA::Request', body: "{\"object\":\"event\",\"id\":41}") }
|
7
|
+
before { allow(LiveQA::Request).to receive(:execute).and_return(response) }
|
8
|
+
|
9
|
+
subject(:create) { LiveQA::Batch.create(data: []) }
|
10
|
+
|
11
|
+
it { is_expected.to be_successful }
|
12
|
+
end
|
13
|
+
|
14
|
+
end
|
@@ -10,7 +10,15 @@ describe LiveQA::Config do
|
|
10
10
|
|
11
11
|
%i[account_token environment_name api_host api_version].each do |field|
|
12
12
|
context "validate #{field}" do
|
13
|
-
let(:params) {
|
13
|
+
let(:params) {
|
14
|
+
{
|
15
|
+
account_token: 'acc_xx',
|
16
|
+
space_name: 'LiveQA',
|
17
|
+
environment_name: 'test',
|
18
|
+
api_host: 'host',
|
19
|
+
api_version: 'v1',
|
20
|
+
}.merge(field => '')
|
21
|
+
}
|
14
22
|
|
15
23
|
it { expect { config.valid! }.to raise_error(LiveQA::ConfigurationError, "#{field} can't be blank") }
|
16
24
|
end
|
@@ -24,19 +32,5 @@ describe LiveQA::Config do
|
|
24
32
|
it { expect(config.obfuscated_fields).to match_array(%w[another_password password_confirmation password access_token api_key authenticity_token ccv credit_card_number cvv secret secret_token token]) }
|
25
33
|
end
|
26
34
|
|
27
|
-
context 'async_handler' do
|
28
|
-
context 'sidekiq' do
|
29
|
-
let(:params) {{
|
30
|
-
account_token: 'acc_xx',
|
31
|
-
space_name: 'LiveQA',
|
32
|
-
environment_name: 'test',
|
33
|
-
async_handler: :sidekiq
|
34
|
-
}}
|
35
|
-
before { config.valid! }
|
36
|
-
|
37
|
-
it { expect(config.async_handler).to be_a(LiveQA::AsyncHandlers::Sidekiq) }
|
38
|
-
end
|
39
|
-
end
|
40
|
-
|
41
35
|
end
|
42
36
|
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe LiveQA::FormatedLogger do
|
4
|
+
let(:default_logger) { Logger.new(STDOUT) }
|
5
|
+
let(:logger) { LiveQA::FormatedLogger.build(default_logger) }
|
6
|
+
|
7
|
+
describe 'debug' do
|
8
|
+
|
9
|
+
context 'active' do
|
10
|
+
before do
|
11
|
+
allow(default_logger).to receive(:debug)
|
12
|
+
logger.debug('test')
|
13
|
+
end
|
14
|
+
|
15
|
+
it { expect(default_logger).to have_received(:debug).with('[LiveQA] test') }
|
16
|
+
end
|
17
|
+
|
18
|
+
context 'inactive' do
|
19
|
+
before do
|
20
|
+
LiveQA.configurations.log = false
|
21
|
+
allow(default_logger).to receive(:debug)
|
22
|
+
logger.debug('test')
|
23
|
+
end
|
24
|
+
|
25
|
+
it { expect(default_logger).to_not have_received(:debug) }
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
|
30
|
+
describe 'info' do
|
31
|
+
|
32
|
+
context 'active' do
|
33
|
+
before do
|
34
|
+
allow(default_logger).to receive(:info)
|
35
|
+
logger.info('test')
|
36
|
+
end
|
37
|
+
|
38
|
+
it { expect(default_logger).to have_received(:info).with('[LiveQA] test') }
|
39
|
+
end
|
40
|
+
|
41
|
+
context 'inactive' do
|
42
|
+
before do
|
43
|
+
LiveQA.configurations.log = false
|
44
|
+
allow(default_logger).to receive(:info)
|
45
|
+
logger.info('test')
|
46
|
+
end
|
47
|
+
|
48
|
+
it { expect(default_logger).to_not have_received(:info) }
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
52
|
+
|
53
|
+
describe 'info' do
|
54
|
+
|
55
|
+
context 'active' do
|
56
|
+
before do
|
57
|
+
allow(default_logger).to receive(:info)
|
58
|
+
logger.info('test')
|
59
|
+
end
|
60
|
+
|
61
|
+
it { expect(default_logger).to have_received(:info).with('[LiveQA] test') }
|
62
|
+
end
|
63
|
+
|
64
|
+
context 'inactive' do
|
65
|
+
before do
|
66
|
+
LiveQA.configurations.log = false
|
67
|
+
allow(default_logger).to receive(:info)
|
68
|
+
logger.info('test')
|
69
|
+
end
|
70
|
+
|
71
|
+
it { expect(default_logger).to_not have_received(:info) }
|
72
|
+
end
|
73
|
+
|
74
|
+
end
|
75
|
+
|
76
|
+
describe 'error' do
|
77
|
+
|
78
|
+
context 'active' do
|
79
|
+
before do
|
80
|
+
allow(default_logger).to receive(:error)
|
81
|
+
logger.error('test')
|
82
|
+
end
|
83
|
+
|
84
|
+
it { expect(default_logger).to have_received(:error).with('[LiveQA] test') }
|
85
|
+
end
|
86
|
+
|
87
|
+
context 'inactive' do
|
88
|
+
before do
|
89
|
+
LiveQA.configurations.log = false
|
90
|
+
allow(default_logger).to receive(:error)
|
91
|
+
logger.error('test')
|
92
|
+
end
|
93
|
+
|
94
|
+
it { expect(default_logger).to_not have_received(:error) }
|
95
|
+
end
|
96
|
+
|
97
|
+
end
|
98
|
+
|
99
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe LiveQA::Processor::Async do
|
4
|
+
|
5
|
+
let(:processor) { LiveQA::Processor::Async.new }
|
6
|
+
|
7
|
+
context 'enqueue a message' do
|
8
|
+
|
9
|
+
before do
|
10
|
+
allow(processor).to receive(:ensure_worker_running)
|
11
|
+
processor.enqueue({ test: 'hello' })
|
12
|
+
end
|
13
|
+
|
14
|
+
it { expect(processor).to have_received(:ensure_worker_running) }
|
15
|
+
it { expect(processor.instance_variable_get(:@queue).length).to eq(1) }
|
16
|
+
end
|
17
|
+
|
18
|
+
|
19
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe LiveQA::Processor::Batch do
|
4
|
+
let(:batch) { LiveQA::Processor::Batch.new }
|
5
|
+
|
6
|
+
context 'set defaults' do
|
7
|
+
it { expect(batch.messages).to eq([]) }
|
8
|
+
it { expect(batch.can_run?).to be_truthy }
|
9
|
+
it { expect(batch.can_retry?).to be_truthy }
|
10
|
+
it { expect(batch.full?).to be_falsey }
|
11
|
+
end
|
12
|
+
|
13
|
+
context 'add message' do
|
14
|
+
before { batch << { test: 'hello' }}
|
15
|
+
|
16
|
+
it { expect(batch.messages).to eq([{ test: 'hello' }]) }
|
17
|
+
it { expect(batch.full?).to be_falsey }
|
18
|
+
end
|
19
|
+
|
20
|
+
context 'with extended task' do
|
21
|
+
before { batch.update_retry }
|
22
|
+
|
23
|
+
it { expect(batch.can_run?).to be_falsey }
|
24
|
+
it { expect(batch.can_retry?).to be_truthy }
|
25
|
+
end
|
26
|
+
|
27
|
+
context 'with extended task' do
|
28
|
+
before do
|
29
|
+
11.times { batch.update_retry }
|
30
|
+
end
|
31
|
+
|
32
|
+
it { expect(batch.can_run?).to be_falsey }
|
33
|
+
it { expect(batch.can_retry?).to be_falsey }
|
34
|
+
end
|
35
|
+
|
36
|
+
context 'with max number of messages reached' do
|
37
|
+
before { 100.times { batch << { test: 'hello' }}}
|
38
|
+
|
39
|
+
it { expect(batch.full?).to be_truthy }
|
40
|
+
end
|
41
|
+
|
42
|
+
context 'with max size reached' do
|
43
|
+
before { 20.times { batch << 1000.times.map {{ test: 'hello' }}}}
|
44
|
+
|
45
|
+
it { expect(batch.full?).to be_truthy }
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe LiveQA::Processor::Worker do
|
4
|
+
let(:queue) { Queue.new }
|
5
|
+
let(:worker) { LiveQA::Processor::Worker.new(queue) }
|
6
|
+
|
7
|
+
context 'do nothing' do
|
8
|
+
before do
|
9
|
+
allow(worker).to receive(:send_batches)
|
10
|
+
worker.run
|
11
|
+
end
|
12
|
+
|
13
|
+
it { expect(worker).not_to have_received(:send_batches) }
|
14
|
+
end
|
15
|
+
|
16
|
+
context 'with something in the queue' do
|
17
|
+
before do
|
18
|
+
queue << { test: true }
|
19
|
+
allow(LiveQA::Batch).to receive(:create)
|
20
|
+
worker.run
|
21
|
+
end
|
22
|
+
|
23
|
+
it { expect(LiveQA::Batch).to have_received(:create) }
|
24
|
+
end
|
25
|
+
end
|