Home Version Yard documentation License
Fix is a modern Ruby testing framework that emphasizes clear separation between specifications and examples. Unlike traditional testing frameworks, Fix focuses on creating pure specification documents that define expected behaviors without mixing in implementation details.
- Ruby >= 3.1.0
Add to your Gemfile:
gem "fix"
Then execute:
bundle install
Or install it yourself:
gem install fix
-
Specifications vs Examples: Fix makes a clear distinction between specifications (what is expected) and examples (how it's demonstrated). This separation leads to cleaner, more maintainable test suites.
-
Logic-Free Specifications: Your specification documents remain pure and focused on defining behaviors, without getting cluttered by implementation logic.
-
Rich Semantic Language: Following RFC 2119 conventions, Fix uses precise language with keywords like MUST, SHOULD, and MAY to clearly define different requirement levels in specifications.
-
Fast Individual Testing: Tests execute quickly and independently, providing rapid feedback on specification compliance.
Define reusable properties across your specifications:
Fix do let(:name) { "Bob" } let(:age) { 42 } it MUST eq name end
Test behavior under different conditions:
Fix do with name: "Alice", role: "admin" do it MUST be_allowed end with name: "Bob", role: "guest" do it MUST_NOT be_allowed end end
Test how objects respond to specific messages:
Fix do on :upcase do it MUST eq "HELLO" end on :+, 2 do it MUST eq 42 end end
Fix provides three levels of requirements, each with clear semantic meaning:
- MUST/MUST_NOT: Absolute requirements or prohibitions
- SHOULD/SHOULD_NOT: Recommended practices with valid exceptions
- MAY: Optional features
Fix do it MUST be_valid # Required it SHOULD be_optimized # Recommended it MAY include_metadata # Optional end
Create your first test file:
# first_test.rb require "fix" Fix :HelloWorld do it MUST eq "Hello, World!" end Fix[:HelloWorld].test { "Hello, World!" }
Run it:
ruby first_test.rb
Fix is designed to work with real-world applications of any complexity. Here are some examples demonstrating how Fix can be used in different scenarios:
Here's a comprehensive example showing how to specify a user account system:
Fix :UserAccount do # Define reusable properties let(:admin) { User.new(role: "admin") } let(:guest) { User.new(role: "guest") } # Test basic instance properties it MUST be_an_instance_of User # Test with different contexts with role: "admin" do it MUST be_admin on :can_access?, "settings" do it MUST be_true end end with role: "guest" do it MUST_NOT be_admin on :can_access?, "settings" do it MUST be_false end end # Test specific methods on :full_name do with first_name: "John", last_name: "Doe" do it MUST eq "John Doe" end end on :update_password, "new_password" do it MUST change(admin, :password_hash) it MUST be_true # Return value check end end
The implementation might look like this:
class User attr_reader :role, :password_hash def initialize(role:) @role = role @password_hash = nil end def admin? role == "admin" end def can_access?(resource) return true if admin? false end def full_name "#{@first_name} #{@last_name}" end def update_password(new_password) @password_hash = Digest::SHA256.hexdigest(new_password) true end end
Here's how Fix can be used to specify a Duck class:
Fix :Duck do it SHOULD be_an_instance_of :Duck on :swims do it MUST be_an_instance_of :String it MUST eql "Swoosh..." end on :speaks do it MUST raise_exception NoMethodError end on :sings do it MAY eql "♪... ♫..." end end
The implementation:
class Duck def walks "Klop klop!" end def swims "Swoosh..." end def quacks puts "Quaaaaaack!" end end
Running the test:
Fix[:Duck].test { Duck.new }
Fix includes a comprehensive set of matchers through its integration with the Matchi library:
Basic Comparison Matchers
eq(expected)- Tests equality usingeql?it MUST eq(42) # Passes if value.eql?(42) it MUST eq("hello") # Passes if value.eql?("hello")
eql(expected)- Alias for eqbe(expected)- Tests object identity usingequal?string = "test" it MUST be(string) # Passes only if it's exactly the same object
equal(expected)- Alias for be
Type Checking Matchers
be_an_instance_of(class)- Verifies exact class matchit MUST be_an_instance_of(Array) # Passes if value.instance_of?(Array) it MUST be_an_instance_of(User) # Passes if value.instance_of?(User)
be_a_kind_of(class)- Checks class inheritance and module inclusionit MUST be_a_kind_of(Enumerable) # Passes if value.kind_of?(Enumerable) it MUST be_a_kind_of(Animal) # Passes if value inherits from Animal
Change Testing Matchers
change(object, method)- Base matcher for state changes.by(n)- Expects exact change by nit MUST change(user, :points).by(5) # Exactly +5 points
.by_at_least(n)- Expects minimum change by nit MUST change(counter, :value).by_at_least(10) # At least +10
.by_at_most(n)- Expects maximum change by nit MUST change(account, :balance).by_at_most(100) # No more than +100
.from(old).to(new)- Expects change from old to new valueit MUST change(user, :status).from("pending").to("active")
.to(new)- Expects change to new valueit MUST change(post, :title).to("Updated")
Numeric Matchers
be_within(delta).of(value)- Tests if a value is within ±delta of expected valueit MUST be_within(0.1).of(3.14) # Passes if value is between 3.04 and 3.24 it MUST be_within(5).of(100) # Passes if value is between 95 and 105
Pattern Matchers
match(regex)- Tests string against regular expression patternit MUST match(/^\d{3}-\d{2}-\d{4}$/) # SSN format it MUST match(/^[A-Z][a-z]+$/) # Capitalized word
satisfy { |value| ... }- Custom matching with blockit MUST satisfy { |num| num.even? && num > 0 } it MUST satisfy { |user| user.valid? && user.active? }
Exception Matchers
raise_exception(class)- Tests if code raises specified exceptionit MUST raise_exception(ArgumentError) it MUST raise_exception(CustomError, "specific message")
State Matchers
be_true- Tests for trueit MUST be_true # Only passes for true, not truthy values
be_false- Tests for falseit MUST be_false # Only passes for false, not falsey values
be_nil- Tests for nilit MUST be_nil # Passes only for nil
Dynamic Predicate Matchers
be_*- Dynamically matchesobject.*?methodit MUST be_empty # Calls empty? it MUST be_valid # Calls valid? it MUST be_frozen # Calls frozen?
have_*- Dynamically matchesobject.has_*?methodit MUST have_key(:id) # Calls has_key? it MUST have_errors # Calls has_errors? it MUST have_permission # Calls has_permission?
Here's an example using various matchers together:
Fix :Calculator do it MUST be_an_instance_of Calculator on :add, 2, 3 do it MUST eq 5 it MUST be_within(0.001).of(5.0) end on :divide, 1, 0 do it MUST raise_exception ZeroDivisionError end with numbers: [1, 2, 3] do it MUST_NOT be_empty it MUST satisfy { |result| result.all? { |n| n.positive? } } end with string_input: "123" do on :parse do it MUST be_a_kind_of Numeric it MUST satisfy { |n| n > 0 } end end end
Fix brings several unique advantages to Ruby testing that set it apart from traditional testing frameworks:
- Clear Separation of Concerns: Keep your specifications clean and your examples separate
- Semantic Precision: Express requirements with different levels of necessity
- Fast Execution: Get quick feedback on specification compliance
- Pure Specifications: Write specification documents that focus on behavior, not implementation
- Rich Matcher Library: Comprehensive set of matchers for different testing needs
- Modern Ruby: Takes advantage of modern Ruby features and practices
Ready to write better specifications? Visit our GitHub repository to start using Fix in your Ruby projects.
- Blog - Related articles
- Bluesky - Latest updates and discussions
- Documentation - Comprehensive guides and API reference
- Source Code - Contribute and report issues
- asciinema - Watch practical examples in action
Fix follows Semantic Versioning 2.0.
The gem is available as open source under the terms of the MIT License.
This project is sponsored by Sashité