fix 0.21 → 1.0.0.beta1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|