calificador 0.1.0 → 0.2.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 +4 -4
  2. data/README.md +35 -16
  3. data/TODO.md +16 -0
  4. data/calificador.gemspec +54 -0
  5. data/lib/calificador.rb +8 -4
  6. data/lib/calificador/assert.rb +15 -0
  7. data/lib/calificador/assertor.rb +79 -35
  8. data/lib/calificador/build/attribute_evaluator.rb +34 -30
  9. data/lib/calificador/build/basic_factory.rb +195 -0
  10. data/lib/calificador/build/mock_factory.rb +151 -0
  11. data/lib/calificador/build/object_factory.rb +85 -0
  12. data/lib/calificador/build/trait.rb +0 -20
  13. data/lib/calificador/context/basic_context.rb +406 -0
  14. data/lib/calificador/context/class_method_context.rb +0 -0
  15. data/lib/calificador/{spec → context}/condition_context.rb +1 -3
  16. data/lib/calificador/{spec/type_context.rb → context/instance_context.rb} +5 -10
  17. data/lib/calificador/context/operation_context.rb +27 -0
  18. data/lib/calificador/context/override/argument_override.rb +73 -0
  19. data/lib/calificador/context/override/basic_override.rb +14 -0
  20. data/lib/calificador/context/override/factory_override.rb +31 -0
  21. data/lib/calificador/context/override/property_override.rb +61 -0
  22. data/lib/calificador/context/test_environment.rb +283 -0
  23. data/lib/calificador/{spec → context}/test_method.rb +2 -31
  24. data/lib/calificador/{spec → context}/test_root.rb +3 -15
  25. data/lib/calificador/{spec/examine_context.rb → context/type_context.rb} +7 -10
  26. data/lib/calificador/key.rb +27 -15
  27. data/lib/calificador/minitest/minitest_patches.rb +0 -2
  28. data/lib/calificador/test.rb +1 -3
  29. data/lib/calificador/test_mixin.rb +143 -139
  30. data/lib/calificador/util/call_formatter.rb +5 -5
  31. data/lib/calificador/util/core_extensions.rb +104 -79
  32. data/lib/calificador/util/proxy_object.rb +63 -0
  33. data/lib/calificador/version.rb +1 -1
  34. metadata +22 -42
  35. data/lib/calificador/build/attribute_container.rb +0 -103
  36. data/lib/calificador/build/factory.rb +0 -132
  37. data/lib/calificador/spec/basic_context.rb +0 -353
  38. data/lib/calificador/spec/class_method_context.rb +0 -42
  39. data/lib/calificador/spec/instance_method_context.rb +0 -38
  40. data/lib/calificador/spec/test_environment.rb +0 -141
  41. data/lib/calificador/spec/value_override.rb +0 -37
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6f7600ea7641198f91585844a8701550ba999fb7b2e91c49a4d0c4e1728429d7
4
- data.tar.gz: 9b81427ec6996cb065868cac1d0406f0af6262163c442f33b193709d838498a9
3
+ metadata.gz: 391cd5fa12b8710d063b8523dbb6d3350c13162ff241279c5d979ce9b7a3b555
4
+ data.tar.gz: 0d0527fda2ab45e903f057d6cd22a23588cde7b1a7c4ad352c718dc577785d59
5
5
  SHA512:
6
- metadata.gz: 27229e0b80d7da65f9c9c039070b2aa42981a9d64408e2c687ec7ee1af24a82ec70eb678d03ff8e3c1d57272cc8ce45734236991b4b325ea824a2597afb356b2
7
- data.tar.gz: 2d0ef75a9ed7549a0deaeb5d5b62c8d277db62b8dc2c3a7ac3f1f5c8e33e9b325f3a56a7c426eff5a469dd491cdf2839c5007170f92321f3d26e94ff2484507b
6
+ metadata.gz: 588366fbb53142a962e3a18b4b6fdfc9232729f22967d2ceb2105bd032293fea21d7c41ba3650d48a85827a12e926de27be63fe9523a6b9913aebb2f23fa4b39
7
+ data.tar.gz: 24c4a70eebd0528f2fb6a041a7283f65b23636048f480632e5975b6e78c862b479f0202142d45f2b9e77c3ee089b4852e4f0172989071e3f0390e10f833449c1
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- ## What is it?
1
+ # What is it?
2
2
 
3
3
  A small Gem that lets you write concise and readable unit tests. It is heavily inspired by [Minitest], [Factory Bot], and [Assertive Expressive] but tries to boil everything down to be as concise as possible:
4
4
 
@@ -6,12 +6,18 @@ A small Gem that lets you write concise and readable unit tests. It is heavily i
6
6
  module ReadMe
7
7
  require "calificador"
8
8
 
9
- # Define a test class
9
+ # Define class to test
10
10
  class HighTea
11
11
  attr_accessor :scones, :posh
12
12
 
13
+ def initialize(scones:)
14
+ raise ArgumentError, "Cannot have a negative amount of scones" if scones.negative?
15
+
16
+ @scones = scones
17
+ end
18
+
13
19
  def eat_scone
14
- raise "Out of scones" if scones == 0
20
+ raise "Out of scones" if scones.zero?
15
21
 
16
22
  @scones -= 1
17
23
  end
@@ -27,37 +33,50 @@ A small Gem that lets you write concise and readable unit tests. It is heavily i
27
33
 
28
34
  # Define a factory for the test subject
29
35
  factory HighTea do
30
- scones { 2 }
36
+ # Set properties on the created object
31
37
  posh { false }
32
38
 
33
- # Use raits to define variants of your test subject
39
+ # Define transient properties that will not be set automatically
40
+ transient do
41
+ # Constructor arguments are automatically set from properties
42
+ scones { 2 }
43
+ end
44
+
45
+ # Use traits to define variants of your test subject
34
46
  trait :style do
35
47
  posh { true }
36
48
  end
37
49
  end
38
50
 
39
- # Define test method
40
- must "have tea and scones" do
41
- # Write assertions using plain Ruby methods instead of spec DSL methods
42
- refute { subject.tea }.nil?
43
- assert { subject.scones } > 0
51
+ # Test class methods
52
+ type do
53
+ operation :new do
54
+ must "set a default amount of scones" do
55
+ # Write assertions using plain Ruby methods instead of spec DSL methods
56
+ assert { subject.scones } > 0
57
+ end
58
+
59
+ must "provide an instance that has tea" do
60
+ refute { subject.tea }.nil?
61
+ end
62
+ end
44
63
  end
45
64
 
46
- # Get nice test names. This one will be called "HighTea must have me let a scone"
65
+ # Get nice test names. This one will be called "HighTea must let me have a scone"
47
66
  must "let me have a scone" do
48
67
  count = subject.scones
49
68
  subject.eat_scone
50
69
  assert { subject.scones } == count - 1
51
70
  end
52
71
 
53
- # Adjust test subject using traits or properties for minor variations
54
- must "complain if out of scones", scones: 0 do
72
+ # Modify test subject using traits or properties for minor variations
73
+ must "complain if out of scones", props { scones { 0 } } do
55
74
  assert { subject.eat_scone }.raises?(StandardError)
56
75
  end
57
76
 
58
77
  # Create subcontexts for variations of the test subject using traits and properties
59
78
  with :style do
60
- # Still nice test names. This one is "HighTea with style should have expensive tea"
79
+ # Still nice test names. This one is "HighTea with style must have expensive tea"
61
80
  must "have expensive tea" do
62
81
  assert { subject.tea }.include?("First Flush")
63
82
  end
@@ -79,8 +98,8 @@ Calificador is an experiment in getting rid of as much mental load, boilerplate
79
98
 
80
99
  Only a handful of DSL methods are required:
81
100
 
82
- * Test structure: examine, must, where/with/without
83
- * Factory methods: factory, traits
101
+ * Test structure: examine, type, operation, where/with/without, must
102
+ * Factory methods: factory, mock, traits
84
103
  * Assertions: assert, refute and raises?
85
104
 
86
105
  It also tries to keep things simple by avoiding all the special assertion and expectation methods you need to learn for other test frameworks. Instead of having to learn that `include` is the RSpec matcher for `include?`, or Minitest's `assert_match` is used to assert regular expression matches, you can write `refute { something }.include? "value"` or `assert { something }.match %r{pattern}` using Ruby's normal `String#match` method.
data/TODO.md ADDED
@@ -0,0 +1,16 @@
1
+ # TODOs
2
+
3
+ ## must
4
+
5
+ ```ruby
6
+ must "description", trait, properties, arguments
7
+ ```
8
+
9
+
10
+ M1==>inspect
11
+ M1==>inspect
12
+ M1==>methods
13
+ M1==>private_methods
14
+ M1==>instance_variables
15
+ M1==>class
16
+ M1==>kind_of?
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ LIB_DIR = File.join(__dir__, "lib")
4
+ $LOAD_PATH.unshift(LIB_DIR) unless $LOAD_PATH.include?(LIB_DIR)
5
+
6
+ require "calificador/version"
7
+ require "json"
8
+ require "pathname"
9
+
10
+ Gem::Specification.new do |spec|
11
+ raise "RubyGems 2.0 or newer is required." unless spec.respond_to?(:metadata)
12
+
13
+ spec.name = "calificador"
14
+ spec.version = Calificador::VERSION
15
+ spec.summary = "Concise and readable unit tests"
16
+
17
+ spec.required_ruby_version = ">= 2.7"
18
+
19
+ spec.authors = ["Jochen Seeber"]
20
+ spec.email = ["jochen@seeber.me"]
21
+ spec.homepage = "https://github.com/jochenseeber/calificador"
22
+
23
+ spec.metadata["issue_tracker"] = "https://github.com/jochenseeber/calificador/issues"
24
+ spec.metadata["documentation"] = "http://jochenseeber.github.com/calificador"
25
+ spec.metadata["source_code"] = "https://github.com/jochenseeber/calificador"
26
+ spec.metadata["wiki"] = "https://github.com/jochenseeber/calificador/wiki"
27
+
28
+ spec.files = Dir[
29
+ "*.gemspec",
30
+ "*.md",
31
+ "*.txt",
32
+ "lib/**/*.rb",
33
+ ]
34
+
35
+ spec.require_paths = [
36
+ "lib",
37
+ ]
38
+
39
+ spec.bindir = "cmd"
40
+ spec.executables = spec.files.filter { |f| File.dirname(f) == "cmd" && File.file?(f) }.map { |f| File.basename(f) }
41
+
42
+ spec.add_dependency "minitest", "~> 5.14"
43
+ spec.add_dependency "zeitwerk", "~> 2.3"
44
+
45
+ spec.add_development_dependency "bundler", "~> 2.1"
46
+ spec.add_development_dependency "debase", "~> 0.2"
47
+ spec.add_development_dependency "qed", "~> 2.9"
48
+ spec.add_development_dependency "rake", "~> 13.0"
49
+ spec.add_development_dependency "rubocop", ">= 0.85"
50
+ spec.add_development_dependency "rubocop-rake", ">= 0.5.1"
51
+ spec.add_development_dependency "ruby-debug-ide", "~> 0.7"
52
+ spec.add_development_dependency "simplecov", "~> 0.18"
53
+ spec.add_development_dependency "yard", "~> 0.9"
54
+ end
data/lib/calificador.rb CHANGED
@@ -1,17 +1,21 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "zeitwerk"
3
+ require "calificador/util/core_extensions"
4
4
  require "minitest"
5
+ require "pathname"
6
+ require "zeitwerk"
5
7
 
6
8
  loader = Zeitwerk::Loader.for_gem
7
9
  loader.setup
8
10
 
9
11
  # Main module
10
12
  module Calificador
11
- using Calificador::Util::CoreExtensions
12
-
13
13
  MISSING = Util::Missing.instance
14
14
 
15
+ BASE_DIR = Pathname(__FILE__).expand_path.dirname
16
+
17
+ METHOD_PATTERN = %r{[[:alpha:]](?:[[:alnum:]]|_)*}.freeze
18
+
15
19
  class << self
16
20
  attr_writer :call_formatter
17
21
 
@@ -40,5 +44,5 @@ module Calificador
40
44
  end
41
45
  end
42
46
 
43
- Minitest::Assertion.prepend(Calificador::Minitest::MinitestPatches::AssertionMethods)
44
47
  Class.include(Calificador::Util::ClassMixin)
48
+ Minitest::Assertion.prepend(Calificador::Minitest::MinitestPatches::AssertionMethods)
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "calificador"
4
+
5
+ module Calificador
6
+ module Assert
7
+ def assert(&block)
8
+ Calificador::Assertor.new(block: block)
9
+ end
10
+
11
+ def refute(&block)
12
+ Calificador::Assertor.new(negated: true, block: block)
13
+ end
14
+ end
15
+ end
@@ -1,64 +1,67 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "minitest"
4
+ require "singleton"
4
5
 
5
6
  module Calificador
6
- class Assertor < BasicObject
7
- def initialize(test:, negated: false, block: nil)
8
- @test = test
7
+ class Assertor < Util::ProxyObject
8
+ class DefaultHandler
9
+ include Singleton
10
+
11
+ def assert(condition, message)
12
+ raise ::Minitest::Assertion, message unless condition
13
+ end
14
+
15
+ def refute(condition, message)
16
+ raise ::Minitest::Assertion, message if condition
17
+ end
18
+ end
19
+
20
+ def initialize(handler: DefaultHandler.instance, negated: false, block: nil)
21
+ super()
22
+
23
+ @handler = handler
9
24
  @negated = negated ? true : false
10
25
  @block = block
11
26
  @value = MISSING
12
27
  @triggered = false
13
28
  end
14
29
 
15
- def respond_to?(method, include_all = false)
16
- __value.respond_to?(method, include_all) || super
17
- end
18
-
19
- def method_missing(method, *arguments, **options, &block) # rubocop:disable Style/MissingRespondToMissing
20
- if __value.respond_to?(method)
21
- __check(method: method, arguments: arguments, options: options, &block)
22
- else
23
- super
24
- end
25
- end
26
-
27
30
  def ==(other)
28
31
  message = ::Kernel.proc do
29
- actual = ::Calificador.call_formatter.value(value: __value)
30
- expected = ::Calificador.call_formatter.value(value: other)
32
+ actual = ::Calificador.call_formatter.format_value(value: __value)
33
+ expected = ::Calificador.call_formatter.format_value(value: other)
31
34
 
32
35
  "Expected #{actual} (#{__value.class}) to#{@negated ? " not" : ""} be equal to #{expected} (#{other.class})"
33
36
  end
34
37
 
35
- __check(method: :"==", message: message, arguments: [other])
38
+ __check(name: :"==", message: message, arguments: [other])
36
39
  end
37
40
 
38
41
  def !=(other)
39
42
  message = ::Kernel.proc do
40
- actual = ::Calificador.call_formatter.value(value: __value)
41
- expected = ::Calificador.call_formatter.value(value: other)
43
+ actual = ::Calificador.call_formatter.format_value(value: __value)
44
+ expected = ::Calificador.call_formatter.format_value(value: other)
42
45
 
43
46
  "Expected #{actual} (#{__value.class}) to#{@negated ? "" : " not"} be equal to #{expected} (#{other.class})"
44
47
  end
45
48
 
46
- __check(method: :"!=", message: message, arguments: [other])
49
+ __check(name: :"!=", message: message, arguments: [other])
47
50
  end
48
51
 
49
52
  def identical?(other)
50
53
  message = ::Kernel.proc do
51
- actual = ::Calificador.call_formatter.value(value: __value)
52
- expected = ::Calificador.call_formatter.value(value: other)
54
+ actual = ::Calificador.call_formatter.format_value(value: __value)
55
+ expected = ::Calificador.call_formatter.format_value(value: other)
53
56
 
54
57
  "Expected #{actual} (#{__value.class}) to#{@negated ? " not" : ""} be identical to #{expected} (#{other.class})"
55
58
  end
56
59
 
57
- __check(method: :"equal?", message: message, arguments: [other])
60
+ __check(name: :"equal?", message: message, arguments: [other])
58
61
  end
59
62
 
60
63
  def not
61
- Assertor.new(test: @test, negated: !@negated, block: @block)
64
+ Assertor.new(handler: @handler, negated: !@negated, block: @block)
62
65
  end
63
66
 
64
67
  def raises?(*exception_classes, &block)
@@ -70,23 +73,23 @@ module Calificador
70
73
  ::Kernel.raise ::ArgumentError, "Exception assert must have a block" if block.nil?
71
74
 
72
75
  message = ::Kernel.proc do
73
- actual = ::Calificador.call_formatter.value(value: @block)
76
+ actual = ::Calificador.call_formatter.format_value(value: @block)
74
77
  "Expected #{actual} (#{@block}) to#{@negated ? " not" : ""} raise #{exception_classes.join(", ")}"
75
78
  end
76
79
 
77
80
  begin
78
81
  result = block.call
79
82
 
80
- @test.assert(@negated, message)
83
+ __assert(@negated, message: message)
81
84
 
82
85
  result
83
86
  rescue *exception_classes => e
84
- @test.refute(@negated, @test.exception_details(e, message.call))
87
+ __refute(@negated, message: __exception_details(e, message: message))
85
88
  e
86
89
  rescue ::Minitest::Assertion, ::SignalException, ::SystemExit
87
90
  ::Kernel.raise
88
91
  rescue ::Exception => e # rubocop:disable Lint/RescueException
89
- @test.assert(@negated, @test.exception_details(e, message.call))
92
+ __assert(@negated, message: __exception_details(e, message: message))
90
93
  end
91
94
  end
92
95
 
@@ -101,6 +104,18 @@ module Calificador
101
104
 
102
105
  protected
103
106
 
107
+ def __respond_to_missing?(name:, include_all:)
108
+ __value.respond_to?(name, false)
109
+ end
110
+
111
+ def __method_missing(name:, arguments:, keywords:, block:)
112
+ if __value.respond_to?(name)
113
+ __check(name: name, arguments: arguments, keywords: keywords, &block)
114
+ else
115
+ super
116
+ end
117
+ end
118
+
104
119
  def __value
105
120
  if @value.equal?(MISSING)
106
121
  ::Kernel.raise ::StandardError, "No block set for assertion" if @block.nil?
@@ -111,24 +126,53 @@ module Calificador
111
126
  @value
112
127
  end
113
128
 
114
- def __check(method:, message: nil, arguments: [], options: {}, &block)
129
+ def __check(name:, message: nil, arguments: [], keywords: {}, &block)
115
130
  @triggered = true
116
131
 
117
- result = __value.send(method, *arguments, **options, &block)
132
+ result = begin
133
+ __value.send(name, *arguments, **keywords, &block)
134
+ rescue ::StandardError => e
135
+ raise ::Minitest::UnexpectedError, e
136
+ end
118
137
 
119
138
  message ||= ::Kernel.proc do
120
- actual = ::Calificador.call_formatter.value(value: __value)
121
- call = ::Calificador.call_formatter.method(method: method, arguments: arguments, options: options)
139
+ actual = ::Calificador.call_formatter.format_value(value: __value)
140
+ call = ::Calificador.call_formatter.format_method(name: name, arguments: arguments, keywords: keywords)
122
141
  "Expected #{actual} (#{__value.class}) to#{@negated ? " not" : ""} #{call}"
123
142
  end
124
143
 
125
144
  if @negated
126
- @test.refute(result, message)
145
+ __refute(result, message: message)
127
146
  else
128
- @test.assert(result, message)
147
+ __assert(result, message: message)
129
148
  end
130
149
 
131
150
  result
132
151
  end
152
+
153
+ def __assert(condition, message:)
154
+ message = message.call if !condition && message.is_a?(::Proc)
155
+
156
+ @handler.assert(condition, message)
157
+ end
158
+
159
+ def __refute(condition, message:)
160
+ message = message.call if condition && message.is_a?(::Proc)
161
+
162
+ @handler.refute(condition, message)
163
+ end
164
+
165
+ def __exception_details(exception, message:)
166
+ message = message.call if message.is_a?(::Proc)
167
+
168
+ [
169
+ message,
170
+ "Class: <#{exception.class}>",
171
+ "Message: #{exception.message}",
172
+ "---Backtrace---",
173
+ exception.backtrace.map(&:to_s),
174
+ "---------------",
175
+ ].flatten.join("\n")
176
+ end
133
177
  end
134
178
  end
@@ -1,67 +1,69 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- using Calificador::Util::CoreExtensions
4
-
5
3
  module Calificador
6
4
  module Build
7
5
  class AttributeEvaluator
8
6
  EVALUATING = Object.new.freeze
9
7
 
10
- class Proxy
11
- def initialize(delegate:)
12
- @delegate = delegate
13
- end
8
+ class Proxy < Util::ProxyObject
9
+ def initialize(evaluator:)
10
+ super()
14
11
 
15
- def create(type, trait = nil)
16
- key = Key[type, trait]
17
- @delegate.create_object(key: key)
12
+ @evaluator = evaluator
13
+ @environment_proxy = evaluator.environment.proxy
18
14
  end
19
15
 
20
- def respond_to_missing?(method, include_all)
21
- @delegate.attribute?(name: method) ? true : super
16
+ protected
17
+
18
+ def __respond_to_missing?(name:, include_all:)
19
+ @evaluator.attribute?(name: name) || @environment_proxy.respond_to?(name)
22
20
  end
23
21
 
24
- def method_missing(method, *arguments, &block)
25
- if @delegate.attribute?(name: method)
22
+ def __method_missing(name:, arguments:, keywords:, block:)
23
+ if @evaluator.attribute?(name: name)
26
24
  raise ArgumentError, "Getter must not be called with arguments" unless arguments.empty?
27
25
 
28
- @delegate.value(name: method)
26
+ @evaluator.value(name: name)
29
27
  else
30
- super
28
+ @environment_proxy.__send__(name, *arguments, **keywords, &block)
31
29
  end
32
30
  end
33
31
  end
34
32
 
35
- attr_reader :attributes, :values
33
+ attr_reader :attributes, :values, :environment
36
34
 
37
- def initialize(context:)
38
- @context = context
35
+ def initialize(key:, environment:)
36
+ @key = key
37
+ @environment = environment
39
38
  @attributes = {}
40
39
  @values = {}
41
- @proxy = Proxy.new(delegate: self)
40
+ @proxy = Proxy.new(evaluator: self)
42
41
  end
43
42
 
44
- def create_object(key:)
45
- @context.create_object(key: key)
46
- end
47
-
48
- def add_values(values)
49
- @values.merge!(values)
43
+ def add_attribute(attribute)
44
+ @attributes[attribute.name] = attribute
50
45
  end
51
46
 
52
47
  def add_attributes(attributes)
53
48
  attributes.each do |attribute|
54
- @attributes[attribute.name] = attribute
49
+ add_attribute(attribute)
55
50
  end
56
51
  end
57
52
 
53
+ def add_values(values)
54
+ @values.merge!(values)
55
+ end
56
+
58
57
  def value(name:)
59
58
  result = @values.fetch(name) do
60
59
  @values[name] = EVALUATING
61
60
 
62
61
  begin
63
- config = @attributes.fetch(name).config
64
- @values[name] = evaluate(&config)
62
+ attribute = @attributes.fetch(name) do
63
+ raise KeyError, "Could not find attribute '#{name}' for factory #{@key}"
64
+ end
65
+
66
+ @values[name] = evaluate(&attribute.config)
65
67
  rescue StandardError
66
68
  @values.delete(name)
67
69
  raise
@@ -73,10 +75,12 @@ module Calificador
73
75
  result
74
76
  end
75
77
 
76
- def evaluate(*arguments, **options, &block)
77
- block.invoke_with_target(@proxy, *arguments, **options)
78
+ def evaluate(*arguments, &block)
79
+ @proxy.instance_exec(*arguments, &block)
78
80
  end
79
81
 
82
+ ruby2_keywords :evaluate
83
+
80
84
  def attribute?(name:)
81
85
  @attributes.key?(name)
82
86
  end