remi 0.2.42 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (94) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +7 -0
  3. data/Gemfile +1 -1
  4. data/Gemfile.lock +13 -26
  5. data/README.md +1 -1
  6. data/features/step_definitions/remi_step.rb +33 -13
  7. data/features/sub_job_example.feature +24 -0
  8. data/features/sub_transform_example.feature +35 -0
  9. data/features/sub_transform_many_to_many.feature +49 -0
  10. data/features/support/env_app.rb +1 -1
  11. data/jobs/all_jobs_shared.rb +19 -16
  12. data/jobs/copy_source_job.rb +11 -9
  13. data/jobs/csv_file_target_job.rb +10 -9
  14. data/jobs/json_job.rb +18 -14
  15. data/jobs/metadata_job.rb +33 -28
  16. data/jobs/parameters_job.rb +14 -11
  17. data/jobs/sample_job.rb +106 -77
  18. data/jobs/sftp_file_target_job.rb +14 -13
  19. data/jobs/sub_job_example_job.rb +86 -0
  20. data/jobs/sub_transform_example_job.rb +43 -0
  21. data/jobs/sub_transform_many_to_many_job.rb +46 -0
  22. data/jobs/transforms/concatenate_job.rb +16 -12
  23. data/jobs/transforms/data_frame_sieve_job.rb +24 -19
  24. data/jobs/transforms/date_diff_job.rb +15 -11
  25. data/jobs/transforms/nvl_job.rb +16 -12
  26. data/jobs/transforms/parse_date_job.rb +17 -14
  27. data/jobs/transforms/partitioner_job.rb +27 -19
  28. data/jobs/transforms/prefix_job.rb +13 -10
  29. data/jobs/transforms/truncate_job.rb +14 -10
  30. data/jobs/transforms/truthy_job.rb +11 -8
  31. data/lib/remi.rb +25 -11
  32. data/lib/remi/data_frame.rb +4 -4
  33. data/lib/remi/data_frame/daru.rb +1 -37
  34. data/lib/remi/data_subject.rb +234 -48
  35. data/lib/remi/data_subjects/csv_file.rb +171 -0
  36. data/lib/remi/data_subjects/data_frame.rb +106 -0
  37. data/lib/remi/data_subjects/file_system.rb +115 -0
  38. data/lib/remi/data_subjects/local_file.rb +109 -0
  39. data/lib/remi/data_subjects/none.rb +31 -0
  40. data/lib/remi/data_subjects/postgres.rb +186 -0
  41. data/lib/remi/data_subjects/s3_file.rb +84 -0
  42. data/lib/remi/data_subjects/salesforce.rb +211 -0
  43. data/lib/remi/data_subjects/sftp_file.rb +196 -0
  44. data/lib/remi/data_subjects/sub_job.rb +50 -0
  45. data/lib/remi/dsl.rb +74 -0
  46. data/lib/remi/encoder.rb +45 -0
  47. data/lib/remi/extractor.rb +21 -0
  48. data/lib/remi/field_symbolizers.rb +1 -0
  49. data/lib/remi/job.rb +279 -113
  50. data/lib/remi/job/parameters.rb +90 -0
  51. data/lib/remi/job/sub_job.rb +35 -0
  52. data/lib/remi/job/transform.rb +165 -0
  53. data/lib/remi/loader.rb +22 -0
  54. data/lib/remi/monkeys/daru.rb +4 -0
  55. data/lib/remi/parser.rb +44 -0
  56. data/lib/remi/testing/business_rules.rb +17 -23
  57. data/lib/remi/testing/data_stub.rb +2 -2
  58. data/lib/remi/version.rb +1 -1
  59. data/remi.gemspec +3 -0
  60. data/spec/data_subject_spec.rb +475 -11
  61. data/spec/data_subjects/csv_file_spec.rb +69 -0
  62. data/spec/data_subjects/data_frame_spec.rb +52 -0
  63. data/spec/{extractor → data_subjects}/file_system_spec.rb +0 -0
  64. data/spec/{extractor → data_subjects}/local_file_spec.rb +0 -0
  65. data/spec/data_subjects/none_spec.rb +41 -0
  66. data/spec/data_subjects/postgres_spec.rb +80 -0
  67. data/spec/{extractor → data_subjects}/s3_file_spec.rb +0 -0
  68. data/spec/data_subjects/salesforce_spec.rb +117 -0
  69. data/spec/{extractor → data_subjects}/sftp_file_spec.rb +16 -0
  70. data/spec/data_subjects/sub_job_spec.rb +33 -0
  71. data/spec/encoder_spec.rb +38 -0
  72. data/spec/extractor_spec.rb +11 -0
  73. data/spec/fixtures/sf_bulk_helper_stubs.rb +443 -0
  74. data/spec/job/transform_spec.rb +257 -0
  75. data/spec/job_spec.rb +507 -0
  76. data/spec/loader_spec.rb +11 -0
  77. data/spec/parser_spec.rb +38 -0
  78. data/spec/sf_bulk_helper_spec.rb +117 -0
  79. data/spec/testing/data_stub_spec.rb +5 -3
  80. metadata +109 -27
  81. data/features/aggregate.feature +0 -42
  82. data/jobs/aggregate_job.rb +0 -31
  83. data/jobs/transforms/transform_jobs.rb +0 -4
  84. data/lib/remi/data_subject/csv_file.rb +0 -162
  85. data/lib/remi/data_subject/data_frame.rb +0 -52
  86. data/lib/remi/data_subject/postgres.rb +0 -134
  87. data/lib/remi/data_subject/salesforce.rb +0 -136
  88. data/lib/remi/data_subject/sftp_file.rb +0 -65
  89. data/lib/remi/extractor/file_system.rb +0 -92
  90. data/lib/remi/extractor/local_file.rb +0 -43
  91. data/lib/remi/extractor/s3_file.rb +0 -57
  92. data/lib/remi/extractor/sftp_file.rb +0 -83
  93. data/spec/data_subject/csv_file_spec.rb +0 -79
  94. data/spec/data_subject/data_frame.rb +0 -27
@@ -2,13 +2,13 @@ module Remi
2
2
  module Testing
3
3
  module DataStub
4
4
  def stub_row_array
5
- @fields.values.map do |attribs|
5
+ fields.values.map do |attribs|
6
6
  stub_values(attribs)
7
7
  end
8
8
  end
9
9
 
10
10
  def empty_stub_df
11
- self.df = Daru::DataFrame.new([], order: @fields.keys)
11
+ self.df = Daru::DataFrame.new([], order: fields.keys)
12
12
  end
13
13
 
14
14
  def stub_df
@@ -1,3 +1,3 @@
1
1
  module Remi
2
- VERSION = '0.2.42'
2
+ VERSION = '0.3.0'
3
3
  end
@@ -34,6 +34,9 @@ Gem::Specification.new do |s|
34
34
  # s.add_runtime_dependency 'salesforce_bulk_api', ['0.0.12']
35
35
 
36
36
  s.add_development_dependency 'iruby', ['0.2.7']
37
+ s.add_development_dependency 'yard', ['~> 0.9']
38
+ s.add_development_dependency 'redcarpet', ['~> 3.3']
39
+ s.add_development_dependency 'github-markup', ['~> 1.4']
37
40
 
38
41
  s.files = `git ls-files`.split("\n")
39
42
  s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
@@ -1,23 +1,96 @@
1
1
  require_relative 'remi_spec'
2
2
 
3
- # VERY SPARSE TESTING! DO MORE!
4
-
5
3
  describe DataSubject do
4
+ let(:data_subject) { DataSubject.new(name: :awesome_subject) }
5
+
6
+ context 'DSL' do
7
+ let(:dsl_data_subject) do
8
+ DataSubject.new(name: :awesome_dsl_subject) do
9
+ fields :id => {}
10
+ field_symbolizer :salesforce
11
+ end
12
+ end
13
+
14
+ it 'defines the fields' do
15
+ expect(dsl_data_subject.dsl_eval.fields).to eq({ :id => {} })
16
+ end
17
+
18
+ it 'sets the field symbolizer' do
19
+ expect(dsl_data_subject.dsl_eval.field_symbolizer).to eq(Remi::FieldSymbolizers[:salesforce])
20
+ end
21
+ end
22
+
23
+ it 'has a name' do
24
+ expect(data_subject.name).to eq :awesome_subject
25
+ end
26
+
27
+ context '#df_type' do
28
+ it 'returns the dataframe type' do
29
+ expect(data_subject.df_type).to eq :daru
30
+ end
31
+
32
+ it 'sets the dataframe type' do
33
+ expect { data_subject.df_type(:spark) }.to change {
34
+ data_subject.df_type
35
+ }.from(:daru).to(:spark)
36
+ end
37
+ end
38
+
39
+ context '#fields' do
40
+ it 'returns the field metadata' do
41
+ expect(data_subject.fields).to be_a Remi::Fields
42
+ end
43
+
44
+ it 'sets the field metadata' do
45
+ expect { data_subject.fields({ :id => {} }) }.to change {
46
+ data_subject.fields
47
+ }.from({}).to({ :id => {} })
48
+ end
49
+ end
50
+
51
+ context '#field_symbolizer' do
52
+ it 'returns the field symbolizer defined for this source' do
53
+ expect(data_subject.field_symbolizer).to eq Remi::FieldSymbolizers[:standard]
54
+ end
55
+
56
+ it 'sets the field symbolizer' do
57
+ data_subject.field_symbolizer :salesforce
58
+ expect(data_subject.field_symbolizer).to eq Remi::FieldSymbolizers[:salesforce]
59
+ end
60
+ end
6
61
 
7
- describe 'enforcing types' do
8
- let(:dataframe) do
9
- Remi::DataFrame::Daru.new({ my_date: ['10/21/2015'] })
62
+ context '#df' do
63
+ it 'returns the dataframe associated with this subject' do
64
+ expect(data_subject.df).to be_a Remi::DataFrame::Daru
10
65
  end
66
+ end
11
67
 
12
- let(:data_subject) do
13
- DataSubject.new(fields: fields).tap { |ds| ds.df = dataframe }
68
+ context '#df=' do
69
+ let(:reassigned_df) { Daru::DataFrame.new({ a: [1955] }) }
70
+ it 'reassigns the dataframe associated with this subject' do
71
+ data_subject.df = reassigned_df
72
+ expect(data_subject.df).to eq reassigned_df
14
73
  end
15
74
 
75
+ it 'converts any non-remi dataframes to a remi dataframe' do
76
+ data_subject.df = reassigned_df
77
+ expect(data_subject.df).to be_a Remi::DataFrame::Daru
78
+ end
79
+ end
80
+
81
+ context '#enforce_types' do
82
+ let(:dataframe) { Remi::DataFrame::Daru.new({ my_date: ['10/21/2015'] }) }
83
+
16
84
  let(:fields) do
17
- Fields.new({
18
- my_date: { type: :date, in_format: '%m/%d/%Y' },
85
+ {
86
+ my_date: { type: :date, in_format: '%m/%d/%Y' },
19
87
  other_date: { type: :date, in_format: '%m/%d/%Y' }
20
- })
88
+ }
89
+ end
90
+
91
+ before do
92
+ data_subject.fields = fields
93
+ data_subject.df = dataframe
21
94
  end
22
95
 
23
96
  it 'converts a date string to a date using an in_format' do
@@ -25,12 +98,22 @@ describe DataSubject do
25
98
  expect(data_subject.df[:my_date].to_a).to eq [Date.new(2015, 10, 21)]
26
99
  end
27
100
 
28
- it 'does not do any conversion if the type is not specified' do
101
+ it 'converts types when explicitly specified' do
102
+ data_subject.enforce_types(:date)
103
+ expect(data_subject.df[:my_date].to_a).to eq [Date.new(2015, 10, 21)]
104
+ end
105
+
106
+ it 'does not do any conversion if the field has no type specified' do
29
107
  fields[:my_date].delete(:type)
30
108
  data_subject.enforce_types
31
109
  expect(data_subject.df[:my_date].to_a).to eq ['10/21/2015']
32
110
  end
33
111
 
112
+ it 'does not do any conversion if field metadata does not match the selected enforcement type' do
113
+ data_subject.enforce_types(:decimal)
114
+ expect(data_subject.df[:my_date].to_a).to eq ['10/21/2015']
115
+ end
116
+
34
117
  it 'throws an error if the data does not conform to its type' do
35
118
  dataframe[:my_date].recode! { |v| '2015-10-21' }
36
119
  expect { data_subject.enforce_types }.to raise_error ArgumentError
@@ -42,3 +125,384 @@ describe DataSubject do
42
125
  end
43
126
  end
44
127
  end
128
+
129
+
130
+
131
+
132
+ describe DataSource do
133
+ let(:data_source) { DataSource.new }
134
+
135
+ let(:my_extractor) { double('my_extractor') }
136
+ let(:my_extractor2) { double('my_extractor2') }
137
+ let(:my_parser) { Remi::Parser.new }
138
+
139
+
140
+ before do
141
+ allow(my_extractor).to receive(:extract) .and_return 'result_1'
142
+ allow(my_extractor2).to receive(:extract) .and_return 'result_2'
143
+ allow(my_parser).to receive(:parse)
144
+ end
145
+
146
+
147
+ context 'DSL' do
148
+ let(:dsl_data_source) do
149
+ scoped_my_extractor = my_extractor
150
+ scoped_my_extractor2 = my_extractor2
151
+ scoped_my_parser = my_parser
152
+
153
+ DataSource.new(name: :awesome_dsl_source) do
154
+ extractor scoped_my_extractor
155
+ extractor scoped_my_extractor2
156
+ parser scoped_my_parser
157
+ end
158
+ end
159
+
160
+ it 'adds extractors to the list of extractors' do
161
+ expect(dsl_data_source.dsl_eval.extractors).to eq [my_extractor, my_extractor2]
162
+ end
163
+
164
+ it 'sets the parser' do
165
+ expect(dsl_data_source.dsl_eval.parser).to eq my_parser
166
+ end
167
+
168
+ context '#df' do
169
+ it 'executes the DSL commands that have been declared' do
170
+ expect(my_extractor).to receive :extract
171
+ expect(my_extractor2).to receive :extract
172
+ expect(my_parser).to receive :parse
173
+ dsl_data_source.df
174
+ end
175
+ end
176
+
177
+ context '#field_symbolizer' do
178
+ context 'field_symbolizer called before parser' do
179
+ let(:before_parser) do
180
+ scoped_my_parser = my_parser
181
+ DataSource.new do
182
+ field_symbolizer :salesforce
183
+ parser scoped_my_parser
184
+ end
185
+ end
186
+
187
+ it 'is used to set the field_symbolizer of the parser' do
188
+ expect {
189
+ before_parser.dsl_eval
190
+ }.to change {
191
+ my_parser.field_symbolizer
192
+ }.from(Remi::FieldSymbolizers[:standard]).to(Remi::FieldSymbolizers[:salesforce])
193
+ end
194
+ end
195
+
196
+ context 'field_symbolizer called after parser' do
197
+ let(:after_parser) do
198
+ scoped_my_parser = my_parser
199
+ DataSource.new do
200
+ parser scoped_my_parser
201
+ field_symbolizer :salesforce
202
+ end
203
+ end
204
+
205
+ it 'sets the field symbolizer of the parser for any parsers defined above' do
206
+ expect {
207
+ after_parser.dsl_eval
208
+ }.to change {
209
+ my_parser.field_symbolizer
210
+ }.from(Remi::FieldSymbolizers[:standard]).to(Remi::FieldSymbolizers[:salesforce])
211
+ end
212
+ end
213
+ end
214
+ end
215
+
216
+ context '#extractor' do
217
+ before { data_source.extractor 'my_extractor' }
218
+
219
+ it 'adds an extractor to the list of extractors' do
220
+ expect(data_source.extractors).to eq ['my_extractor']
221
+ end
222
+
223
+ it 'allows for multiple extractors to be defined' do
224
+ data_source.extractor 'my_extractor2'
225
+ expect(data_source.extractors).to eq ['my_extractor', 'my_extractor2']
226
+ end
227
+ end
228
+
229
+ context '#parser' do
230
+ let(:my_parser) { Remi::Parser.new }
231
+
232
+ context 'default parser' do
233
+ it 'uses the None parser' do
234
+ expect(data_source.parser).to be_a Parser::None
235
+ end
236
+ end
237
+
238
+ context 'defining a parser' do
239
+ before { data_source.parser my_parser }
240
+
241
+ it 'sets the parser' do
242
+ expect(data_source.parser).to eq my_parser
243
+ end
244
+
245
+ it 'only allows one parser to be defined' do
246
+ my_new_parser = my_parser.clone
247
+ data_source.parser my_new_parser
248
+ expect(data_source.parser).to eq my_new_parser
249
+ end
250
+
251
+ it 'sets the context of parser' do
252
+ data_source.parser my_parser
253
+ expect(my_parser.context).to eq data_source
254
+ end
255
+ end
256
+ end
257
+
258
+ context 'with parsers and extractors defined' do
259
+ before do
260
+ data_source.extractor my_extractor
261
+ data_source.extractor my_extractor2
262
+ data_source.parser my_parser
263
+ end
264
+
265
+ context '#extract' do
266
+ it 'extracts data from each extractor' do
267
+ expect(my_extractor).to receive :extract
268
+ expect(my_extractor2).to receive :extract
269
+ data_source.extract
270
+ end
271
+
272
+ it 'collects the results of each extractor' do
273
+ expect(data_source.extract).to eq ['result_1', 'result_2']
274
+ end
275
+ end
276
+
277
+ context '#parse' do
278
+ it 'uses the specified parser to parse the extracted data' do
279
+ expect(my_parser).to receive(:parse) .with('result_1', 'result_2')
280
+ data_source.parse
281
+ end
282
+ end
283
+
284
+ context '#df' do
285
+ context 'a dataframe has not already been defined' do
286
+
287
+ it 'extracts' do
288
+ expect(data_source).to receive :extract
289
+ data_source.df
290
+ end
291
+
292
+ it 'parses' do
293
+ expect(data_source).to receive :parse
294
+ data_source.df
295
+ end
296
+ end
297
+
298
+ context 'a dataframe has already been defined' do
299
+ let(:dataframe) do
300
+ df = double('df')
301
+ allow(df).to receive :df_type
302
+ df
303
+ end
304
+ before { data_source.df = dataframe }
305
+
306
+ it 'simply returns the defined dataframe' do
307
+ expect(data_source.df).to eq dataframe
308
+ end
309
+
310
+ it 'does not extract' do
311
+ expect(data_source).not_to receive :extract
312
+ data_source.df
313
+ end
314
+
315
+ it 'does not parse' do
316
+ expect(data_source).not_to receive :parse
317
+ data_source.df
318
+ end
319
+ end
320
+ end
321
+
322
+ context '#reset', skip: 'todo' do
323
+ it 'clears the current dataframe' do
324
+ end
325
+
326
+ it 'allows the source data to be extracted and parsed again' do
327
+ end
328
+ end
329
+ end
330
+ end
331
+
332
+
333
+ describe DataTarget do
334
+ let(:data_target) { DataTarget.new }
335
+
336
+ let(:my_encoder) { Remi::Encoder.new }
337
+ let(:my_loader) { double('my_loader') }
338
+ let(:my_loader2) { double('my_loader2') }
339
+
340
+ before do
341
+ allow(my_loader).to receive(:load)
342
+ allow(my_loader2).to receive(:load)
343
+ allow(my_encoder).to receive(:encode) .and_return 'encoded data'
344
+ end
345
+
346
+ context 'DSL' do
347
+ let(:dsl_data_target) do
348
+ scoped_my_encoder = my_encoder
349
+ scoped_my_loader = my_loader
350
+ scoped_my_loader2 = my_loader2
351
+
352
+ DataTarget.new do
353
+ encoder scoped_my_encoder
354
+ loader scoped_my_loader
355
+ loader scoped_my_loader2
356
+ end
357
+ end
358
+
359
+ it 'adds loaders to the list of loaders' do
360
+ expect(dsl_data_target.dsl_eval.loaders).to eq [my_loader, my_loader2]
361
+ end
362
+
363
+ it 'sets the encoder' do
364
+ expect(dsl_data_target.dsl_eval.encoder).to eq my_encoder
365
+ end
366
+
367
+ context '#load' do
368
+ it 'executes the DSL commands that have been declared' do
369
+ df_double = double('df')
370
+ allow(df_double).to receive(:size) .and_return(1)
371
+
372
+ allow(dsl_data_target).to receive(:df) .and_return(df_double)
373
+
374
+ expect(my_encoder).to receive :encode
375
+ expect(my_loader).to receive :load
376
+ expect(my_loader2).to receive :load
377
+ dsl_data_target.load
378
+ end
379
+ end
380
+
381
+
382
+ context '#field_symbolizer' do
383
+ context 'field_symbolizer called before encoder' do
384
+ let(:before_encoder) do
385
+ scoped_my_encoder = my_encoder
386
+ DataTarget.new do
387
+ field_symbolizer :salesforce
388
+ encoder scoped_my_encoder
389
+ end
390
+ end
391
+
392
+ it 'is used to set the field_symbolizer of the encoder' do
393
+ expect {
394
+ before_encoder.dsl_eval
395
+ }.to change {
396
+ my_encoder.field_symbolizer
397
+ }.from(Remi::FieldSymbolizers[:standard]).to(Remi::FieldSymbolizers[:salesforce])
398
+ end
399
+ end
400
+
401
+ context 'field_symbolizer called after encoder' do
402
+ let(:after_encoder) do
403
+ scoped_my_encoder = my_encoder
404
+ DataTarget.new do
405
+ encoder scoped_my_encoder
406
+ field_symbolizer :salesforce
407
+ end
408
+ end
409
+
410
+ it 'sets the field symbolizer of the encoder for any encoders defined above' do
411
+ expect {
412
+ after_encoder.dsl_eval
413
+ }.to change {
414
+ my_encoder.field_symbolizer
415
+ }.from(Remi::FieldSymbolizers[:standard]).to(Remi::FieldSymbolizers[:salesforce])
416
+ end
417
+ end
418
+ end
419
+ end
420
+
421
+ context '#encoder' do
422
+ let(:my_encoder) { Remi::Encoder.new }
423
+
424
+ context 'default encoder' do
425
+ it 'uses the None encoder' do
426
+ expect(data_target.encoder).to be_a Encoder::None
427
+ end
428
+ end
429
+
430
+ context 'defining an encoder' do
431
+ before { data_target.encoder my_encoder }
432
+
433
+ it 'sets the encoder' do
434
+ expect(data_target.encoder).to eq my_encoder
435
+ end
436
+
437
+ it 'only allows one encoder to be defined' do
438
+ my_new_encoder = my_encoder.clone
439
+ data_target.encoder my_new_encoder
440
+ expect(data_target.encoder).to eq my_new_encoder
441
+ end
442
+
443
+ it 'sets the context of encoder' do
444
+ data_target.encoder my_encoder
445
+ expect(my_encoder.context).to eq data_target
446
+ end
447
+ end
448
+ end
449
+
450
+ context '#loader' do
451
+ before { data_target.loader 'my_loader' }
452
+
453
+ it 'adds a loader to the list of loaders' do
454
+ expect(data_target.loaders).to eq ['my_loader']
455
+ end
456
+
457
+ it 'allows for multiple loaders to be defined' do
458
+ data_target.loader 'my_loader2'
459
+ expect(data_target.loaders).to eq ['my_loader', 'my_loader2']
460
+ end
461
+ end
462
+
463
+ context '#load' do
464
+ before do
465
+ data_target.encoder my_encoder
466
+ data_target.loader my_loader
467
+ data_target.loader my_loader2
468
+
469
+ df_double = double('df')
470
+ allow(df_double).to receive(:size) .and_return(1)
471
+
472
+ allow(data_target).to receive(:df) .and_return(df_double)
473
+ end
474
+
475
+ it 'encodes data represented in the dataframe' do
476
+ expect(my_encoder).to receive(:encode).once
477
+ data_target.load
478
+ end
479
+
480
+ it 'passes encoded data to each of the loaders' do
481
+ expect(my_loader).to receive(:load).with('encoded data')
482
+ expect(my_loader2).to receive(:load).with('encoded data')
483
+ data_target.load
484
+ end
485
+
486
+ it 'triggers a load for all of the loaders' do
487
+ expect(my_loader).to receive(:load).once
488
+ expect(my_loader2).to receive(:load).once
489
+ data_target.load
490
+ end
491
+
492
+ it 'does not trigger loads twice' do
493
+ expect(my_loader).to receive(:load).once
494
+ expect(my_loader2).to receive(:load).once
495
+ data_target.load
496
+ data_target.load
497
+ end
498
+
499
+ context '#load!' do
500
+ it 'triggers loads every time it is called' do
501
+ expect(my_loader).to receive(:load).twice
502
+ expect(my_loader2).to receive(:load).twice
503
+ data_target.load!
504
+ data_target.load!
505
+ end
506
+ end
507
+ end
508
+ end