machinist_redux 3.0.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.
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