|
1 | 1 | # frozen_string_literal: true |
2 | 2 |
|
3 | 3 | require_relative "fix/doc" |
| 4 | +require_relative "fix/error/missing_specification_block" |
4 | 5 | require_relative "fix/error/specification_not_found" |
5 | 6 | require_relative "fix/set" |
6 | 7 | require_relative "kernel" |
7 | 8 |
|
8 | | -# Namespace for the Fix framework. |
| 9 | +# The Fix framework namespace provides core functionality for managing and running test specifications. |
| 10 | +# Fix offers a unique approach to testing by clearly separating specifications from their implementations. |
9 | 11 | # |
10 | | -# Provides core functionality for managing and running test specifications. |
11 | | -# Fix supports two modes of operation: |
12 | | -# 1. Named specifications that can be referenced later |
13 | | -# 2. Anonymous specifications for immediate testing |
| 12 | +# Fix supports two primary modes of operation: |
| 13 | +# 1. Named specifications that can be stored and referenced later |
| 14 | +# 2. Anonymous specifications for immediate one-time testing |
14 | 15 | # |
15 | | -# @example Creating and running a named specification |
16 | | -# Fix :Answer do |
17 | | -# it MUST equal 42 |
| 16 | +# Available matchers through the Matchi library include: |
| 17 | +# - Basic Comparison: eq, eql, be, equal |
| 18 | +# - Type Checking: be_an_instance_of, be_a_kind_of |
| 19 | +# - State & Changes: change(object, method).by(n), by_at_least(n), by_at_most(n), from(old).to(new), to(new) |
| 20 | +# - Value Testing: be_within(delta).of(value), match(regex), satisfy { |value| ... } |
| 21 | +# - Exceptions: raise_exception(class) |
| 22 | +# - State Testing: be_true, be_false, be_nil |
| 23 | +# - Predicate Matchers: be_*, have_* (e.g., be_empty, have_key) |
| 24 | +# |
| 25 | +# @example Creating and running a named specification with various matchers |
| 26 | +# Fix :Calculator do |
| 27 | +# on(:add, 0.1, 0.2) do |
| 28 | +# it SHOULD be 0.3 # Technically true but fails due to floating point precision |
| 29 | +# it MUST be_an_instance_of(Float) # Type checking |
| 30 | +# it MUST be_within(0.0001).of(0.3) # Proper floating point comparison |
| 31 | +# end |
| 32 | +# |
| 33 | +# on(:divide, 1, 0) do |
| 34 | +# it MUST raise_exception ZeroDivisionError # Exception testing |
| 35 | +# end |
| 36 | +# end |
| 37 | +# |
| 38 | +# Fix[:Calculator].test { Calculator.new } |
| 39 | +# |
| 40 | +# @example Using state change matchers |
| 41 | +# Fix :UserAccount do |
| 42 | +# on(:deposit, 100) do |
| 43 | +# it MUST change(account, :balance).by(100) |
| 44 | +# it SHOULD change(account, :updated_at) |
| 45 | +# end |
| 46 | +# |
| 47 | +# on(:update_status, :premium) do |
| 48 | +# it MUST change(account, :status).from(:basic).to(:premium) |
| 49 | +# end |
| 50 | +# end |
| 51 | +# |
| 52 | +# @example Using predicate matchers |
| 53 | +# Fix :Collection do |
| 54 | +# with items: [] do |
| 55 | +# it MUST be_empty # Tests empty? |
| 56 | +# it MUST_NOT have_errors # Tests has_errors? |
| 57 | +# end |
18 | 58 | # end |
19 | 59 | # |
20 | | -# Fix[:Answer].test { 42 } |
| 60 | +# @example Complete specification with multiple matchers |
| 61 | +# Fix :Product do |
| 62 | +# let(:price) { 42.99 } |
21 | 63 | # |
22 | | -# @example Creating and running an anonymous specification |
23 | | -# Fix do |
24 | | -# it MUST be_positive |
25 | | -# end.test { 42 } |
| 64 | +# it MUST be_an_instance_of Product # Type checking |
| 65 | +# it MUST_NOT be_nil # Nil checking |
26 | 66 | # |
27 | | -# @see Fix::Set |
28 | | -# @see Fix::Builder |
| 67 | +# on(:price) do |
| 68 | +# it MUST be_within(0.01).of(42.99) # Floating point comparison |
| 69 | +# end |
| 70 | +# |
| 71 | +# with category: "electronics" do |
| 72 | +# it MUST satisfy { |p| p.valid? } # Custom validation |
| 73 | +# |
| 74 | +# on(:save) do |
| 75 | +# it MUST change(product, :updated_at) # State change |
| 76 | +# it SHOULD_NOT raise_exception # Exception checking |
| 77 | +# end |
| 78 | +# end |
| 79 | +# end |
| 80 | +# |
| 81 | +# @see Fix::Set For managing collections of specifications |
| 82 | +# @see Fix::Doc For storing and retrieving specifications |
| 83 | +# @see Fix::Dsl For the domain-specific language used in specifications |
| 84 | +# @see Fix::Matcher For the complete list of available matchers |
29 | 85 | # |
30 | 86 | # @api public |
31 | 87 | module Fix |
32 | | - class << self |
33 | | - # Retrieves and loads a built specification for testing. |
34 | | - # |
35 | | - # @example Run a named specification |
36 | | - # Fix[:Answer].test { 42 } |
37 | | - # |
38 | | - # @param name [String, Symbol] The constant name of the specification |
39 | | - # @return [Fix::Set] The loaded specification set ready for testing |
40 | | - # @raise [Fix::Error::SpecificationNotFound] If the named specification doesn't exist |
41 | | - def [](name) |
42 | | - name = normalize_name(name) |
43 | | - validate_specification_exists!(name) |
44 | | - Set.load(name) |
45 | | - end |
| 88 | + # Creates a new specification set, optionally registering it under a name. |
| 89 | + # |
| 90 | + # @param name [Symbol, nil] Optional name to register the specification under. |
| 91 | + # If nil, creates an anonymous specification for immediate use. |
| 92 | + # @yieldreturn [void] Block containing the specification definition using Fix DSL |
| 93 | + # @return [Fix::Set] A new specification set ready for testing |
| 94 | + # @raise [Fix::Error::MissingSpecificationBlock] If no block is provided |
| 95 | + # |
| 96 | + # @example Create a named specification |
| 97 | + # Fix :StringValidator do |
| 98 | + # on(:validate, "hello@example.com") do |
| 99 | + # it MUST be_valid_email |
| 100 | + # it MUST satisfy { |result| result.errors.empty? } |
| 101 | + # end |
| 102 | + # end |
| 103 | + # |
| 104 | + # @example Create an anonymous specification |
| 105 | + # Fix do |
| 106 | + # it MUST be_positive |
| 107 | + # it MUST be_within(0.1).of(42.0) |
| 108 | + # end.test { 42 } |
| 109 | + # |
| 110 | + # @api public |
| 111 | + def self.spec(name = nil, &block) |
| 112 | + raise Error::MissingSpecificationBlock if block.nil? |
46 | 113 |
|
47 | | - # Lists all defined specification names. |
48 | | - # |
49 | | - # @example Get all specification names |
50 | | - # Fix.specification_names #=> [:Answer, :Calculator, :UserProfile] |
51 | | - # |
52 | | - # @return [Array<Symbol>] Sorted array of specification names |
53 | | - def specification_names |
54 | | - Doc.constants.sort |
55 | | - end |
56 | | - |
57 | | - # Checks if a specification is defined. |
58 | | - # |
59 | | - # @example Check for specification existence |
60 | | - # Fix.spec_defined?(:Answer) #=> true |
61 | | - # |
62 | | - # @param name [String, Symbol] Name of the specification to check |
63 | | - # @return [Boolean] true if specification exists, false otherwise |
64 | | - def spec_defined?(name) |
65 | | - specification_names.include?(normalize_name(name)) |
66 | | - end |
| 114 | + Set.build(name, &block) |
| 115 | + end |
67 | 116 |
|
68 | | - private |
| 117 | + # Retrieves a previously registered specification by name. |
| 118 | + # |
| 119 | + # @param name [Symbol] The constant name of the specification to retrieve |
| 120 | + # @return [Fix::Set] The loaded specification set ready for testing |
| 121 | + # @raise [Fix::Error::SpecificationNotFound] If the named specification doesn't exist |
| 122 | + # |
| 123 | + # @example |
| 124 | + # # Define a specification with multiple matchers |
| 125 | + # Fix :EmailValidator do |
| 126 | + # on(:validate, "test@example.com") do |
| 127 | + # it MUST be_valid |
| 128 | + # it MUST_NOT raise_exception |
| 129 | + # it SHOULD satisfy { |result| result.score > 0.8 } |
| 130 | + # end |
| 131 | + # end |
| 132 | + # |
| 133 | + # # Later, retrieve and use it |
| 134 | + # Fix[:EmailValidator].test { MyEmailValidator } |
| 135 | + # |
| 136 | + # @see #spec For creating new specifications |
| 137 | + # @see Fix::Set#test For running tests against a specification |
| 138 | + # @see Fix::Matcher For available matchers |
| 139 | + # |
| 140 | + # @api public |
| 141 | + def self.[](name) |
| 142 | + raise Error::SpecificationNotFound, name unless key?(name) |
69 | 143 |
|
70 | | - # Converts any specification name into a symbol. |
71 | | - # This allows for consistent name handling regardless of input type. |
72 | | - # |
73 | | - # @param name [String, Symbol] The name to normalize |
74 | | - # @return [Symbol] The normalized name |
75 | | - # @example |
76 | | - # normalize_name("Answer") #=> :Answer |
77 | | - # normalize_name(:Answer) #=> :Answer |
78 | | - def normalize_name(name) |
79 | | - String(name).to_sym |
80 | | - end |
| 144 | + Set.load(name) |
| 145 | + end |
81 | 146 |
|
82 | | - # Verifies the existence of a specification and raises an error if not found. |
83 | | - # This ensures early failure when attempting to use undefined specifications. |
84 | | - # |
85 | | - # @param name [Symbol] The specification name to validate |
86 | | - # @raise [Fix::Error::SpecificationNotFound] If specification doesn't exist |
87 | | - def validate_specification_exists!(name) |
88 | | - return if spec_defined?(name) |
| 147 | + # Lists all defined specification names. |
| 148 | + # |
| 149 | + # @return [Array<Symbol>] Sorted array of registered specification names |
| 150 | + # |
| 151 | + # @example |
| 152 | + # Fix :First do; end |
| 153 | + # Fix :Second do; end |
| 154 | + # |
| 155 | + # Fix.keys #=> [:First, :Second] |
| 156 | + # |
| 157 | + # @api public |
| 158 | + def self.keys |
| 159 | + Doc.constants.sort |
| 160 | + end |
89 | 161 |
|
90 | | - raise Error::SpecificationNotFound, name |
91 | | - end |
| 162 | + # Checks if a specification is registered under the given name. |
| 163 | + # |
| 164 | + # @param name [Symbol] The name to check for |
| 165 | + # @return [Boolean] true if a specification exists with this name, false otherwise |
| 166 | + # |
| 167 | + # @example |
| 168 | + # Fix :Example do |
| 169 | + # it MUST be_an_instance_of(Example) |
| 170 | + # end |
| 171 | + # |
| 172 | + # Fix.key?(:Example) #=> true |
| 173 | + # Fix.key?(:Missing) #=> false |
| 174 | + # |
| 175 | + # @api public |
| 176 | + def self.key?(name) |
| 177 | + keys.include?(name) |
92 | 178 | end |
93 | 179 | end |
0 commit comments