Jekyll2025-01-03T21:57:04+00:00https://fixrb.dev/feed.xmlFix specing frameworkFix is a modern Ruby testing framework built around a key architectural principle: the complete separation between specifications and tests. It allows you to write pure specification documents that define expected behaviors, and then independently challenge any implementation against these specifications. Cyril KatoThe Three Levels of Requirements Inspired by RFC 21192024-12-30T00:00:00+00:002024-12-30T00:00:00+00:00https://fixrb.dev/framework/testing/2024/12/30/the-three-levels-of-requirements-inspired-by-rfc-2119In software development, precision in language is crucial. This is particularly true when defining test specifications. Fix, through the Spectus library, introduces three levels of requirements directly inspired by RFC 2119, offering rich and nuanced semantics for your specifications.

The Three Requirement Levels

MUST/MUST_NOT: The Absolute

The MUST level represents an absolute requirement of the specification. When you use MUST, you indicate that a feature is mandatory and non-negotiable. Formally, for a test using MUST to pass, the expectation must be conclusively met. If the expectation fails, the test fails unconditionally.

Fix :Password do
  it MUST_NOT be_empty
  it MUST have_length_of(8..128)

  with content: "password123" do
    it MUST include_number
    it MUST_NOT be_common_password
  end
end

In this example, each test marked with MUST represents a rule that cannot be broken. An empty or too short password is simply not acceptable. The test will fail if any of these conditions are not met, regardless of any other circumstances.

SHOULD/SHOULD_NOT: The Recommendation

SHOULD indicates a strong recommendation that can be ignored in particular circumstances, but whose implications must be understood and carefully evaluated. From a technical standpoint, if an expectation marked with SHOULD fails, the test can still pass if no exception is raised. This allows for graceful degradation of optional but recommended features.

Fix :EmailService do
  on :send, to: "[email protected]" do
    it MUST deliver_email
    it SHOULD be_rate_limited
    it SHOULD_NOT take_longer_than(5.seconds)
  end
end

Here, while email delivery is mandatory (MUST), rate limiting is strongly recommended (SHOULD) but could be disabled in specific contexts. If the rate limiting check fails but doesn’t raise an exception, the test will still pass, indicating that while the recommendation wasn’t followed, the system is still functioning as expected.

MAY: The Optional

MAY indicates a truly optional feature. In Fix’s implementation, a MAY requirement has a unique behavior: the test will pass either if the expectation is met OR if a NoMethodError is raised. This elegantly handles cases where optional features are not implemented at all. This is particularly valuable for specifying features that might be implemented differently across various contexts or might not be implemented at all.

Fix :UserProfile do
  on :avatar do
    it MUST be_valid_image
    it SHOULD be_less_than(5.megabytes)
    it MAY be_square
    it MAY support_animation
  end
end

In this example:

  • The avatar must be a valid image (MUST) - this will fail if not met
  • It should be lightweight (SHOULD) - this will pass if it fails without exception
  • It may be square (MAY) - this will pass if either:
    1. The expectation is met (the avatar is square)
    2. The method to check squareness isn’t implemented (raises NoMethodError)
  • Similarly, animation support is optional and can be entirely unimplemented

This three-level system allows for precise specification of requirements while maintaining flexibility in implementation. Here’s a more complex example that demonstrates all three levels working together:

Fix :Document do
  # Absolute requirements - must pass their expectations
  it MUST have_content
  it MUST have_created_at

  # Strong recommendations - can fail without exception
  it SHOULD have_author
  it SHOULD be_versioned

  # Optional features - can be unimplemented
  it MAY be_encryptable
  it MAY support_collaborative_editing

  on :publish do
    it MUST change(document, :status).to(:published)  # Must succeed
    it SHOULD notify_subscribers             # Can fail gracefully
    it MAY trigger_indexing                  # Can be unimplemented
  end
end

Historical Evolution: From RSpec to Fix

This semantic approach contrasts with RSpec’s history. Originally, RSpec used the should keyword as its main interface:

# Old RSpec style
describe User do
  it "should validate email" do
    user.email = "invalid"
    user.should_not be_valid
  end
end

However, this approach had several issues:

  • Monkey-patching Object to add should could cause conflicts
  • Using should for absolute requirements was semantically incorrect
  • Code became harder to maintain due to global namespace pollution

RSpec eventually migrated to the expect syntax:

# Modern RSpec
describe User do
  it "validates email" do
    user.email = "invalid"
    expect(user).not_to be_valid
  end
end

The Fix Approach: Clarity and Precision

Fix takes a different path by fully embracing RFC 2119 semantics. Here’s a complete example illustrating all three levels:

Fix :Article do
  # Absolute requirements
  it MUST have_title
  it MUST have_content

  # Strong recommendations
  it SHOULD have_meta_description
  it SHOULD be_properly_formatted

  # Optional features
  it MAY have_cover_image
  it MAY have_comments_enabled

  on :publish do
    it MUST change(article, :status).to(:published)
    it SHOULD trigger_notification
    it MAY be_featured
  end
end

# Test against a specific implementation
Fix[:Article].test { Article.new(title: "Test", content: "Content") }

This approach offers several advantages:

  • Clear and precise semantics for each requirement level
  • No global monkey-patching
  • Living documentation that exactly reflects developer intentions
  • Better team communication through standardized vocabulary

Conclusion

Fix’s three requirement levels, inherited from Spectus, offer a powerful and nuanced way to express your testing expectations. This approach, combined with the clear separation between specifications and implementations, makes Fix particularly well-suited for writing maintainable and communicative tests.

RSpec’s evolution shows us the importance of precise semantics and clean architecture. Fix capitalizes on these lessons while offering a modern and elegant approach to Ruby testing.

]]>
Cyril Kato
Rethinking Test Architecture with Clear Specification Separation2024-12-29T00:00:00+00:002024-12-29T00:00:00+00:00https://fixrb.dev/framework/testing/2024/12/29/rethinking-test-architecture-with-fixIn the Ruby development world, testing frameworks like RSpec or Minitest have become industry standards. However, Fix proposes a radically different approach that deserves our attention: a clean separation between specifications and their implementation.

Traditional Architecture: A Mix of Concerns

Traditionally, Ruby testing frameworks mix the definition of expected behaviors and the concrete examples that verify them in the same file. Let’s look at a classic example with RSpec:

RSpec.describe Calculator do
  subject(:calculator) do
    described_class.new(first_number)
  end
  describe "when first number is 2" do
    let(:first_number) do
      2
    end
    context "#add" do
      let(:second_number) do
        3
      end
      it "adds two numbers" do
        expect(calculator.add(second_number)).to be(5)
      end
    end
  end
end

In this example, the specification (adding two numbers) is wrapped in several layers of setup and context, mixing the what (adding numbers should equal 5) with the how (creating instances, setting up variables).

The Fix Approach: A Clear Separation of Responsibilities

Fix adopts a radically different philosophy by clearly dividing two aspects:

  1. Specifications: pure documents that define expected behaviors
  2. Tests: concrete implementations that challenge these specifications

Here’s how the same calculator test looks with Fix:

# 1. The specification - a pure and reusable document
Fix :Calculator do
  on(:new, 2) do
    on(:add, 3) do
      it MUST be 5
    end
  end
end

# 2. The test - a specific implementation
Fix[:Calculator].test { Calculator }

The contrast is striking. Fix’s specification is:

  • Concise: the expected behavior is expressed in just a few lines
  • Clear: the flow from constructor to method call is immediately apparent
  • Pure: it describes only what should happen, not how to set it up

This separation brings several major advantages:

1. Specification Reusability

Specifications become autonomous documents that can be reused across different implementations. The same calculator specification could be used to test different calculator classes as long as they follow the same interface.

2. Isolated Testing

Each test can be executed in complete isolation, which makes debugging easier and improves test code maintainability. The separation between specification and implementation means you can change how you test without changing what you’re testing.

A More Complex Example

Let’s see how this separation applies in a more elaborate case:

# A generic specification for a payment system
Fix :PaymentSystem do
  with amount: 100 do
    it MUST be_positive

    on :process do
      it MUST be_successful
      it MUST change(account, :balance).by(-100)
    end
  end

  with amount: -50 do
    on :process do
      it MUST raise_exception(InvalidAmountError)
    end
  end
end

# Specific tests for different implementations
Fix[:PaymentSystem].test { StripePayment.new(amount: 100) }
Fix[:PaymentSystem].test { PaypalPayment.new(amount: 100) }

In this example, the payment system specification is generic and can be applied to different payment implementations. By focusing on the interface rather than the implementation details, we create a reusable contract that any payment processor can fulfill.

Benefits in Practice

This architectural separation provides several practical benefits:

  1. Documentation: Specifications serve as clear, living documentation of your system’s expected behavior

  2. Maintainability: Changes to test implementation don’t require changes to specifications

  3. Flexibility: The same specifications can verify multiple implementations, making it ideal for:
    • Testing different versions of a class
    • Verifying third-party integrations
    • Ensuring consistency across microservices
  4. Clarity: By separating what from how, both specifications and tests become more focused and easier to understand

Conclusion

Fix invites us to rethink how we write our tests in Ruby. By clearly separating specifications from concrete tests, it allows us to:

  • Create clearer and reusable specifications
  • Maintain living documentation of our expectations
  • Test different implementations against the same specifications
  • Isolate problems more easily

This unique architectural approach makes Fix a particularly interesting tool for projects that require precise and reusable specifications while maintaining great flexibility in their implementation.

]]>
Cyril Kato
The Fundamental Distinction Between Expected and Actual Values in Testing2024-11-04T11:00:00+00:002024-11-04T11:00:00+00:00https://fixrb.dev/design/testing/2024/11/04/fundamental-distinction-between-expected-and-actual-values-in-testingThe distinction between expected and actual values in automated testing is more than just a convention - it’s a fundamental architectural principle that deserves our attention.

To emphasize the semantic difference between these two values, we must understand that the expected value is a known constant, an integral part of the specification. In contrast, the actual value, obtained by challenging a foreign object, must be evaluated against the expected value by the predicate. This is precisely the role of the latter: to ensure that the obtained value satisfies the criteria defined by the expected value.

This distinction is so fundamental that it must be honored in the very design of the predicate: it receives the expected value as a reference during initialization, then evaluates the actual value against this reference. This asymmetry in value handling reflects their profoundly different nature, a principle that modern testing libraries like Matchi have wisely chosen to implement.

Let’s illustrate this principle with a concrete example. Consider a simple equivalence test between two strings:

EXPECTED_VALUE = "foo"
actual_value = "bar"

At first glance, these two approaches seem equivalent:

EXPECTED_VALUE.eql?(actual_value) # => false
actual_value.eql?(EXPECTED_VALUE) # => false

However, this apparent symmetry can be deceptive. Consider a scenario where the actual value has been compromised:

def actual_value.eql?(*)
  true
end

actual_value.eql?(EXPECTED_VALUE) # => true

This example, though simplified, highlights a fundamental principle: the predicate must never trust the actual value. Delegating the responsibility of comparison to the latter would mean trusting it for its own evaluation. This is why the predicate must always maintain control of the comparison, using the expected value as a reference to evaluate the actual value, and not the reverse.

This rigorous approach to predicate design contributes not only to the robustness of tests but also to their clarity and long-term maintainability.

]]>
Cyril Kato
From RSpec to Fix: A Journey Towards Simpler Testing2015-09-06T11:00:00+00:002015-09-06T11:00:00+00:00https://fixrb.dev/framework/comparison/2015/09/06/from-rspec-to-fixThe Quest for Simplicity

One of the most striking differences between Fix and traditional testing frameworks like RSpec lies in their complexity. Let’s look at some numbers that tell an interesting story:

RSpec’s Components (version 3.3)

Total: 17,129 lines of code

Fix’s Components (version 0.7)

Total: 510 lines of code

The core philosophy is simple: a testing framework shouldn’t be more complex than the code it tests. This dramatic difference in code size (16,619 lines of code) reflects Fix’s commitment to minimalism and clarity.

A Real-World Comparison

Let’s look at a real-world example that demonstrates the key differences in approach. Consider this Monster class:

class Monster
  def self.get
    {
      boo: {
        name: "Boo",
        life: 123,
        mana: 42
      },
      hasu: {
        name: "Hasu",
        life: 88,
        mana: 40
      }
    }
  end

  def get(id)
    self.class.get.fetch(id)
  end
end

RSpec’s Layered Approach

require_relative "monster"
require "rspec/autorun"

RSpec.describe Monster do
  describe ".get" do
    subject(:monsters) { described_class.get }

    describe "#keys" do
      it { expect(monsters.keys).to eql %i(boo hasu) }
    end
  end

  describe ".new" do
    subject(:described_instance) { described_class.new }

    describe "#get" do
      subject(:monster) { described_instance.get(name) }

      context "with Boo monster" do
        let(:name) { :boo }
        it { expect(monster).to eql({ name: "Boo", life: 123, mana: 42 }) }
      end

      context "with Boom monster" do
        let(:name) { :boom }
        it { expect { monster }.to raise_exception KeyError }
      end
    end
  end
end

Fix’s Direct Style

require_relative "monster"
require "fix"

Fix.describe Monster do
  on :get do
    on :keys do
      it { MUST eql %i(boo hasu) }
    end
  end

  on :new do
    on :get, :boo do
      it { MUST eql({ name: "Boo", life: 123, mana: 42 }) }
    end

    on :get, :boom do
      it { MUST raise_exception KeyError }
    end
  end
end

Key Differentiators

  1. Method Chaining: Fix allows describing methods with one expression, whether for class or instance methods. This leads to more concise and readable code.

  2. Single Source of Truth: All specifications are derived from the described front object populated at the root. There’s no need for explicit or implicit subjects - there’s just one read-only dynamic subject deduced from the front object and described methods.

  3. Consistent Syntax: Fix maintains the same syntax regardless of what’s being tested. Whether you’re checking a value, expecting an error, or verifying a state change, the syntax remains uniform and predictable.

Clarity in Practice

Fix encourages a more direct and less ceremonial approach to testing. Compare how both frameworks handle error checking:

RSpec:

expect { problematic_call }.to raise_exception(ErrorType)

Fix:

it { MUST raise_exception ErrorType }

Or value comparison:

RSpec:

expect(value).to eq(expected)

Fix:

it { MUST eql expected }

This consistency helps reduce cognitive load and makes tests easier to write and understand.

Conclusion

Fix represents a fresh approach to Ruby testing that prioritizes simplicity and clarity. By reducing complexity and maintaining a consistent syntax, it helps developers focus on what matters: writing clear, maintainable tests that effectively verify their code’s behavior.

Want to try Fix for yourself? Get started with:

gem install fix

Visit our documentation to learn more about how Fix can improve your testing workflow.

]]>
Cyril Kato
A Fresh Take on Ruby Testing2015-09-03T11:00:00+00:002015-09-03T11:00:00+00:00https://fixrb.dev/framework/release/2015/09/03/a-fresh-take-on-ruby-testingToday marks an exciting milestone in our journey as we announce the release of Fix 0.7! After months of careful development and multiple iterations, we’re proud to present a testing framework that brings simplicity and clarity back to Ruby testing.

The Philosophy Behind Fix

Fix emerged from a simple observation: testing frameworks shouldn’t be more complex than the code they’re testing. Built on just 148 lines of code and powered by the Spectus expectation library, Fix takes a deliberately minimalist approach. We’ve intentionally omitted features like benchmarking and mocking to focus on what matters most: writing clear, maintainable specifications.

Key Features

  • Pure Specification Documents: Fix treats specs as living documents that remain logic-free and crystal clear
  • Version-Resistant: Specifications remain stable across Fix versions, protecting against software erosion
  • No Magic: We avoid monkey-patching and other Ruby “magic tricks” that can obscure behavior
  • Authentic Ruby Objects: Work with pure, unmuted Ruby objects for unambiguous and structured specs

Why Another Testing Framework?

After a decade of Ruby development, I found myself struggling to understand RSpec’s source code. This raised a concerning question: if a testing framework is more complex than the code it tests, how confident can we be in our tests?

Let’s look at a revealing example:

class App
  def equal?(*)
    true
  end
end

require "rspec"

RSpec.describe App do
  it "is the answer to life, the universe and everything" do
    expect(described_class.new).to equal(42)
  end
end

Running this with RSpec:

$ rspec wat_spec.rb
.

Finished in 0.00146 seconds (files took 0.17203 seconds to load)
1 example, 0 failures

Surprisingly, RSpec tells us that App.new equals 42! While this specific issue could be resolved by reversing actual and expected values, it highlights potential risks in complex testing frameworks.

Fix in Action

Let’s see how Fix handles a real-world test case:

# car_spec.rb
require "fix"

Fix :Car do
  on :new, color: "red" do
    it { MUST be_an_instance_of Car }

    on :color do
      it { MUST eql "red" }
    end

    on :start do
      it { MUST change(car, :running?).from(false).to(true) }
    end
  end
end

# Running the specification
Fix[:Car].test { Car }

Notice how Fix:

  • Keeps specifications clear and concise
  • Uses method chaining for natural readability
  • Focuses on behavior rather than implementation details
  • Maintains consistent syntax across different types of tests

Looking Forward

As we approach version 1.0.0, our focus remains on stability and refinement rather than adding new features. Fix is ready for production use, and we’re excited to see how the Ruby community puts it to work.

Key Areas of Focus

  1. Documentation Enhancement: Making it easier for newcomers to get started
  2. Performance Optimization: Ensuring Fix remains lightweight and fast
  3. Community Feedback: Incorporating real-world usage patterns
  4. Ecosystem Growth: Building tools and extensions around the core framework

Get Involved

Whether you love it or see room for improvement, we want to hear from you. Your feedback will help shape the future of Fix.

Happy testing!

]]>
Cyril Kato