machinist_redux 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. checksums.yaml +7 -0
  2. data/.codeclimate.yml +17 -0
  3. data/.gitignore +24 -0
  4. data/.rspec +2 -0
  5. data/.rubocop.yml +60 -0
  6. data/.rubocop_todo.yml +92 -0
  7. data/.travis.yml +23 -0
  8. data/Appraisals +11 -0
  9. data/Gemfile +32 -0
  10. data/LICENSE.txt +21 -0
  11. data/README.md +307 -0
  12. data/Rakefile +20 -0
  13. data/gemfiles/rails_4.2.gemfile +38 -0
  14. data/gemfiles/rails_4.2.gemfile.lock +232 -0
  15. data/gemfiles/rails_5.0.gemfile +38 -0
  16. data/gemfiles/rails_5.0.gemfile.lock +232 -0
  17. data/gemfiles/rails_5.1.gemfile +34 -0
  18. data/gemfiles/rails_5.1.gemfile.lock +231 -0
  19. data/lib/generators/machinist/install/USAGE +2 -0
  20. data/lib/generators/machinist/install/install_generator.rb +46 -0
  21. data/lib/generators/machinist/install/templates/blueprints.rb +9 -0
  22. data/lib/generators/machinist/install/templates/machinist.rb.erb +7 -0
  23. data/lib/generators/machinist/model/model_generator.rb +11 -0
  24. data/lib/machinist.rb +5 -0
  25. data/lib/machinist/active_record.rb +14 -0
  26. data/lib/machinist/active_record/blueprint.rb +14 -0
  27. data/lib/machinist/active_record/lathe.rb +21 -0
  28. data/lib/machinist/blueprint.rb +84 -0
  29. data/lib/machinist/exceptions.rb +30 -0
  30. data/lib/machinist/lathe.rb +65 -0
  31. data/lib/machinist/machinable.rb +100 -0
  32. data/lib/machinist/version.rb +3 -0
  33. data/machinist.gemspec +23 -0
  34. data/spec/machinist/active_record_spec.rb +106 -0
  35. data/spec/machinist/blueprint_inheritance_spec.rb +101 -0
  36. data/spec/machinist/blueprint_spec.rb +76 -0
  37. data/spec/machinist/exceptions_spec.rb +16 -0
  38. data/spec/machinist/machinable_spec.rb +91 -0
  39. data/spec/spec_helper.rb +110 -0
  40. data/spec/support/active_record_environment.rb +62 -0
  41. metadata +94 -0
@@ -0,0 +1,30 @@
1
+ module Machinist
2
+ # Raised when make! is called on a class whose blueprints don't support
3
+ # saving.
4
+ class BlueprintCantSaveError < RuntimeError
5
+ attr_reader :blueprint
6
+
7
+ def initialize(blueprint)
8
+ @blueprint = blueprint
9
+ end
10
+
11
+ def message
12
+ "make! is not supported by blueprints for class #{@blueprint.klass.name}"
13
+ end
14
+ end
15
+
16
+ # Raised when calling make on a class with no corresponding blueprint
17
+ # defined.
18
+ class NoBlueprintError < RuntimeError
19
+ attr_reader :klass, :name
20
+
21
+ def initialize(klass, name)
22
+ @klass = klass
23
+ @name = name
24
+ end
25
+
26
+ def message
27
+ "No #{@name} blueprint defined for class #{@klass.name}"
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,65 @@
1
+ module Machinist
2
+ # When you make an object, the blueprint for that object is instance-evaled
3
+ # against a Lathe.
4
+ #
5
+ # The Lathe implements all the methods that are available to the blueprint,
6
+ # including method_missing to let the blueprint define attributes.
7
+ class Lathe
8
+ def initialize(klass, serial_number, attributes = {})
9
+ @klass = klass
10
+ @serial_number = serial_number
11
+ @assigned_attributes = {}
12
+
13
+ @object = @klass.new
14
+ attributes.each { |key, value| assign_attribute(key, value) }
15
+ end
16
+
17
+ # Returns a unique serial number for the object under construction.
18
+ def sn
19
+ @serial_number
20
+ end
21
+
22
+ # Returns the object under construction.
23
+ attr_reader :object
24
+
25
+ def method_missing(attribute, *args, &block) #:nodoc:
26
+ unless attribute_assigned?(attribute)
27
+ assign_attribute(attribute, make_attribute(attribute, args, &block))
28
+ end
29
+ end
30
+
31
+ # Undef a couple of methods that are common ActiveRecord attributes.
32
+ # (Both of these are deprecated in Ruby 1.8 anyway.)
33
+ undef_method :id if respond_to?(:id)
34
+ undef_method :type if respond_to?(:type)
35
+
36
+ protected
37
+
38
+ def make_attribute(attribute, args, &block) #:nodoc:
39
+ count = args.shift if args.first.is_a?(0.class)
40
+ if count
41
+ Array.new(count) { make_one_value(attribute, args, &block) }
42
+ else
43
+ make_one_value(attribute, args, &block)
44
+ end
45
+ end
46
+
47
+ def make_one_value(attribute, args) #:nodoc:
48
+ raise_argument_error(attribute) unless args.empty?
49
+ yield
50
+ end
51
+
52
+ def assign_attribute(key, value) #:nodoc:
53
+ @assigned_attributes[key.to_sym] = value
54
+ @object.send("#{key}=", value)
55
+ end
56
+
57
+ def attribute_assigned?(key) #:nodoc:
58
+ @assigned_attributes.key?(key.to_sym)
59
+ end
60
+
61
+ def raise_argument_error(attribute) #:nodoc:
62
+ raise ArgumentError, "Invalid arguments to attribute #{attribute} in blueprint"
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,100 @@
1
+ module Machinist
2
+ # Extend classes with this module to define the blueprint and make methods.
3
+ module Machinable
4
+ # Define a blueprint with the given name for this class.
5
+ #
6
+ # e.g.
7
+ # Post.blueprint do
8
+ # title { "A Post" }
9
+ # body { "Lorem ipsum..." }
10
+ # end
11
+ #
12
+ # If you provide the +name+ argument, a named blueprint will be created.
13
+ # See the +blueprint_name+ argument to the make method.
14
+ def blueprint(name = :master, &block)
15
+ @blueprints ||= {}
16
+ if block_given?
17
+ parent = (name == :master ? superclass : self) # Where should we look for the parent blueprint?
18
+ @blueprints[name] = blueprint_class.new(self, parent: parent, &block)
19
+ end
20
+ @blueprints[name]
21
+ end
22
+
23
+ # Construct an object from a blueprint.
24
+ #
25
+ # :call-seq:
26
+ # make([count], [blueprint_name], [attributes = {}])
27
+ #
28
+ # [+count+]
29
+ # The number of objects to construct. If +count+ is provided, make
30
+ # returns an array of objects rather than a single object.
31
+ # [+blueprint_name+]
32
+ # Construct the object from the named blueprint, rather than the master
33
+ # blueprint.
34
+ # [+attributes+]
35
+ # Override the attributes from the blueprint with values from this hash.
36
+ def make(*args)
37
+ decode_args_to_make(*args) do |blueprint, attributes|
38
+ blueprint.make(attributes)
39
+ end
40
+ end
41
+
42
+ # Construct and save an object from a blueprint, if the class allows saving.
43
+ #
44
+ # :call-seq:
45
+ # make!([count], [blueprint_name], [attributes = {}])
46
+ #
47
+ # Arguments are the same as for make.
48
+ def make!(*args)
49
+ decode_args_to_make(*args) do |blueprint, attributes|
50
+ raise BlueprintCantSaveError, blueprint unless blueprint.respond_to?(:make!)
51
+ blueprint.make!(attributes)
52
+ end
53
+ end
54
+
55
+ # Remove all blueprints defined on this class.
56
+ def clear_blueprints!
57
+ @blueprints = {}
58
+ end
59
+
60
+ # Classes that include Machinable can override this method if they want to
61
+ # use a custom blueprint class when constructing blueprints.
62
+ #
63
+ # The default is Machinist::Blueprint.
64
+ def blueprint_class
65
+ Machinist::Blueprint
66
+ end
67
+
68
+ private
69
+
70
+ # Parses the arguments to make.
71
+ #
72
+ # Yields a blueprint and an attributes hash to the block, which should
73
+ # construct an object from them. The block may be called multiple times to
74
+ # construct multiple objects.
75
+ def decode_args_to_make(*args) #:nodoc:
76
+ count, name, attributes = *decode_args(args)
77
+ blueprint = ensure_blueprint(name)
78
+
79
+ if count.nil?
80
+ yield(blueprint, attributes)
81
+ else
82
+ Array.new(count) { yield(blueprint, attributes) }
83
+ end
84
+ end
85
+
86
+ def decode_args(args)
87
+ shift_arg = ->(klass) { args.shift if args.first.is_a?(klass) }
88
+ count = shift_arg[0.class]
89
+ name = shift_arg[Symbol] || :master
90
+ attributes = shift_arg[Hash] || {}
91
+ raise ArgumentError, "Couldn't understand arguments" unless args.empty?
92
+ [count, name, attributes]
93
+ end
94
+
95
+ def ensure_blueprint(name)
96
+ @blueprints ||= {}
97
+ @blueprints[name] || raise(NoBlueprintError.new(self, name))
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,3 @@
1
+ module Machinist
2
+ VERSION = '3.0.0'.freeze
3
+ end
data/machinist.gemspec ADDED
@@ -0,0 +1,23 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ lib = File.expand_path('../lib', __FILE__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+ require 'machinist/version'
6
+
7
+ Gem::Specification.new do |gem|
8
+ gem.name = 'machinist_redux'
9
+ gem.version = Machinist::VERSION
10
+ gem.authors = ['Pete Yandell', 'Attila Györffy', 'Dominic Sayers']
11
+ gem.email = ['dominic@sayers.cc']
12
+ gem.description = 'Machinist makes it easy to create objects for use in tests. It generates data for the ' \
13
+ "attributes you don't care about, and constructs any necessary associated objects, leaving you "\
14
+ 'to specify only the fields you care about in your test.'
15
+ gem.summary = "Fixtures aren't fun. Machinist was."
16
+ gem.homepage = 'http://github.com/dominicsayers/machinist'
17
+ gem.license = 'MIT'
18
+
19
+ gem.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR)
20
+ gem.executables = gem.files.grep(%r{^bin/}).map { |f| File.basename(f) }
21
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
22
+ gem.require_paths = ['lib']
23
+ end
@@ -0,0 +1,106 @@
1
+ require 'support/active_record_environment'
2
+
3
+ RSpec.describe Machinist::ActiveRecord do
4
+ include ActiveRecordEnvironment
5
+
6
+ before do
7
+ empty_database!
8
+ end
9
+
10
+ context 'make' do
11
+ it 'returns an unsaved object' do
12
+ Post.blueprint {}
13
+ post = Post.make
14
+ expect(post).to be_a(Post)
15
+ expect(post).to be_new_record
16
+ end
17
+ end
18
+
19
+ context 'make!' do
20
+ it 'makes and saves objects' do
21
+ Post.blueprint {}
22
+ post = Post.make!
23
+ expect(post).to be_a(Post)
24
+ expect(post).not_to be_new_record
25
+ end
26
+
27
+ it 'raises an exception for an invalid object' do
28
+ User.blueprint {}
29
+ expect do
30
+ User.make!(username: '')
31
+ end.to raise_error(ActiveRecord::RecordInvalid)
32
+ end
33
+ end
34
+
35
+ context 'associations support' do
36
+ it 'handles belongs_to associations' do
37
+ User.blueprint do
38
+ username { "user_#{sn}" }
39
+ end
40
+ Post.blueprint do
41
+ author
42
+ end
43
+ post = Post.make!
44
+ expect(post).to be_a(Post)
45
+ expect(post).not_to be_new_record
46
+ expect(post.author).to be_a(User)
47
+ expect(post.author).not_to be_new_record
48
+ end
49
+
50
+ it 'handles has_many associations' do
51
+ Post.blueprint do
52
+ comments(3)
53
+ end
54
+ Comment.blueprint {}
55
+ post = Post.make!
56
+ expect(post).to be_a(Post)
57
+ expect(post).not_to be_new_record
58
+ expect(post.comments.size).to eq(3)
59
+ post.comments.each do |comment|
60
+ expect(comment).to be_a(Comment)
61
+ expect(comment).not_to be_new_record
62
+ end
63
+ end
64
+
65
+ it 'handles habtm associations' do
66
+ Post.blueprint do
67
+ tags(3)
68
+ end
69
+ Tag.blueprint do
70
+ name { "tag_#{sn}" }
71
+ end
72
+ post = Post.make!
73
+ expect(post).to be_a(Post)
74
+ expect(post).not_to be_new_record
75
+ expect(post.tags.size).to eq(3)
76
+ post.tags.each do |tag|
77
+ expect(tag).to be_a(Tag)
78
+ expect(tag).not_to be_new_record
79
+ end
80
+ end
81
+
82
+ it 'handles overriding associations' do
83
+ User.blueprint do
84
+ username { "user_#{sn}" }
85
+ end
86
+ Post.blueprint do
87
+ author { User.make(username: "post_author_#{sn}") }
88
+ end
89
+ post = Post.make!
90
+ expect(post).to be_a(Post)
91
+ expect(post).not_to be_new_record
92
+ expect(post.author).to be_a(User)
93
+ expect(post.author).not_to be_new_record
94
+ expect(post.author.username).to match(/^post_author_\d+$/)
95
+ end
96
+ end
97
+
98
+ context 'error handling' do
99
+ it 'raises an exception for an attribute with no value' do
100
+ User.blueprint { username }
101
+ expect do
102
+ User.make
103
+ end.to raise_error(ArgumentError)
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,101 @@
1
+ require 'ostruct'
2
+
3
+ module InheritanceSpecs
4
+ class Grandpa
5
+ extend Machinist::Machinable
6
+ attr_accessor :name, :age
7
+ end
8
+
9
+ class Dad < Grandpa
10
+ extend Machinist::Machinable
11
+ attr_accessor :name, :age
12
+ end
13
+
14
+ class Son < Dad
15
+ extend Machinist::Machinable
16
+ attr_accessor :name, :age
17
+ end
18
+ end
19
+
20
+ RSpec.describe Machinist::Blueprint do
21
+ describe 'explicit inheritance' do
22
+ it 'inherits attributes from the parent blueprint' do
23
+ parent_blueprint = described_class.new(OpenStruct) do
24
+ name { 'Fred' }
25
+ age { 97 }
26
+ end
27
+
28
+ child_blueprint = described_class.new(OpenStruct, parent: parent_blueprint) do
29
+ name { 'Bill' }
30
+ end
31
+
32
+ child = child_blueprint.make
33
+ expect(child.name).to eq('Bill')
34
+ expect(child.age).to eq(97)
35
+ end
36
+
37
+ it 'takes the serial number from the parent' do
38
+ parent_blueprint = described_class.new(OpenStruct) do
39
+ parent_serial { sn }
40
+ end
41
+
42
+ child_blueprint = described_class.new(OpenStruct, parent: parent_blueprint) do
43
+ child_serial { sn }
44
+ end
45
+
46
+ expect(parent_blueprint.make.parent_serial).to eq('0001')
47
+ expect(child_blueprint.make.child_serial).to eq('0002')
48
+ expect(parent_blueprint.make.parent_serial).to eq('0003')
49
+ end
50
+ end
51
+
52
+ describe 'class inheritance' do
53
+ before do
54
+ [InheritanceSpecs::Grandpa, InheritanceSpecs::Dad, InheritanceSpecs::Son].each(&:clear_blueprints!)
55
+ end
56
+
57
+ it 'inherits blueprinted attributes from the parent class' do
58
+ InheritanceSpecs::Dad.blueprint do
59
+ name { 'Fred' }
60
+ end
61
+ InheritanceSpecs::Son.blueprint {}
62
+ expect(InheritanceSpecs::Son.make.name).to eq('Fred')
63
+ end
64
+
65
+ it 'overrides blueprinted attributes in the child class' do
66
+ InheritanceSpecs::Dad.blueprint do
67
+ name { 'Fred' }
68
+ end
69
+ InheritanceSpecs::Son.blueprint do
70
+ name { 'George' }
71
+ end
72
+ expect(InheritanceSpecs::Dad.make.name).to eq('Fred')
73
+ expect(InheritanceSpecs::Son.make.name).to eq('George')
74
+ end
75
+
76
+ it 'inherits from blueprinted attributes in ancestor class' do
77
+ InheritanceSpecs::Grandpa.blueprint do
78
+ name { 'Fred' }
79
+ end
80
+ InheritanceSpecs::Son.blueprint {}
81
+ expect(InheritanceSpecs::Grandpa.make.name).to eq('Fred')
82
+ expect { InheritanceSpecs::Dad.make }.to raise_error(RuntimeError)
83
+ expect(InheritanceSpecs::Son.make.name).to eq('Fred')
84
+ end
85
+
86
+ it 'follows inheritance for named blueprints correctly' do
87
+ InheritanceSpecs::Dad.blueprint do
88
+ name { 'John' }
89
+ age { 56 }
90
+ end
91
+ InheritanceSpecs::Dad.blueprint(:special) do
92
+ name { 'Paul' }
93
+ end
94
+ InheritanceSpecs::Son.blueprint(:special) do
95
+ age { 37 }
96
+ end
97
+ expect(InheritanceSpecs::Son.make(:special).name).to eq('John')
98
+ expect(InheritanceSpecs::Son.make(:special).age).to eq(37)
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,76 @@
1
+ require 'ostruct'
2
+
3
+ RSpec.describe Machinist::Blueprint do
4
+ it 'makes an object of the given class' do
5
+ blueprint = described_class.new(OpenStruct) {}
6
+ expect(blueprint.make).to be_an(OpenStruct)
7
+ end
8
+
9
+ it 'constructs an attribute from the blueprint' do
10
+ blueprint = described_class.new(OpenStruct) do
11
+ name { 'Fred' }
12
+ end
13
+ expect(blueprint.make.name).to eq('Fred')
14
+ end
15
+
16
+ it 'constructs an array for an attribute in the blueprint' do
17
+ blueprint = described_class.new(OpenStruct) do
18
+ things(3) { Object.new }
19
+ end
20
+ things = blueprint.make.things
21
+ expect(things).to be_an(Array)
22
+ expect(things.size).to eq(3)
23
+ things.each { |thing| expect(thing).to be_an(Object) }
24
+ expect(things.uniq).to eq(things)
25
+ end
26
+
27
+ it 'allows passing in attributes to override the blueprint' do
28
+ block_called = false
29
+ blueprint = described_class.new(OpenStruct) do
30
+ name do
31
+ block_called = true
32
+ 'Fred'
33
+ end
34
+ end
35
+ expect(blueprint.make(name: 'Bill').name).to eq('Bill')
36
+ expect(block_called).to be_falsey
37
+ end
38
+
39
+ it 'provides a serial number within the blueprint' do
40
+ blueprint = described_class.new(OpenStruct) do
41
+ name { "Fred #{sn}" }
42
+ end
43
+ expect(blueprint.make.name).to eq('Fred 0001')
44
+ expect(blueprint.make.name).to eq('Fred 0002')
45
+ end
46
+
47
+ it 'provides access to the object being constructed within the blueprint' do
48
+ blueprint = described_class.new(OpenStruct) do
49
+ title { 'Test' }
50
+ body { object.title }
51
+ end
52
+ expect(blueprint.make.body).to eq('Test')
53
+ end
54
+
55
+ it 'allows attribute names to be strings' do
56
+ blueprint = described_class.new(OpenStruct) do
57
+ name { 'Fred' }
58
+ end
59
+ expect(blueprint.make('name' => 'Bill').name).to eq('Bill')
60
+ end
61
+
62
+ # These are normally a problem because of name clashes with the standard (but
63
+ # deprecated) Ruby methods. This test makes sure we work around this.
64
+ it 'works with type and id attributes' do
65
+ klass = Class.new do
66
+ attr_accessor :id, :type
67
+ end
68
+ blueprint = described_class.new(klass) do
69
+ id { 'custom id' }
70
+ type { 'custom type' }
71
+ end
72
+ object = blueprint.make
73
+ expect(object.id).to eq('custom id')
74
+ expect(object.type).to eq('custom type')
75
+ end
76
+ end