calificador 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 6f7600ea7641198f91585844a8701550ba999fb7b2e91c49a4d0c4e1728429d7
4
+ data.tar.gz: 9b81427ec6996cb065868cac1d0406f0af6262163c442f33b193709d838498a9
5
+ SHA512:
6
+ metadata.gz: 27229e0b80d7da65f9c9c039070b2aa42981a9d64408e2c687ec7ee1af24a82ec70eb678d03ff8e3c1d57272cc8ce45734236991b4b325ea824a2597afb356b2
7
+ data.tar.gz: 2d0ef75a9ed7549a0deaeb5d5b62c8d277db62b8dc2c3a7ac3f1f5c8e33e9b325f3a56a7c426eff5a469dd491cdf2839c5007170f92321f3d26e94ff2484507b
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2020 Jochen Seeber
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
13
+ all 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
21
+ THE SOFTWARE.
@@ -0,0 +1,97 @@
1
+ ## What is it?
2
+
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
+
5
+ ```ruby
6
+ module ReadMe
7
+ require "calificador"
8
+
9
+ # Define a test class
10
+ class HighTea
11
+ attr_accessor :scones, :posh
12
+
13
+ def eat_scone
14
+ raise "Out of scones" if scones == 0
15
+
16
+ @scones -= 1
17
+ end
18
+
19
+ def tea
20
+ @posh ? "Darjeeling First Flush" : "Earl Grey"
21
+ end
22
+ end
23
+
24
+ # Unit test
25
+ class HighTeaTest < Calificador::Test
26
+ examines HighTea
27
+
28
+ # Define a factory for the test subject
29
+ factory HighTea do
30
+ scones { 2 }
31
+ posh { false }
32
+
33
+ # Use raits to define variants of your test subject
34
+ trait :style do
35
+ posh { true }
36
+ end
37
+ end
38
+
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
44
+ end
45
+
46
+ # Get nice test names. This one will be called "HighTea must have me let a scone"
47
+ must "let me have a scone" do
48
+ count = subject.scones
49
+ subject.eat_scone
50
+ assert { subject.scones } == count - 1
51
+ end
52
+
53
+ # Adjust test subject using traits or properties for minor variations
54
+ must "complain if out of scones", scones: 0 do
55
+ assert { subject.eat_scone }.raises?(StandardError)
56
+ end
57
+
58
+ # Create subcontexts for variations of the test subject using traits and properties
59
+ with :style do
60
+ # Still nice test names. This one is "HighTea with style should have expensive tea"
61
+ must "have expensive tea" do
62
+ assert { subject.tea }.include?("First Flush")
63
+ end
64
+ end
65
+ end
66
+ end
67
+
68
+ ReadMe::HighTeaTest.run_all_tests
69
+ ```
70
+
71
+ [Minitest]: https://github.com/seattlerb/minitest
72
+ [Factory Bot]: https://github.com/thoughtbot/factory_bot
73
+ [Shoulda Context]: https://github.com/thoughtbot/shoulda-context
74
+ [Assertive Expressive]: https://github.com/rubyworks/ae
75
+
76
+ ## Why?
77
+
78
+ Calificador is an experiment in getting rid of as much mental load, boilerplate and distractions as possible. It tries to create a simple, easy to learn and easy to understand DSL for unit tests.
79
+
80
+ Only a handful of DSL methods are required:
81
+
82
+ * Test structure: examine, must, where/with/without
83
+ * Factory methods: factory, traits
84
+ * Assertions: assert, refute and raises?
85
+
86
+ 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.
87
+
88
+
89
+ ## Frequently asked questions
90
+
91
+ Q: What's with the name?
92
+
93
+ A: In the [Spanish Inquisition](https://en.wikipedia.org/wiki/Spanish_Inquisition), a defendant was examined by calificadores, who determined if there was heresy involved.
94
+
95
+ Q: The Spanish Inquisition? I did not expect that.
96
+
97
+ A: Well, [nobody expects the Spanish Inquisition](https://www.youtube.com/watch?v=sAn7baRbhx4).
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "zeitwerk"
4
+ require "minitest"
5
+
6
+ loader = Zeitwerk::Loader.for_gem
7
+ loader.setup
8
+
9
+ # Main module
10
+ module Calificador
11
+ using Calificador::Util::CoreExtensions
12
+
13
+ MISSING = Util::Missing.instance
14
+
15
+ class << self
16
+ attr_writer :call_formatter
17
+
18
+ def call_formatter
19
+ @call_formatter ||= Calificador::Util::CallFormatter.new
20
+ end
21
+ end
22
+
23
+ def self.test(subject_type, trait: Key::INHERITED_TRAIT, &block)
24
+ raise "Subject type must be a #{Module}" unless subject_type.is_a?(Module)
25
+
26
+ test_class = Class.new(Calificador::Test)
27
+ test_class_name = "#{subject_type.base_name}Test"
28
+ test_module = subject_type.parent_module
29
+
30
+ if test_module
31
+ test_module.const_set(test_class_name, test_class)
32
+ else
33
+ Kernel.const_set(test_class_name, test_class)
34
+ end
35
+
36
+ test_class.examines(subject_type, trait)
37
+ test_class.class_eval(&block)
38
+
39
+ test_class
40
+ end
41
+ end
42
+
43
+ Minitest::Assertion.prepend(Calificador::Minitest::MinitestPatches::AssertionMethods)
44
+ Class.include(Calificador::Util::ClassMixin)
@@ -0,0 +1,134 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "minitest"
4
+
5
+ module Calificador
6
+ class Assertor < BasicObject
7
+ def initialize(test:, negated: false, block: nil)
8
+ @test = test
9
+ @negated = negated ? true : false
10
+ @block = block
11
+ @value = MISSING
12
+ @triggered = false
13
+ end
14
+
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
+ def ==(other)
28
+ message = ::Kernel.proc do
29
+ actual = ::Calificador.call_formatter.value(value: __value)
30
+ expected = ::Calificador.call_formatter.value(value: other)
31
+
32
+ "Expected #{actual} (#{__value.class}) to#{@negated ? " not" : ""} be equal to #{expected} (#{other.class})"
33
+ end
34
+
35
+ __check(method: :"==", message: message, arguments: [other])
36
+ end
37
+
38
+ def !=(other)
39
+ message = ::Kernel.proc do
40
+ actual = ::Calificador.call_formatter.value(value: __value)
41
+ expected = ::Calificador.call_formatter.value(value: other)
42
+
43
+ "Expected #{actual} (#{__value.class}) to#{@negated ? "" : " not"} be equal to #{expected} (#{other.class})"
44
+ end
45
+
46
+ __check(method: :"!=", message: message, arguments: [other])
47
+ end
48
+
49
+ def identical?(other)
50
+ message = ::Kernel.proc do
51
+ actual = ::Calificador.call_formatter.value(value: __value)
52
+ expected = ::Calificador.call_formatter.value(value: other)
53
+
54
+ "Expected #{actual} (#{__value.class}) to#{@negated ? " not" : ""} be identical to #{expected} (#{other.class})"
55
+ end
56
+
57
+ __check(method: :"equal?", message: message, arguments: [other])
58
+ end
59
+
60
+ def not
61
+ Assertor.new(test: @test, negated: !@negated, block: @block)
62
+ end
63
+
64
+ def raises?(*exception_classes, &block)
65
+ @triggered = true
66
+
67
+ block ||= @block
68
+
69
+ ::Kernel.raise ::ArgumentError, "Exception classes must not be empty" if exception_classes.empty?
70
+ ::Kernel.raise ::ArgumentError, "Exception assert must have a block" if block.nil?
71
+
72
+ message = ::Kernel.proc do
73
+ actual = ::Calificador.call_formatter.value(value: @block)
74
+ "Expected #{actual} (#{@block}) to#{@negated ? " not" : ""} raise #{exception_classes.join(", ")}"
75
+ end
76
+
77
+ begin
78
+ result = block.call
79
+
80
+ @test.assert(@negated, message)
81
+
82
+ result
83
+ rescue *exception_classes => e
84
+ @test.refute(@negated, @test.exception_details(e, message.call))
85
+ e
86
+ rescue ::Minitest::Assertion, ::SignalException, ::SystemExit
87
+ ::Kernel.raise
88
+ rescue ::Exception => e # rubocop:disable Lint/RescueException
89
+ @test.assert(@negated, @test.exception_details(e, message.call))
90
+ end
91
+ end
92
+
93
+ def __check_triggered
94
+ unless @triggered
95
+ ::Kernel.raise ::StandardError, <<~MESSAGE.gsub("\n", " ")
96
+ Assertor (#{@block.source_location.join(":")}) was not triggered. You probably need to call a method to
97
+ check for something, our your check must be changed to use a method not defined on BasicObject.
98
+ MESSAGE
99
+ end
100
+ end
101
+
102
+ protected
103
+
104
+ def __value
105
+ if @value.equal?(MISSING)
106
+ ::Kernel.raise ::StandardError, "No block set for assertion" if @block.nil?
107
+
108
+ @value = @block.call
109
+ end
110
+
111
+ @value
112
+ end
113
+
114
+ def __check(method:, message: nil, arguments: [], options: {}, &block)
115
+ @triggered = true
116
+
117
+ result = __value.send(method, *arguments, **options, &block)
118
+
119
+ message ||= ::Kernel.proc do
120
+ actual = ::Calificador.call_formatter.value(value: __value)
121
+ call = ::Calificador.call_formatter.method(method: method, arguments: arguments, options: options)
122
+ "Expected #{actual} (#{__value.class}) to#{@negated ? " not" : ""} #{call}"
123
+ end
124
+
125
+ if @negated
126
+ @test.refute(result, message)
127
+ else
128
+ @test.assert(result, message)
129
+ end
130
+
131
+ result
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Calificador
4
+ module Build
5
+ # Attribute description
6
+ class Attribute
7
+ TYPES = %i[property transient init].freeze
8
+
9
+ attr_reader :name, :type, :config
10
+
11
+ def initialize(name:, type:, config:)
12
+ raise "Illegal property type #{type}. Valid types are #{TYPES.join(", ")}" unless TYPES.include?(type.to_sym)
13
+
14
+ @name = name.to_sym
15
+ @type = type.to_sym
16
+ @config = config
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ using Calificador::Util::CoreExtensions
4
+
5
+ module Calificador
6
+ module Build
7
+ # Factory calss
8
+ class AttributeContainer
9
+ class Dsl
10
+ def initialize(delegate:)
11
+ @delegate = delegate
12
+ @property_type = nil
13
+ end
14
+
15
+ def add_attribute(name, type: nil, &config)
16
+ type ||= @delegate.parent&.attribute(name: name)&.type || @property_type || :property
17
+ @delegate.add_attribute(Attribute.new(name: name, type: type, config: config))
18
+ end
19
+
20
+ def transient(&block)
21
+ raise ArgumentError, "Transient requires a block" if block.nil?
22
+
23
+ old_property_type = @property_type
24
+ @property_type = :transient
25
+
26
+ begin
27
+ instance_exec(self, &block)
28
+ ensure
29
+ @property_type = old_property_type
30
+ end
31
+ end
32
+
33
+ def init_with(&block)
34
+ raise "Initializer requires a block to create the object" if block.nil?
35
+
36
+ @delegate.init_with = block
37
+ end
38
+
39
+ def before_create(&block)
40
+ raise "Before requires a block to call" if block.nil?
41
+
42
+ @delegate.before_create = block
43
+ end
44
+
45
+ def after_create(&block)
46
+ raise "After requires a block to call" if block.nil?
47
+
48
+ @delegate.after_create = block
49
+ end
50
+
51
+ def respond_to_missing?(method, include_all = false)
52
+ if method.start_with?("__")
53
+ super
54
+ else
55
+ true
56
+ end
57
+ end
58
+
59
+ def method_missing(method, *arguments, &block)
60
+ if method.start_with?("__")
61
+ super
62
+ else
63
+ unless arguments.empty?
64
+ raise ::ArgumentError, <<~ERROR
65
+ Attribute #{method} cannot have arguments. Please use a block to configure the value
66
+ ERROR
67
+ end
68
+
69
+ raise ::ArgumentError, "Attribute #{method} must have a block to provide the value" if block.nil?
70
+
71
+ add_attribute(method, &block)
72
+ end
73
+ end
74
+ end
75
+
76
+ attr_reader :parent, :description
77
+ attr_accessor :init_with, :before_create, :after_create
78
+
79
+ def initialize(parent:, description:)
80
+ @parent = parent
81
+ @description = description.dup.freeze
82
+ @attributes = {}
83
+ @init_with = nil
84
+ @before_create = nil
85
+ @after_create = nil
86
+ end
87
+
88
+ def attributes
89
+ @attributes.dup.freeze
90
+ end
91
+
92
+ def attribute(name:)
93
+ @attributes[name]
94
+ end
95
+
96
+ def add_attribute(attribute)
97
+ raise KeyError, "Duplicate attribute name #{name}" if @attributes.key?(attribute.name)
98
+
99
+ @attributes[attribute.name] = attribute
100
+ end
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ using Calificador::Util::CoreExtensions
4
+
5
+ module Calificador
6
+ module Build
7
+ class AttributeEvaluator
8
+ EVALUATING = Object.new.freeze
9
+
10
+ class Proxy
11
+ def initialize(delegate:)
12
+ @delegate = delegate
13
+ end
14
+
15
+ def create(type, trait = nil)
16
+ key = Key[type, trait]
17
+ @delegate.create_object(key: key)
18
+ end
19
+
20
+ def respond_to_missing?(method, include_all)
21
+ @delegate.attribute?(name: method) ? true : super
22
+ end
23
+
24
+ def method_missing(method, *arguments, &block)
25
+ if @delegate.attribute?(name: method)
26
+ raise ArgumentError, "Getter must not be called with arguments" unless arguments.empty?
27
+
28
+ @delegate.value(name: method)
29
+ else
30
+ super
31
+ end
32
+ end
33
+ end
34
+
35
+ attr_reader :attributes, :values
36
+
37
+ def initialize(context:)
38
+ @context = context
39
+ @attributes = {}
40
+ @values = {}
41
+ @proxy = Proxy.new(delegate: self)
42
+ end
43
+
44
+ def create_object(key:)
45
+ @context.create_object(key: key)
46
+ end
47
+
48
+ def add_values(values)
49
+ @values.merge!(values)
50
+ end
51
+
52
+ def add_attributes(attributes)
53
+ attributes.each do |attribute|
54
+ @attributes[attribute.name] = attribute
55
+ end
56
+ end
57
+
58
+ def value(name:)
59
+ result = @values.fetch(name) do
60
+ @values[name] = EVALUATING
61
+
62
+ begin
63
+ config = @attributes.fetch(name).config
64
+ @values[name] = evaluate(&config)
65
+ rescue StandardError
66
+ @values.delete(name)
67
+ raise
68
+ end
69
+ end
70
+
71
+ raise StandardError, "Endless recursion while evaluating attribute #{name}" if result == EVALUATING
72
+
73
+ result
74
+ end
75
+
76
+ def evaluate(*arguments, **options, &block)
77
+ block.invoke_with_target(@proxy, *arguments, **options)
78
+ end
79
+
80
+ def attribute?(name:)
81
+ @attributes.key?(name)
82
+ end
83
+ end
84
+ end
85
+ end