fix 0.21 → 1.0.0.beta1
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.
- checksums.yaml +4 -4
- data/LICENSE.md +2 -2
- data/README.md +64 -397
- data/lib/fix/context.rb +147 -0
- data/lib/fix/expectation_result_not_found_error.rb +5 -0
- data/lib/fix/it.rb +31 -0
- data/lib/fix/suspicious_success_error.rb +5 -0
- data/lib/fix.rb +15 -169
- data/lib/kernel.rb +2 -96
- metadata +94 -39
- data/lib/fix/doc.rb +0 -118
- data/lib/fix/dsl.rb +0 -139
- data/lib/fix/error/invalid_specification_name.rb +0 -12
- data/lib/fix/error/missing_specification_block.rb +0 -14
- data/lib/fix/error/missing_subject_block.rb +0 -15
- data/lib/fix/error/specification_not_found.rb +0 -12
- data/lib/fix/matcher.rb +0 -76
- data/lib/fix/requirement.rb +0 -119
- data/lib/fix/run.rb +0 -88
- data/lib/fix/set.rb +0 -224
data/lib/fix/dsl.rb
DELETED
@@ -1,139 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require "defi/method"
|
4
|
-
|
5
|
-
require_relative "matcher"
|
6
|
-
require_relative "requirement"
|
7
|
-
|
8
|
-
module Fix
|
9
|
-
# Abstract class for handling the domain-specific language.
|
10
|
-
#
|
11
|
-
# @api private
|
12
|
-
class Dsl
|
13
|
-
extend Matcher
|
14
|
-
extend Requirement
|
15
|
-
|
16
|
-
# Sets a user-defined property.
|
17
|
-
#
|
18
|
-
# @example
|
19
|
-
# require "fix"
|
20
|
-
#
|
21
|
-
# Fix do
|
22
|
-
# let(:name) { "Bob" }
|
23
|
-
# end
|
24
|
-
#
|
25
|
-
# @param name [String, Symbol] The name of the property.
|
26
|
-
# @yield The block that defines the property's value
|
27
|
-
# @yieldreturn [Object] The value to be returned by the property
|
28
|
-
#
|
29
|
-
# @return [Symbol] A private method that defines the block content.
|
30
|
-
#
|
31
|
-
# @api public
|
32
|
-
def self.let(name, &)
|
33
|
-
private define_method(name, &)
|
34
|
-
end
|
35
|
-
|
36
|
-
# Defines an example group with user-defined properties that describes a
|
37
|
-
# unit to be tested.
|
38
|
-
#
|
39
|
-
# @example
|
40
|
-
# require "fix"
|
41
|
-
#
|
42
|
-
# Fix do
|
43
|
-
# with password: "secret" do
|
44
|
-
# it MUST be true
|
45
|
-
# end
|
46
|
-
# end
|
47
|
-
#
|
48
|
-
# @param kwargs [Hash] The list of properties to define in this context
|
49
|
-
# @yield The block that defines the specs for this context
|
50
|
-
# @yieldreturn [void]
|
51
|
-
#
|
52
|
-
# @return [Class] A new class representing this context
|
53
|
-
#
|
54
|
-
# @api public
|
55
|
-
def self.with(**kwargs, &)
|
56
|
-
klass = ::Class.new(self)
|
57
|
-
klass.const_get(:CONTEXTS) << klass
|
58
|
-
kwargs.each { |name, value| klass.let(name) { value } }
|
59
|
-
klass.instance_eval(&)
|
60
|
-
klass
|
61
|
-
end
|
62
|
-
|
63
|
-
# Defines an example group that describes a unit to be tested.
|
64
|
-
#
|
65
|
-
# @example
|
66
|
-
# require "fix"
|
67
|
-
#
|
68
|
-
# Fix do
|
69
|
-
# on :+, 2 do
|
70
|
-
# it MUST be 42
|
71
|
-
# end
|
72
|
-
# end
|
73
|
-
#
|
74
|
-
# @param method_name [String, Symbol] The method to send to the subject
|
75
|
-
# @param args [Array] Positional arguments to pass to the method
|
76
|
-
# @param kwargs [Hash] Keyword arguments to pass to the method
|
77
|
-
# @yield The block containing the specifications for this context
|
78
|
-
# @yieldreturn [void]
|
79
|
-
#
|
80
|
-
# @return [Class] A new class representing this context
|
81
|
-
#
|
82
|
-
# @api public
|
83
|
-
def self.on(method_name, *args, **kwargs, &block)
|
84
|
-
klass = ::Class.new(self)
|
85
|
-
klass.const_get(:CONTEXTS) << klass
|
86
|
-
|
87
|
-
const_name = :"MethodContext_#{block.object_id}"
|
88
|
-
const_set(const_name, klass)
|
89
|
-
|
90
|
-
klass.define_singleton_method(:challenges) do
|
91
|
-
challenge = ::Defi::Method.new(method_name, *args, **kwargs)
|
92
|
-
super() + [challenge]
|
93
|
-
end
|
94
|
-
|
95
|
-
klass.instance_eval(&block)
|
96
|
-
klass
|
97
|
-
end
|
98
|
-
|
99
|
-
# Defines a concrete spec definition.
|
100
|
-
#
|
101
|
-
# @example
|
102
|
-
# require "fix"
|
103
|
-
#
|
104
|
-
# Fix { it MUST be 42 }
|
105
|
-
#
|
106
|
-
# Fix do
|
107
|
-
# it { MUST be 42 }
|
108
|
-
# end
|
109
|
-
#
|
110
|
-
# @param requirement [Object, nil] The requirement to test
|
111
|
-
# @yield A block defining the requirement if not provided directly
|
112
|
-
# @yieldreturn [Object] The requirement definition
|
113
|
-
#
|
114
|
-
# @return [Symbol] Name of the generated test method
|
115
|
-
#
|
116
|
-
# @raise [ArgumentError] If neither or both requirement and block are provided
|
117
|
-
#
|
118
|
-
# @api public
|
119
|
-
def self.it(requirement = nil, &block)
|
120
|
-
raise ::ArgumentError, "Must provide either requirement or block, not both" if requirement && block
|
121
|
-
raise ::ArgumentError, "Must provide either requirement or block" unless requirement || block
|
122
|
-
|
123
|
-
location = caller_locations(1, 1).fetch(0)
|
124
|
-
location = [location.path, location.lineno].join(":")
|
125
|
-
|
126
|
-
test_method_name = :"test_#{(requirement || block).object_id}"
|
127
|
-
define_method(test_method_name) do
|
128
|
-
[location, requirement || singleton_class.class_eval(&block), self.class.challenges]
|
129
|
-
end
|
130
|
-
end
|
131
|
-
|
132
|
-
# The list of challenges to be addressed to the object to be tested.
|
133
|
-
#
|
134
|
-
# @return [Array<Defi::Method>] A list of challenges.
|
135
|
-
def self.challenges
|
136
|
-
[]
|
137
|
-
end
|
138
|
-
end
|
139
|
-
end
|
@@ -1,12 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Fix
|
4
|
-
module Error
|
5
|
-
# Error raised when an invalid specification name is provided during declaration
|
6
|
-
class InvalidSpecificationName < ::NameError
|
7
|
-
def initialize(name)
|
8
|
-
super("Invalid specification name '#{name}'. Specification names must be valid Ruby constants.")
|
9
|
-
end
|
10
|
-
end
|
11
|
-
end
|
12
|
-
end
|
@@ -1,14 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Fix
|
4
|
-
module Error
|
5
|
-
# Error raised when attempting to build a specification without a block
|
6
|
-
class MissingSpecificationBlock < ::ArgumentError
|
7
|
-
MISSING_BLOCK_ERROR = "Block is required for building a specification"
|
8
|
-
|
9
|
-
def initialize
|
10
|
-
super(MISSING_BLOCK_ERROR)
|
11
|
-
end
|
12
|
-
end
|
13
|
-
end
|
14
|
-
end
|
@@ -1,15 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Fix
|
4
|
-
module Error
|
5
|
-
# Error raised when attempting to test a specification without providing a subject block
|
6
|
-
class MissingSubjectBlock < ::ArgumentError
|
7
|
-
MISSING_BLOCK_ERROR = "Subject block is required for testing a specification. " \
|
8
|
-
"Use: test { subject } or match? { subject }"
|
9
|
-
|
10
|
-
def initialize
|
11
|
-
super(MISSING_BLOCK_ERROR)
|
12
|
-
end
|
13
|
-
end
|
14
|
-
end
|
15
|
-
end
|
@@ -1,12 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Fix
|
4
|
-
module Error
|
5
|
-
# Error raised when a specification cannot be found at runtime
|
6
|
-
class SpecificationNotFound < ::NameError
|
7
|
-
def initialize(name)
|
8
|
-
super("Specification '#{name}' not found. Make sure it's defined before running the test.")
|
9
|
-
end
|
10
|
-
end
|
11
|
-
end
|
12
|
-
end
|
data/lib/fix/matcher.rb
DELETED
@@ -1,76 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require "matchi"
|
4
|
-
|
5
|
-
module Fix
|
6
|
-
# Collection of expectation matchers.
|
7
|
-
# Provides a comprehensive set of matchers for testing different aspects of objects.
|
8
|
-
#
|
9
|
-
# The following matchers are available:
|
10
|
-
#
|
11
|
-
# Basic Comparison:
|
12
|
-
# - eq(expected) # Checks equality using eql?
|
13
|
-
# it MUST eq(42)
|
14
|
-
# it MUST eq("hello")
|
15
|
-
# - eql(expected) # Alias for eq
|
16
|
-
# - be(expected) # Checks exact object identity using equal?
|
17
|
-
# string = "test"
|
18
|
-
# it MUST be(string) # Passes only if it's the same object
|
19
|
-
# - equal(expected) # Alias for be
|
20
|
-
#
|
21
|
-
# Type Checking:
|
22
|
-
# - be_an_instance_of(class) # Checks exact class match
|
23
|
-
# it MUST be_an_instance_of(Array)
|
24
|
-
# - be_a_kind_of(class) # Checks class inheritance and module inclusion
|
25
|
-
# it MUST be_a_kind_of(Enumerable)
|
26
|
-
#
|
27
|
-
# State & Changes:
|
28
|
-
# - change(object, method) # Base for checking state changes
|
29
|
-
# .by(n) # Exact change by n
|
30
|
-
# it MUST change(user, :points).by(5)
|
31
|
-
# .by_at_least(n) # Minimum change by n
|
32
|
-
# it MUST change(counter, :value).by_at_least(10)
|
33
|
-
# .by_at_most(n) # Maximum change by n
|
34
|
-
# it MUST change(account, :balance).by_at_most(100)
|
35
|
-
# .from(old).to(new) # Change from old to new value
|
36
|
-
# it MUST change(user, :status).from("pending").to("active")
|
37
|
-
# .to(new) # Change to new value
|
38
|
-
# it MUST change(post, :title).to("Updated")
|
39
|
-
#
|
40
|
-
# Value Testing:
|
41
|
-
# - be_within(delta).of(value) # Checks numeric value within delta
|
42
|
-
# it MUST be_within(0.1).of(3.14)
|
43
|
-
# - match(regex) # Tests against regular expression
|
44
|
-
# it MUST match(/^\d{3}-\d{2}-\d{4}$/) # SSN format
|
45
|
-
# - satisfy { |value| ... } # Custom matcher with block
|
46
|
-
# it MUST satisfy { |num| num.even? && num > 0 }
|
47
|
-
#
|
48
|
-
# Exceptions:
|
49
|
-
# - raise_exception(class) # Checks if code raises exception
|
50
|
-
# it MUST raise_exception(ArgumentError)
|
51
|
-
# it MUST raise_exception(CustomError, "specific message")
|
52
|
-
#
|
53
|
-
# State Testing:
|
54
|
-
# - be_true # Tests for true
|
55
|
-
# it MUST be_true # Only passes for true, not truthy values
|
56
|
-
# - be_false # Tests for false
|
57
|
-
# it MUST be_false # Only passes for false, not falsey values
|
58
|
-
# - be_nil # Tests for nil
|
59
|
-
# it MUST be_nil
|
60
|
-
#
|
61
|
-
# Predicate Matchers:
|
62
|
-
# - be_* # Matches object.*? method
|
63
|
-
# it MUST be_empty # Calls empty?
|
64
|
-
# it MUST be_valid # Calls valid?
|
65
|
-
# it MUST be_frozen # Calls frozen?
|
66
|
-
# - have_* # Matches object.has_*? method
|
67
|
-
# it MUST have_key(:id) # Calls has_key?
|
68
|
-
# it MUST have_errors # Calls has_errors?
|
69
|
-
#
|
70
|
-
# @note All matchers can be used with MUST, MUST_NOT, SHOULD, SHOULD_NOT, and MAY
|
71
|
-
# @see https://github.com/fixrb/matchi for more details about the matchers
|
72
|
-
# @api private
|
73
|
-
module Matcher
|
74
|
-
include Matchi
|
75
|
-
end
|
76
|
-
end
|
data/lib/fix/requirement.rb
DELETED
@@ -1,119 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require "spectus/requirement/optional"
|
4
|
-
require "spectus/requirement/recommended"
|
5
|
-
require "spectus/requirement/required"
|
6
|
-
|
7
|
-
module Fix
|
8
|
-
# Implements requirement levels as defined in RFC 2119.
|
9
|
-
# Provides methods for specifying different levels of requirements
|
10
|
-
# in test specifications: MUST, SHOULD, and MAY.
|
11
|
-
#
|
12
|
-
# @api private
|
13
|
-
module Requirement
|
14
|
-
# rubocop:disable Naming/MethodName
|
15
|
-
|
16
|
-
# This method means that the definition is an absolute requirement of the
|
17
|
-
# specification.
|
18
|
-
#
|
19
|
-
# @example Test exact equality
|
20
|
-
# it MUST eq(42)
|
21
|
-
#
|
22
|
-
# @example Test type matching
|
23
|
-
# it MUST be_an_instance_of(User)
|
24
|
-
#
|
25
|
-
# @example Test state changes
|
26
|
-
# it MUST change(user, :status).from("pending").to("active")
|
27
|
-
#
|
28
|
-
# @param matcher [#match?] The matcher that defines the required condition
|
29
|
-
# @return [::Spectus::Requirement::Required] An absolute requirement level instance
|
30
|
-
#
|
31
|
-
# @api public
|
32
|
-
def MUST(matcher)
|
33
|
-
::Spectus::Requirement::Required.new(negate: false, matcher:)
|
34
|
-
end
|
35
|
-
|
36
|
-
# This method means that the definition is an absolute prohibition of the
|
37
|
-
# specification.
|
38
|
-
#
|
39
|
-
# @example Test prohibited state
|
40
|
-
# it MUST_NOT be_nil
|
41
|
-
#
|
42
|
-
# @example Test prohibited type
|
43
|
-
# it MUST_NOT be_a_kind_of(AdminUser)
|
44
|
-
#
|
45
|
-
# @example Test prohibited exception
|
46
|
-
# it MUST_NOT raise_exception(SecurityError)
|
47
|
-
#
|
48
|
-
# @param matcher [#match?] The matcher that defines the prohibited condition
|
49
|
-
# @return [::Spectus::Requirement::Required] An absolute prohibition level instance
|
50
|
-
#
|
51
|
-
# @api public
|
52
|
-
def MUST_NOT(matcher)
|
53
|
-
::Spectus::Requirement::Required.new(negate: true, matcher:)
|
54
|
-
end
|
55
|
-
|
56
|
-
# This method means that there may exist valid reasons in particular
|
57
|
-
# circumstances to ignore this requirement, but the implications must be
|
58
|
-
# understood and carefully weighed.
|
59
|
-
#
|
60
|
-
# @example Test numeric boundaries
|
61
|
-
# it SHOULD be_within(0.1).of(expected_value)
|
62
|
-
#
|
63
|
-
# @example Test pattern matching
|
64
|
-
# it SHOULD match(/^[A-Z][a-z]+$/)
|
65
|
-
#
|
66
|
-
# @example Test custom condition
|
67
|
-
# it SHOULD satisfy { |obj| obj.valid? && obj.complete? }
|
68
|
-
#
|
69
|
-
# @param matcher [#match?] The matcher that defines the recommended condition
|
70
|
-
# @return [::Spectus::Requirement::Recommended] A recommended requirement level instance
|
71
|
-
#
|
72
|
-
# @api public
|
73
|
-
def SHOULD(matcher)
|
74
|
-
::Spectus::Requirement::Recommended.new(negate: false, matcher:)
|
75
|
-
end
|
76
|
-
|
77
|
-
# This method means that there may exist valid reasons in particular
|
78
|
-
# circumstances when the behavior is acceptable, but the implications should be
|
79
|
-
# understood and weighed carefully.
|
80
|
-
#
|
81
|
-
# @example Test state changes to avoid
|
82
|
-
# it SHOULD_NOT change(object, :state)
|
83
|
-
#
|
84
|
-
# @example Test predicate conditions to avoid
|
85
|
-
# it SHOULD_NOT be_empty
|
86
|
-
# it SHOULD_NOT have_errors
|
87
|
-
#
|
88
|
-
# @param matcher [#match?] The matcher that defines the discouraged condition
|
89
|
-
# @return [::Spectus::Requirement::Recommended] A discouraged requirement level instance
|
90
|
-
#
|
91
|
-
# @api public
|
92
|
-
def SHOULD_NOT(matcher)
|
93
|
-
::Spectus::Requirement::Recommended.new(negate: true, matcher:)
|
94
|
-
end
|
95
|
-
|
96
|
-
# This method means that the item is truly optional. Implementations may
|
97
|
-
# include this feature if it enhances their product, and must be prepared to
|
98
|
-
# interoperate with implementations that include or omit this feature.
|
99
|
-
#
|
100
|
-
# @example Test optional functionality
|
101
|
-
# it MAY respond_to(:cache_key)
|
102
|
-
#
|
103
|
-
# @example Test optional state
|
104
|
-
# it MAY be_frozen
|
105
|
-
#
|
106
|
-
# @example Test optional predicates
|
107
|
-
# it MAY have_attachments
|
108
|
-
#
|
109
|
-
# @param matcher [#match?] The matcher that defines the optional condition
|
110
|
-
# @return [::Spectus::Requirement::Optional] An optional requirement level instance
|
111
|
-
#
|
112
|
-
# @api public
|
113
|
-
def MAY(matcher)
|
114
|
-
::Spectus::Requirement::Optional.new(negate: false, matcher:)
|
115
|
-
end
|
116
|
-
|
117
|
-
# rubocop:enable Naming/MethodName
|
118
|
-
end
|
119
|
-
end
|
data/lib/fix/run.rb
DELETED
@@ -1,88 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require "expresenter/fail"
|
4
|
-
|
5
|
-
module Fix
|
6
|
-
# Executes a test specification by running a subject against a set of challenges
|
7
|
-
# and requirements.
|
8
|
-
#
|
9
|
-
# The Run class orchestrates test execution by:
|
10
|
-
# 1. Evaluating the test subject in the proper environment
|
11
|
-
# 2. Applying a series of method challenges to the result
|
12
|
-
# 3. Verifying the final value against the requirement
|
13
|
-
#
|
14
|
-
# @example Running a simple test
|
15
|
-
# run = Run.new(env, requirement)
|
16
|
-
# run.test { MyClass.new }
|
17
|
-
#
|
18
|
-
# @example Running with method challenges
|
19
|
-
# run = Run.new(env, requirement, challenge1, challenge2)
|
20
|
-
# run.test { MyClass.new } # Will call methods defined in challenges
|
21
|
-
#
|
22
|
-
# @api private
|
23
|
-
class Run
|
24
|
-
# The test environment containing defined variables and methods
|
25
|
-
# @return [::Fix::Dsl] A context instance
|
26
|
-
attr_reader :environment
|
27
|
-
|
28
|
-
# The specification requirement to validate against
|
29
|
-
# @return [::Spectus::Requirement::Base] An expectation
|
30
|
-
attr_reader :requirement
|
31
|
-
|
32
|
-
# The list of method calls to apply to the subject
|
33
|
-
# @return [Array<::Defi::Method>] A list of challenges
|
34
|
-
attr_reader :challenges
|
35
|
-
|
36
|
-
# Initializes a new test run with the given environment and challenges.
|
37
|
-
#
|
38
|
-
# @param environment [::Fix::Dsl] Context instance with test setup
|
39
|
-
# @param requirement [::Spectus::Requirement::Base] Expectation to verify
|
40
|
-
# @param challenges [Array<::Defi::Method>] Method calls to apply
|
41
|
-
#
|
42
|
-
# @example
|
43
|
-
# Run.new(test_env, must_be_positive, increment_method)
|
44
|
-
def initialize(environment, requirement, *challenges)
|
45
|
-
@environment = environment
|
46
|
-
@requirement = requirement
|
47
|
-
@challenges = challenges
|
48
|
-
end
|
49
|
-
|
50
|
-
# Verifies if the subject meets the requirement after applying all challenges.
|
51
|
-
#
|
52
|
-
# @param subject [Proc] The block of code to be tested
|
53
|
-
#
|
54
|
-
# @raise [::Expresenter::Fail] When the test specification fails
|
55
|
-
# @return [::Expresenter::Pass] When the test specification passes
|
56
|
-
#
|
57
|
-
# @example Basic testing
|
58
|
-
# run.test { 42 }
|
59
|
-
#
|
60
|
-
# @example Testing with subject modification
|
61
|
-
# run.test { User.new(name: "John") }
|
62
|
-
#
|
63
|
-
# @see https://github.com/fixrb/expresenter
|
64
|
-
def test(&subject)
|
65
|
-
requirement.call { actual_value(&subject) }
|
66
|
-
rescue ::Expresenter::Fail => e
|
67
|
-
e
|
68
|
-
end
|
69
|
-
|
70
|
-
private
|
71
|
-
|
72
|
-
# Computes the final value to test by applying all challenges to the subject.
|
73
|
-
#
|
74
|
-
# @param subject [Proc] The initial test subject
|
75
|
-
# @return [#object_id] The final value after applying all challenges
|
76
|
-
#
|
77
|
-
# @example Internal process
|
78
|
-
# # If challenges are [:upcase, :reverse]
|
79
|
-
# # and subject returns "hello"
|
80
|
-
# # actual_value will return "OLLEH"
|
81
|
-
def actual_value(&subject)
|
82
|
-
initial_value = environment.instance_eval(&subject)
|
83
|
-
challenges.inject(initial_value) do |obj, challenge|
|
84
|
-
challenge.to(obj).call
|
85
|
-
end
|
86
|
-
end
|
87
|
-
end
|
88
|
-
end
|