manufactory 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE CHANGED
@@ -1,3 +1,6 @@
1
+ Manufactory was originaly imported from Machinist by Peter Yandell
2
+ which is under MIT license as well, so the copyrights are:
3
+ Copyright (c) 2008 Peter Yandell
1
4
  Copyright (c) 2009 Jakub Stastny
2
5
 
3
6
  Permission is hereby granted, free of charge, to any person obtaining
@@ -1,12 +1,12 @@
1
1
  h1. About
2
2
 
3
- Manufactory is a minimalistic and clean rewrite of machinist with some improvements.
3
+ Manufactory is a minimalistic and clean rewrite of manufactory with some improvements.
4
4
 
5
5
  h1. What is different?
6
6
 
7
7
  h2. Real DataMapper Support
8
8
 
9
- Machinist doesn't work with DataMapper right now. It's not a machinist problem, it's DataMapper issue, but who cares, it just doesn't work.
9
+ Manufactory doesn't work with DataMapper right now. It's not a manufactory problem, it's DataMapper issue, but who cares, it just doesn't work.
10
10
 
11
11
  h2. Initialization
12
12
 
@@ -29,3 +29,288 @@ PaymentType.blueprint(:paypel, PaymentType.blueprint)
29
29
  name "PayPal"
30
30
  shortcut nil
31
31
  end
32
+
33
+
34
+
35
+
36
+
37
+
38
+
39
+ Manufactory makes it easy to create test data within your tests. It generates data for the fields you don't care about, and constructs any necessary associated objects, leaving you to only specify the fields you *do* care about in your tests. For example:
40
+
41
+ describe Comment do
42
+ before do
43
+ # This will make a Comment, a Post, and a User (the author of
44
+ # the Post), and generate values for all their attributes:
45
+ @comment = Comment.make(:spam => true)
46
+ end
47
+
48
+ it "should not include comments marked as spam in the without_spam named scope" do
49
+ Comment.without_spam.should_not include(@comment)
50
+ end
51
+ end
52
+
53
+ You tell Manufactory how to do this with blueprints:
54
+
55
+ require 'manufactory/active_record'
56
+ require 'sham'
57
+ require 'faker'
58
+
59
+ Sham.name { Faker::Name.name }
60
+ Sham.email { Faker::Internet.email }
61
+ Sham.title { Faker::Lorem.sentence }
62
+ Sham.body { Faker::Lorem.paragraph }
63
+
64
+ User.blueprint do
65
+ name
66
+ email
67
+ end
68
+
69
+ Post.blueprint do
70
+ title
71
+ author
72
+ body
73
+ end
74
+
75
+ Comment.blueprint do
76
+ post
77
+ author_name { Sham.name }
78
+ author_email { Sham.email }
79
+ body
80
+ end
81
+
82
+
83
+ Download & Install
84
+ ==================
85
+
86
+ ### Installing as a Rails plugin
87
+
88
+ ./script/plugin install git://github.com/botanicus/manufactory.git
89
+
90
+ ### Installing as a Gem
91
+
92
+ sudo gem install manufactory
93
+
94
+ ### Setting up your project
95
+
96
+ Create a `blueprints.rb` file to hold your blueprints in your test (or spec) directory. It should start with:
97
+
98
+ require 'manufactory/active_record'
99
+ require 'sham'
100
+
101
+ Substitute `data_mapper` or `sequel` for `active_record` if that's your weapon of choice.
102
+
103
+ Require `blueprints.rb` in your `test_helper.rb` (or `spec_helper.rb`):
104
+
105
+ require File.expand_path(File.dirname(__FILE__) + "/blueprints")
106
+
107
+ Set Sham to reset before each test. In the `class Test::Unit::TestCase` block in your `test_helper.rb`, add:
108
+
109
+ setup { Sham.reset }
110
+
111
+ or, if you're on RSpec, in the `Spec::Runner.configure` block in your `spec_helper.rb`, add:
112
+
113
+ config.before(:all) { Sham.reset(:before_all) }
114
+ config.before(:each) { Sham.reset(:before_each) }
115
+
116
+
117
+ Documentation
118
+ =============
119
+
120
+ Sham - Generating Attribute Values
121
+ ----------------------------------
122
+
123
+ Sham lets you generate random but repeatable unique attributes values.
124
+
125
+ For example, you could define a way to generate random names as:
126
+
127
+ Sham.name { (1..10).map { ('a'..'z').to_a.rand }.join }
128
+
129
+ Then, to generate a name, call:
130
+
131
+ Sham.name
132
+
133
+ So why not just define a helper method to do this? Sham ensures two things for you:
134
+
135
+ 1. You get the same sequence of values each time your test is run
136
+ 2. You don't get any duplicate values
137
+
138
+ Sham works very well with the excellent [Faker gem](http://faker.rubyforge.org/) by Benjamin Curtis. Using this, a much nicer way to generate names is:
139
+
140
+ Sham.name { Faker::Name.name }
141
+
142
+ Sham also supports generating numbered sequences if you prefer.
143
+
144
+ Sham.name {|index| "Name #{index}" }
145
+
146
+ If you want to allow duplicate values for a sham, you can pass the `:unique` option:
147
+
148
+ Sham.coin_toss(:unique => false) { rand(2) == 0 ? 'heads' : 'tails' }
149
+
150
+ You can create a bunch of sham definitions in one hit like this:
151
+
152
+ Sham.define do
153
+ title { Faker::Lorem.words(5).join(' ') }
154
+ name { Faker::Name.name }
155
+ body { Faker::Lorem.paragraphs(3).join("\n\n") }
156
+ end
157
+
158
+
159
+ Blueprints - Generating Objects
160
+ -------------------------------
161
+
162
+ A blueprint describes how to generate an object. The idea is that you let the blueprint take care of making up values for attributes that you don't care about in your test, leaving you to focus on the just the things that you're testing.
163
+
164
+ A simple blueprint might look like this:
165
+
166
+ Post.blueprint do
167
+ title { Sham.title }
168
+ author { Sham.name }
169
+ body { Sham.body }
170
+ end
171
+
172
+ You can then construct a Post from this blueprint with:
173
+
174
+ Post.make
175
+
176
+ When you call `make`, Manufactory calls `Post.new`, then runs through the attributes in your blueprint, calling the block for each attribute to generate a value. The Post is then saved and reloaded. An exception is thrown if Post can't be saved.
177
+
178
+ You can override values defined in the blueprint by passing a hash to make:
179
+
180
+ Post.make(:title => "A Specific Title")
181
+
182
+ If you don't supply a block for an attribute in the blueprint, Manufactory will look for a Sham definition with the same name as the attribute, so you can shorten the above blueprint to:
183
+
184
+ Post.blueprint do
185
+ title
186
+ author { Sham.name }
187
+ body
188
+ end
189
+
190
+ If you want to generate an object without saving it to the database, replace `make` with `make_unsaved`. (`make_unsaved` also ensures that any associated objects that need to be generated are not saved - although not if you are using Sequel. See the section on associations below.)
191
+
192
+ You can refer to already assigned attributes when constructing a new attribute:
193
+
194
+ Post.blueprint do
195
+ title
196
+ author { Sham.name }
197
+ body { "Post by #{author}" }
198
+ end
199
+
200
+
201
+ ### Named Blueprints
202
+
203
+ Named blueprints let you define variations on an object. For example, suppose some of your Users are administrators:
204
+
205
+ User.blueprint do
206
+ name
207
+ email
208
+ end
209
+
210
+ User.blueprint(:admin) do
211
+ name { Sham.name + " (admin)" }
212
+ admin { true }
213
+ end
214
+
215
+ Calling:
216
+
217
+ User.make(:admin)
218
+
219
+ will use the `:admin` blueprint.
220
+
221
+ Named blueprints call the default blueprint to set any attributes not specifically provided, so in this example the `email` attribute will still be generated even for an admin user.
222
+
223
+
224
+ ### Belongs\_to Associations
225
+
226
+ If you're generating an object that belongs to another object, you can generate the associated object like this:
227
+
228
+ Comment.blueprint do
229
+ post { Post.make }
230
+ end
231
+
232
+ Calling `Comment.make` will construct a Comment and its associated Post, and save both.
233
+
234
+ If you want to override the value for post when constructing the comment, you can do this:
235
+
236
+ post = Post.make(:title => "A particular title)
237
+ comment = Comment.make(:post => post)
238
+
239
+ Manufactory will not call the blueprint block for the post attribute, so this won't generate two posts.
240
+
241
+ Manufactory is smart enough to look at the association and work out what sort of object it needs to create, so you can shorten the above blueprint to:
242
+
243
+ Comment.blueprint do
244
+ post
245
+ end
246
+
247
+
248
+ ### Other Associations
249
+
250
+ For has\_many and has\_and\_belongs\_to\_many associations, ActiveRecord insists that the object be saved before any associated objects can be saved. That means you can't generate the associated objects from within the blueprint.
251
+
252
+ The simplest solution is to write a test helper:
253
+
254
+ def make_post_with_comments(attributes = {})
255
+ post = Post.make(attributes)
256
+ 3.times { post.comments.make }
257
+ post
258
+ end
259
+
260
+ Note here that you can call `make` on a has\_many association. (This isn't yet supported for DataMapper.)
261
+
262
+ Make can take a block, into which it passes the constructed object, so the above can be written as:
263
+
264
+ def make_post_with_comments
265
+ Post.make(attributes) do |post|
266
+ 3.times { post.comments.make }
267
+ end
268
+ end
269
+
270
+
271
+ ### Using Blueprints in Rails Controller Tests
272
+
273
+ The `plan` method behaves like `make`, except it returns a hash of attributes, and doesn't save the object. This is useful for passing in to controller tests:
274
+
275
+ test "should create post" do
276
+ assert_difference('Post.count') do
277
+ post :create, :post => Post.plan
278
+ end
279
+ assert_redirected_to post_path(assigns(:post))
280
+ end
281
+
282
+ `plan` will save any associated objects. In this example, it will create an Author, and it knows that the controller expects an `author_id` attribute, rather than an `author` attribute, and makes this translation for you.
283
+
284
+ You can also call plan on has\_many associations, making it easy to test nested controllers:
285
+
286
+ test "should create comment" do
287
+ post = Post.make
288
+ assert_difference('Comment.count') do
289
+ post :create, :post_id => post.id, :comment => post.comments.plan
290
+ end
291
+ assert_redirected_to post_comment_path(post, assigns(:comment))
292
+ end
293
+
294
+ (Calling plan on associations is not yet supported in DataMapper.)
295
+
296
+
297
+ ### Blueprints on Plain Old Ruby Objects
298
+
299
+ Manufactory also works with plain old Ruby objects. Let's say you have a class like:
300
+
301
+ class Post
302
+ attr_accessor :title
303
+ attr_accessor :body
304
+ end
305
+
306
+ You can then do the following in your `blueprints.rb`:
307
+
308
+ require 'manufactory/object'
309
+
310
+ Post.blueprint do
311
+ title "A title!"
312
+ body "A body!"
313
+ end
314
+
315
+
316
+ Thanks to Thoughtbot's [Factory Girl](http://github.com/thoughtbot/factory_girl/tree/master). Manufactory was written because I loved the idea behind Factory Girl, but I thought the philosophy wasn't quite right, and I hated the syntax.
@@ -1,8 +1,12 @@
1
1
  # encoding: utf-8
2
2
 
3
+ $:.unshift(File.dirname(__FILE__)) unless $:.include?(File.dirname(__FILE__))
4
+
5
+ require "manufactory/dsl"
3
6
  require "manufactory/sham"
4
7
 
5
8
  module Manufactory
9
+ VERSION ||= "0.0.2"
6
10
  # ActiveRecord::Base.extend(Manufactory::Blueprints)
7
11
  # Post.blueprint(&block)
8
12
  module Blueprints
@@ -18,12 +22,24 @@ module Manufactory
18
22
  end
19
23
  end
20
24
 
25
+ module ManufactoryMixin
26
+ include Manufactory::Blueprints
27
+ def make(adapter, name = :default, attributes = Hash.new, &block)
28
+ name, attributes = :default, name if name.is_a?(Hash) && attributes.empty?
29
+ callables = self.blueprints[name]
30
+ adapter = adapter.new(self, name, callables)
31
+ instance = adapter.run(attributes)
32
+ instance.instance_eval(&block) if block_given?
33
+ return instance
34
+ end
35
+ end
36
+
21
37
  # Adapter.new(User, :admin, age: 24)
22
38
  class Adapter
23
39
  attr_reader :klass, :name, :callables
24
40
  def initialize(klass, name, callables)
25
- @klass = klass
26
- @name = name
41
+ @klass = klass
42
+ @name = name
27
43
  @callables = callables
28
44
  end
29
45
 
@@ -31,102 +47,25 @@ module Manufactory
31
47
  blueprint = klass.blueprints[name.to_sym]
32
48
  raise "No blueprint #{name} for class #{klass}" if blueprint.nil?
33
49
  default_attributes = Hash.new
50
+ object = klass.new(default_attributes) # will be discarded at the end
34
51
  self.callables.each do |callable|
35
- default_attributes.merge!(DSL.new(self, callable).run)
52
+ default_attributes.merge!(DSL.new(self, object, callable).run)
36
53
  end
37
54
  self.initialize_object(klass, default_attributes, attributes)
38
55
  end
39
56
 
40
- protected
41
- def initialize_object(klass, default_attributes, attributes)
42
- klass.new(default_attributes.merge(attributes))
43
- end
44
- end
45
-
46
- # DSL is used to execute the blueprint and construct an object.
47
- # The blueprint is instance_eval'd against the Lathe.
48
- class DSL
49
- attr_reader :adapter, :blueprint
50
- def initialize(adapter, blueprint)
51
- @adapter = adapter
52
- @blueprint = blueprint
53
- end
54
-
55
- def run
56
- self.instance_eval(&self.blueprint)
57
- assigned_attributes
58
- end
59
-
60
- def object
61
- yield @object if block_given?
62
- @object
63
- end
64
-
65
- def method_missing(symbol, *args, &block)
66
- if attribute_assigned?(symbol)
67
- # If we've already assigned the attribute, return that.
68
- @object.send(symbol)
69
- elsif @adapter.has_association?(@object, symbol) && !nil_or_empty?(@object.send(symbol))
70
- # If the attribute is an association and is already assigned, return that.
71
- @object.send(symbol)
72
- else
73
- # Otherwise generate a value and assign it.
74
- assigned_attributes[symbol] = generate_attribute_value(symbol, *args, &block)
75
- end
76
- end
77
-
78
- def assigned_attributes
79
- @assigned_attributes ||= {}
80
- end
81
-
82
- # Undef a couple of methods that are common ActiveRecord attributes.
83
- # (Both of these are deprecated in Ruby 1.8 anyway.)
84
- undef_method :id if respond_to?(:id)
85
- undef_method :type if respond_to?(:type)
86
-
87
- private
88
-
89
- def nil_or_empty?(object)
90
- object.respond_to?(:empty?) ? object.empty? : object.nil?
91
- end
92
-
93
- def attribute_assigned?(key)
94
- assigned_attributes.has_key?(key.to_sym)
57
+ def has_association?(object, attribute)
58
+ false
95
59
  end
96
60
 
97
- def generate_attribute_value(attribute, *args)
98
- if block_given?
99
- # If we've got a block, use that to generate the value.
100
- yield
101
- else
102
- # Otherwise, look for an association or a sham.
103
- if @adapter.has_association?(object, attribute)
104
- @adapter.class_for_association(object, attribute).make(args.first || {})
105
- elsif args.empty?
106
- Sham.send(attribute)
107
- else
108
- # If we've got a constant, just use that.
109
- args.first
110
- end
61
+ protected
62
+ def initialize_object(klass, default_attributes, attributes)
63
+ attributes = default_attributes.merge(attributes)
64
+ instance = klass.new
65
+ attributes.each do |attribute, value|
66
+ instance.send("#{attribute}=", value)
111
67
  end
68
+ return instance
112
69
  end
113
-
114
- end
115
-
116
- # This sets a flag that stops make from saving objects, so
117
- # that calls to make from within a blueprint don't create
118
- # anything inside make_unsaved.
119
- def self.with_save_nerfed
120
- begin
121
- @@nerfed = true
122
- yield
123
- ensure
124
- @@nerfed = false
125
- end
126
- end
127
-
128
- @@nerfed = false
129
- def self.nerfed?
130
- @@nerfed
131
70
  end
132
71
  end