data_works 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 0e62f25e7b770a6d1514caae52a22b905b672b31
4
+ data.tar.gz: fe2201c91ce8f997f748a3d3aa39b3b645c0e984
5
+ SHA512:
6
+ metadata.gz: f97c7e9eb712b0dbe669533dcc7b508bbaf0f0b3a35c9e72b74e1dd43e4a2d076b170fe5a3b131c43f0bad7e7c40c0d2b74eba2a6694411fcf9b4e7159c9f06a
7
+ data.tar.gz: c9f7f51c4abfde40ebe6658b6ca739b721fbd059b6a682c1461adca6b7594b2e99a4c78f469b1c384d5b96a3d1f4582b3cdde1098696a92c3daef253764e6672
@@ -0,0 +1,24 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ Guardfile
8
+ InstalledFiles
9
+ _yardoc
10
+ coverage
11
+ doc/
12
+ lib/bundler/man
13
+ pkg
14
+ rdoc
15
+ spec/reports
16
+ test/tmp
17
+ test/version_tmp
18
+ tmp
19
+ *.bundle
20
+ *.so
21
+ *.o
22
+ *.a
23
+ mkmf.log
24
+ .idea
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --order rand
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in copyable.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2018 District Management Group
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,529 @@
1
+ # DataWorks
2
+
3
+ DataWorks makes it easier to work with FactoryGirl in the context of a complex
4
+ data model.
5
+
6
+ DataWorks has these benefits:
7
+
8
+ * Reduces test data bloat.
9
+ * Improves test clarity.
10
+ * Improves test correctness.
11
+ * Reduces refactoring cost when changing the data model.
12
+ * Improves test performance.
13
+
14
+
15
+ ## Contents
16
+
17
+ * [The Problem That DataWorks Solves](#the-problem-that-dataworks-solves)
18
+ * [How DataWorks Solves This Problem](#how-dataworks-solves-this-problem)
19
+ * [Approach](#approach)
20
+ * [Implementation](#implementation)
21
+ * [Benefits](#benefits)
22
+ * [Usage](#usage)
23
+ * [Basic Example](#basic-example)
24
+ * [Referencing Objects](#referencing-objects)
25
+ * [Adding Multiple Objects](#adding-multiple-objects)
26
+ * [Avoiding Object Reuse](#avoiding-object-reuse)
27
+ * [Overriding Attributes](#overriding-attributes)
28
+ * [Better Variable Names](#better-variable-names)
29
+ * [Visualizing the Object Graph](#visualizing-the-object-graph)
30
+ * [Setup and Configuration](#setup-and-configuration)
31
+ * [Configuration](#configuration)
32
+ * [Subclassing DataWorks::Base](#subclassing-dataworksbase)
33
+ * [Issues](#issues)
34
+
35
+
36
+
37
+ ## The Problem That DataWorks Solves
38
+
39
+ Consider the following data model:
40
+
41
+
42
+ ,----------,
43
+ | |
44
+ .----(1) County (1)----.
45
+ | | | |
46
+ | '----------' |
47
+ | |
48
+ | |
49
+ ,---(*)----, ,---(*)----,
50
+ | | | |
51
+ | Person (*)---------(1) School |
52
+ | | | |
53
+ '----------' '----------'
54
+
55
+
56
+ * A Person must live in a County.
57
+ * A Person can optionally attend a School.
58
+ * A School must belong to a County.
59
+
60
+ Let's say we need to test the method School#number_of_people. So we factory
61
+ two people and a school and add the people to the school. Then we test
62
+ that number_of_people returns two. The problem is that our object graph
63
+ would look like this:
64
+
65
+
66
+ ,----------, ,----------, ,----------,
67
+ | | | | | |
68
+ | County | | County | | County |
69
+ | | | | | |
70
+ '----.-----' '----.-----' '----.-----'
71
+ | | |
72
+ | | |
73
+ ,----'-----, ,----'-----, ,----'-----,
74
+ | | | | | |
75
+ | Person | | Person |------| School |
76
+ | | | | | |
77
+ '----.-----' '----------' '----.-----'
78
+ | |
79
+ `-----------------------------------'
80
+
81
+
82
+ Each Person object and the School object would have their own individual
83
+ county because the factories don't know that they should all be the same
84
+ county. Now this probably won't affect the accuracy of a simple method
85
+ like number_of_people, but in the case of more complex business logic,
86
+ having an incorrect object graph could cause the test to fail even though
87
+ the code under test is correct. So we need to make sure our factory
88
+ objects reflect a valid state of the system.
89
+
90
+ The takeaway is that we need to manually factory a County object and pass
91
+ it to the Person and School factories to ensure that only one County object
92
+ exists.
93
+
94
+ Now consider a more complex data model:
95
+
96
+
97
+ ,-----------,
98
+ | |
99
+ | Region |
100
+ | |
101
+ '----(1)----'
102
+ |
103
+ |
104
+ |
105
+ ,----(*)----,
106
+ | |
107
+ .----(1) State (1)----.
108
+ | | | |
109
+ | '-----------' |
110
+ | |
111
+ | |
112
+ ,----(*)---, ,----(*)---, ,------------------,
113
+ | | | | | |
114
+ | Town | .----(1) County (1)------(1) SchoolDistrict |
115
+ | | | | | | |
116
+ '----(1)---' | '-----.----' '-------(1)--------'
117
+ | | |
118
+ | ,------' |
119
+ | | |
120
+ ,----(*)--(*) ,---(*)----,
121
+ | | | |
122
+ | Person (*)----------------------------------(1) School |
123
+ | | | |
124
+ '----------' '----------'
125
+
126
+
127
+ * A Person must live in a County.
128
+ * A Person must live in a Town.
129
+ * Towns can span Counties.
130
+ * A Town must be in a State.
131
+ * A County must be in a State.
132
+ * A State must be in a Region.
133
+ * A County has one and only one SchoolDistrict.
134
+ * A SchoolDistrict has many Schools.
135
+ * A Person can optionally attend a School.
136
+
137
+ Let's say we need to test the method School#number_of_people. So we factory
138
+ a couple of Person models and a School model. But if that's all we did,
139
+ we'll have a proliferation of separate parent models, such as three separate
140
+ County objects, five separate State objects, and five separate Region
141
+ objects.
142
+
143
+ In order to craft accurate test data, we're forced to manage this manually.
144
+ We need to factory all the parent objects and pass them down to their
145
+ child factories. So even though we are mostly caring about how a School
146
+ model interacts with a Person model, we are forced to factory five different
147
+ parent models.
148
+
149
+
150
+
151
+ ## How DataWorks Solves This Problem
152
+
153
+ ### Approach
154
+
155
+ DataWorks solves this problem by following one simple rule: **by default, reuse
156
+ objects that already exist.**
157
+
158
+ When using FactoryGirl, a factory only concerns itself with its particular
159
+ neighborhood of the data model. Since FactoryGirl factories do not know
160
+ the big picture, their default is not to reuse existing objects but create
161
+ new ones. This results in the proliferation of unwanted objects.
162
+
163
+ DataWorks knows the big picture of your data model and assumes that you are
164
+ wanting to build a consistently connected network of models. So it will
165
+ reuse objects that already exist unless explicitly told otherwise.
166
+
167
+ ### Implementation
168
+
169
+ DataWorks exists as a layer on top of FactoryGirl. DataWorks assumes that
170
+ you have FactoryGirl factories for each model and that they create valid
171
+ objects. DataWorks always uses a create strategy with FactoryGirl.
172
+
173
+ ### Benefits
174
+
175
+ Here's how DataWorks gives us the following benefits:
176
+
177
+ * **Reduces test data bloat.** The amount of test data code that you have
178
+ to write is significantly reduced.
179
+ * **Improves test clarity.** DataWorks allows you to factory only the
180
+ objects that are relevant to what's being tested. Having to factory
181
+ up a bunch of indirectly-related objects distracts from what the test
182
+ is trying to communicate.
183
+ * **Improves test correctness.** Manually preventing the duplication of
184
+ parent factories is more error-prone than having DataWorks do it
185
+ automatically.
186
+ * **Reduces refactoring cost when changing the data model.** If you refactor
187
+ the data model high up in the parent chain, you aren't forced to touch
188
+ practically every test that has factoried test data, since DataWorks allows
189
+ for implied, not explicit, parents.
190
+ * **Improves test performance.** Because intermediate models aren't being
191
+ created, there is a small performance benefit.
192
+
193
+
194
+
195
+ ## Usage
196
+
197
+ ### Basic Example
198
+
199
+ ```ruby
200
+ describe "School#number_of_people" do
201
+ before do
202
+ data = TheDataWorks.new
203
+ @school = data.add_school
204
+ data.add_person(school: @school)
205
+ data.add_person(school: @school)
206
+ end
207
+
208
+ it "returns the correct number of people attending that school" do
209
+ expect( @school.number_of_people ).to eq( 2 )
210
+ end
211
+ end
212
+ ```
213
+
214
+ * Always start by creating a new DataWorks object. This starts DataWorks off
215
+ with a blank slate of objects.
216
+ * Use add_[model_name] to create a new object.
217
+ * The necessary parent objects (SchoolDistrict, Town, County, etc.) are implied.
218
+
219
+ ### Referencing Objects
220
+
221
+ * Any object that's created through DataWorks can be referenced by calling
222
+ [model_name][number] to DataWorks.
223
+ * Objects are numbered in the order they are created.
224
+ * the_[model_name] is a synonym for [model_name]1 that can aid test readability.
225
+
226
+ Here's a test that puts two people in one school and three in another:
227
+
228
+ ```ruby
229
+ describe "School#number_of_people" do
230
+ before do
231
+ data = TheDataWorks.new
232
+ data.add_school
233
+ data.add_school
234
+ data.add_person(school: data.school1)
235
+ data.add_person(school: data.school1)
236
+ data.add_person(school: data.school2)
237
+ data.add_person(school: data.school2)
238
+ data.add_person(school: data.school2)
239
+ @school = data.school1
240
+ end
241
+
242
+ it "returns the correct number of people attending that school" do
243
+ expect( @school.number_of_people ).to eq( 2 )
244
+ end
245
+ end
246
+ ```
247
+
248
+ If we wanted to check the number of people in the school district, we can
249
+ access the SchoolDistrict created behind the scenes like this:
250
+
251
+ ```ruby
252
+ data.school_district1.number_of_people
253
+ ```
254
+
255
+ Or we could use the synonym:
256
+
257
+ ```ruby
258
+ data.the_school_district.number_of_people
259
+ ```
260
+
261
+ ### Adding Multiple Objects
262
+
263
+ You can factory many objects at once by passing an integer count as the first argument to `add_`
264
+
265
+ DataWorks treats any call to `add_` with an integer as the first argument as an attempt to create
266
+ multiple objects. For clarity and readability DataWorks allows you to use the plural name of the
267
+ models being created. This is not required
268
+
269
+ Given
270
+
271
+ ```ruby
272
+ data = TheDataWorks.new
273
+ data.add_schools(10)
274
+ ```
275
+
276
+ and
277
+
278
+ data.add_school(10)
279
+
280
+ Will both create 10 school records.
281
+
282
+ Plural model names are converted to singular using the `ActiveSupport` inflections module. If you
283
+ need to specify any additional inflection rules you can add them in your test configuration. For
284
+ reference here is the example given in the Rails documentation.
285
+
286
+ ```ruby
287
+ ActiveSupport::Inflector.inflections(:en) do |inflect|
288
+ inflect.plural /^(ox)$/i, '\1\2en'
289
+ inflect.singular /^(ox)en/i, '\1'
290
+
291
+ inflect.irregular 'octopus', 'octopi'
292
+
293
+ inflect.uncountable 'equipment'
294
+ end
295
+ ```
296
+
297
+ ### Avoiding Object Reuse
298
+
299
+ Let's say that we want to factory two Schools and want to avoid having them
300
+ automatically share a SchoolDistrict. We can do this by simply manually
301
+ creating separate SchoolDistrict objects and explicitly setting them when
302
+ we create the Schools:
303
+
304
+ ```ruby
305
+ data = TheDataWorks.new
306
+ data.add_school_districts(2)
307
+ data.add_school(school_district: data.school_district1)
308
+ data.add_school(school_district: data.school_district2)
309
+ ```
310
+
311
+ The case above works find for records which only need to be different in one
312
+ association but what if we want to generate a chain of associations or need to
313
+ create many record which all need to share certain associations?
314
+
315
+ Given the data model described above say that we want to be able to create two
316
+ schools but have them each belong to a different state with the two states
317
+ belonging to the same region. DataWorks can restrict the possible 'parentage' of
318
+ models using the `#set_restriction` method.
319
+
320
+ ```ruby
321
+ massachusetts = data.add_state(name: 'Massachusetts')
322
+ vermont = data.add_state(name: 'Vermont')
323
+
324
+ # We now have two states so lets create those schools...
325
+
326
+ mass_school = data.add_school # this will cause a SchoolDistrict and County to be created
327
+
328
+ data.set_restriction(for_model: :state, to: vermont)
329
+
330
+ vermont_school = data.add_school # this will also cause a SchoolDistrict and County to be created
331
+ ```
332
+
333
+ The return value of `#set_restriction` is the parent object passed as `to:`.
334
+
335
+ Had we not used the `set_restriction` method above we would have needed to set
336
+ the vermont model hierarchy up manually like so.
337
+
338
+ ```ruby
339
+ vermont_county = data.add_county
340
+ vermont_district = data.add_school_district(county: vermont_county)
341
+ vermont_school = data.add_school(school_district: vermont_district)
342
+ ```
343
+
344
+ The `#set_restriction` method also accepts a block. When the block form is used
345
+ the restriction applies only to those records added within the block.
346
+
347
+ The return value of the block form of `#set_restriction` is the last statement
348
+ evaluated in the block.
349
+
350
+ DataWorks also provides a `#set_current_default` method which allows you to set
351
+ the current default record to use when automatically associating models.
352
+
353
+ Say we want to create multiple towns which belong to two states. We can do so
354
+ like this
355
+
356
+ ```ruby
357
+ massachusetts = data.add_state(name: 'Massachusetts')
358
+ vermont = data.add_state(name: 'Vermont')
359
+
360
+ data.add_towns(3) # this will create 3 towns all belonging to massachusetts
361
+
362
+ # massachusetts was the default above since it was created first. Now we
363
+ # want vermont to be the default state for 3 more towns
364
+
365
+ data.set_current_default(to: vermont, for_model: :state)
366
+
367
+ data.add_towns(3) # this will create 3 towns all belonging to vermont
368
+ ```
369
+
370
+ Both `set_current_default` and `set_restriction` have corresponding `clear_X_for`
371
+ methods for removing the default/restricting record.
372
+
373
+ ### Overriding Attributes
374
+
375
+ DataWorks simply delegates to the factory corresponding to the model, so
376
+ it will use the default attributes specified there. If you pass attributes
377
+ to DataWorks, it will pass them on to the factory:
378
+
379
+ ```ruby
380
+ data = TheDataWorks.new
381
+ data.add_school_district(name: 'Washington', rural: true)
382
+ ```
383
+
384
+ ### Better Variable Names
385
+
386
+ Let's say you don't like calling your test data by number and want more
387
+ meaningful names. Just use plain old Ruby variables to make your tests
388
+ clearer:
389
+
390
+ ```ruby
391
+ data = TheDataWorks.new
392
+ franklin = data.add_school_district(name: 'Franklin')
393
+ greenville = data.add_school_district(name: 'Greenville')
394
+ data.add_school(school_district: franklin)
395
+ data.add_school(school_district: greenville)
396
+ ```
397
+
398
+ ### Visualizing the Object Graph
399
+
400
+ If you want to debug your test data and explicitly see the object graph that
401
+ DataWorks created for you, you can use the visualize method which will render
402
+ an object graph for you and automatically open it:
403
+
404
+ ```ruby
405
+ data = TheDataWorks.new
406
+ data.add_school_district
407
+ data.add_school
408
+ data.visualize
409
+ ```
410
+
411
+ If an object labeled "unmanaged" appears in the object graph it means that
412
+ an object was factoried outside of DataWorks. This can happen if your
413
+ base factories create children (not parent) associations by default. Note
414
+ that the object graph cannot show the entire set of all objects that
415
+ were created, just those created through DataWorks and some of the associated
416
+ objects that may have been created outside of DataWorks and attached to
417
+ a DataWorks-managed factory object.
418
+
419
+ ### Setup and Configuration
420
+
421
+ DataWorks expects the following:
422
+
423
+ * You must have factories for all of your models, and these factories must
424
+ produce valid (persistable) models.
425
+ * None of these basic factories should create any child objects by default,
426
+ as this does not give DataWorks a chance to manage them.
427
+ * You must configure DataWorks in your spec_helper.rb by calling
428
+ DataWorks.configure. This is where you teach DataWorks about your
429
+ data model relationships.
430
+ * You must have a class that subclasses DataWorks::Base. Use this in your
431
+ tests; do not use DataWorks::Base directly.
432
+ * You must tell DataWorks if you have any associations with special names (the `belongs_to` or `has_many` do not match the model names). You can do this by passing in a hash instead of a symbol for the parent model name: `{ :association_name => :model_name }`
433
+
434
+ #### Configuration
435
+
436
+ ##### Necessary Parents
437
+
438
+ In your spec_helper.rb file, put the following:
439
+
440
+ ```ruby
441
+ DataWorks.configure do |config|
442
+ config.necessary_parents = {
443
+ classroom: [:school, :grade],
444
+ district: [ ],
445
+ event: [:schedule, :school],
446
+ failure: [:service_schedule_set],
447
+ grade: [ ],
448
+ iep_service: [:service, :student],
449
+ schedule: [:service_schedule_set],
450
+ scheduled_service: [{:schedulable => :event}, :student, :iep_service],
451
+ school: [:district],
452
+ service: [:district, :service_type],
453
+ service_schedule_set: [:district, :service],
454
+ service_type: [ ],
455
+ student: [:school, :classroom],
456
+ }
457
+ end
458
+ ```
459
+
460
+ `config.necessary_parents` is where you tell DataWorks which other factories
461
+ must be created when you create a particular factory. Because FactoryGirl
462
+ causes a proliferation of extra objects, DataWorks does not allow FactoryGirl
463
+ to create necessary associated objects. Instead, DataWorks will create the
464
+ necessary parent objects and pass them down into the factory, ensuring that
465
+ too many parent objects do not get created.
466
+
467
+ This must list all of the models.
468
+
469
+ You must keep this list up to date when you change your data model. To aid
470
+ with this, whenever a belongs_to relationship changes in your code,
471
+ DataWorks raises an error that reminds you that you need to update this
472
+ configuration to match the new data model.
473
+
474
+ ##### Polymorphic Parents
475
+
476
+ The `:scheduled_services` portion of the configuration above demonstrates DataWorks' support for polymorphic associations. Rather than a symbol for the necessary parent a polymorphic class must have a hash, of a single key/value pair, which names the polymorphic relationship and indicates which possible parent should be created by default. Note that other possible parent objects can be passed as arguments when the child object is being created as with any other classes.
477
+
478
+ ##### Autocreated Children
479
+
480
+ DataWorks can handle models that autocreate a child object:
481
+
482
+ ```ruby
483
+ DataWorks.configure do |config|
484
+ config.necessary_parents = {
485
+ ...
486
+ }
487
+
488
+ config.autocreated_children = {
489
+ city: [:city_location]
490
+ }
491
+ end
492
+ ```
493
+
494
+ For example, let's say that every time a `City` model is created, it must have a `CityLocation` model, so there is logic in the `City` model that autocreates a `CityLocation` object. Note that when a model does this, DataWorks has no way of knowing about this autocreated child object and so it is not managed by DataWorks and could end up being a zombie object that could break your tests. So by explicitly listing out the names of the models that get autocreated, this gives DataWorks a chance to remove the zombie object and hook up a DataWorks-aware object in its place.
495
+
496
+ Note that this is only for the situation where a model autocreates a single child model, not multiple.
497
+
498
+ ##### Blessing
499
+
500
+ Once you're satisfied the configuration is accurate, run
501
+
502
+ ```sh
503
+ $ rake data_works:bless
504
+ ```
505
+
506
+ And DataWorks will no longer complain (until you the next time you change a
507
+ belongs_to relationship).
508
+
509
+ #### Subclassing DataWorks::Base
510
+
511
+ Subclass DataWorks::Base and call it whatever you want, such as TheDataWorks.
512
+ spec/support is a good place to put this.
513
+
514
+ Inside of this class you can put some convenience functions. For example,
515
+ if you find yourself frequently factorying two types of objects together,
516
+ you can add a convenience function here.
517
+
518
+
519
+
520
+ ## Issues
521
+
522
+ * DataWorks does not yet work with namespaced models.
523
+ * DataWorks does not support factory traits
524
+ * DataWorks does not allow associations where the foreign key name is not the same as the class name
525
+ * The visualization component uses respond_to in verifying associations, which is not the most robust method if has_many is missing or with :through associations.
526
+
527
+ # Credit
528
+
529
+ DataWorks was developed by [Wyatt Greene](https://github.com/techiferous) and [Luke Inglis](https://github.com/ludamillion) at [The District Management Group](https://github.com/dmcouncil).