Open-source project
testdouble/suture avatar
testdouble/suture

testdouble/suture: Ruby Characterization Refactoring

🏥 A Ruby gem that helps you refactor your legacy code

1,423 stars27 forksRubyMIT

At a glance

What is it?
Suture is a Ruby gem for recording legacy code behavior and comparing it with a replacement during refactoring. It suits teams that need to change poorly understood Ruby paths while keeping observed arguments, return values, and errors available as characterization data.
Who is it for?
Adopt Suture when a Ruby code path is difficult to characterize by reading alone and can be exercised with reproducible inputs. Do not choose it for isolated code that already has clear tests, or for paths whose external dependencies cannot be reproduced in the test environment.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 35 days ago.
What is it written in?
Mainly Ruby, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

Why Suture Exists for Legacy Ruby

Suture addresses a specific refactoring problem: changing Ruby code whose current behavior is not trusted or fully understood. The README presents the case as an important but risky operation, especially when the developer is considering replacing a code path instead of merely reorganizing it. Suture does not infer the correct implementation. It records what the existing implementation receives and produces, then uses those observations to help verify a replacement.

The intended user is a Ruby developer or team working with an existing application, rather than someone starting a service from a blank design. Its lifecycle is aimed at code that passes through local development, staging, and production. A queue system, web application, or command-line application can exercise the legacy path and provide realistic inputs. Those inputs become the basis for tests that check whether a new implementation behaves like the old one.

The project addresses uncertainty in behavior, not uncertainty in business requirements. It can show that a method returned a particular value for an argument, or that it raised an error. It does not establish whether that returned value was the desired business result. That distinction matters when reviewing recorded data and deciding which cases belong in the characterization suite.

Suture's Seam and Recording Flow

The core mechanism is an explicit seam placed around code that is about to change. The README defines a seam as an entry point that can be invoked in isolation, accepts arguments, returns a value, and minimizes side effects. In its example, work originally located inside `MyWorker#do_work` moves into `LegacyWorker#call`. The original method retains the mail delivery side effect, while the extracted method represents the repeatable data transformation between the input identifier and the value sent to `MyMailer.send`.

The call site then passes the legacy callable and its arguments to `Suture.create`. The `old` value may be an object with `call`, a method, or a `Proc/lambda`, and `args` contains the values passed to it. At this stage, Suture delegates to the legacy implementation without introducing a meaningful replacement. That makes the seam a controlled place to observe behavior before changing the implementation.

When `SUTURE_RECORD_CALLS` is set to a truthy value, calls at the seam record the arguments passed to the legacy path and its return value. The README says that recordings are saved in a SQLite database. It also says that errors raised by the legacy path can be part of the observed behavior. The database therefore acts as an execution record between the application and the later verification test.

Building Characterization Tests from Recorded Calls

Suture is intended to reduce the manual work of writing characterization tests for code whose behavior is already observed. The README's test invokes `Suture.verify` for the `:worker` seam, passes `LegacyWorker.new` as the subject, and sets `fail_fast` to true. The recorded argument is then used to call the supplied subject, and the recorded return value becomes the expected result.

The test should first run against the legacy code. That ordering provides two checks. First, it reveals whether the test environment contains the data and dependencies needed to reproduce the recorded result. Second, a coverage report can show which parts of `LegacyWorker` and subordinate code the recordings exercise. The README points to `simplecov` as one possible coverage tool, but it does not require a particular report format or command.

External services complicate this process. A recording made against a database, queue, or other external data source depends on the state available when the call occurred. The test may therefore need a database snapshot or another setup that resembles the recording environment. If that environment cannot be recreated, the recording can remain in SQLite while verification cannot reproduce the original behavior. Coverage can also expose side effects outside the intended refactoring boundary, which the team may need to isolate before proceeding.

Starting Suture with a Ruby Seam

The README does not document a gem installation command, a Bundler declaration, or a Suture configuration file. It identifies the project as a Ruby gem and points readers to the repository, but it does not give an exact installation sequence. This tutorial therefore omits an installation command rather than presenting one that the README does not confirm.

The first documented implementation step is to extract a callable unit from the existing method. The following example moves the legacy work into `LegacyWorker#call` while leaving `MyMailer.send` at the original call site:

ruby
class MyWorker
  def do_work(id)
    MyMailer.send(LegacyWorker.new.call(id))
  end
end

class LegacyWorker
  def call(id)
    thing = Thing.find(id)
    # … Still 99 lines. Still terrible …
    thing.result
  end
end

Next, place Suture around that call. The `old` value is the callable implementation, and `args` is the argument array:

ruby
class MyWorker
  def do_work(id)
    MyMailer.send(Suture.create(:worker, {
      old: LegacyWorker.new,
      args: [id]
    }))
  end
end

The README then documents setting `SUTURE_RECORD_CALLS` to a truthy value while exercising the application. It gives this example:

bash
SUTURE_RECORD_CALLS=true bundle exec rails s

That command starts the Rails server with recording enabled. The application must then be used normally so that calls reach the seam. The recorded arguments and return values are written to SQLite, and those calls can then support a verification test.

Production Recording and Dependency Boundaries

Suture can be used beyond local development, but recording in production does not remove the need for a deterministic test environment. The README explicitly suggests running the recording step in production when realistic usage is difficult to generate locally, then fetching the SQLite database after enough calls have been collected.

That approach transfers a data problem along with the recordings. A test based on production records may need access to the same stores used by the recording environment. The database may also contain inputs or outputs that are sensitive or impractical to transfer. The README does not document filtering, redaction, retention, access control, or deletion behavior for the SQLite file. Operators must inspect that data path before recording production traffic.

The seam should be chosen to limit dependency effects. The README suggests passing an object instead of a database `id` when the legacy path depends on external data. That can make recorded inputs more self-contained, although it changes the extracted code and may require additional loading or serialization work. Suture offers evidence of calls made through a seam, not a complete snapshot of the application's state.

Where Suture Is the Wrong Refactoring Tool

Suture is a poor fit when the code has no repeatable input and output boundary. A method that depends on ambient state, timing, network availability, or hidden global configuration cannot be characterized reliably merely by recording one call. The recorded expectation may be correct for one moment and impossible to reproduce later. In that case, separating state and side effects must come first. Suture can support that work after a clear seam exists, but it cannot remove those dependencies by itself.

Suture is also unsuitable as a substitute for a specification. If the current implementation returns an incorrect result for every input, `Suture.verify` can preserve that result as the expected behavior. The test would then confirm compatibility with the legacy path, not correctness. Teams still need domain knowledge when deciding which recorded cases are safe to preserve and which expose a bug that the replacement should fix.

The README does not document parallel recordings, merging multiple databases, comparing large call sets, distributed coordination, or automatic schema migrations for recorded data. Those features should not be assumed. Its documented path consists of identifying a seam, recording calls in SQLite, and verifying a supplied subject against those observations.

Suture Versus Handwritten Characterization Tests

The practical alternative is to write characterization tests directly by hand. A direct test can call an extracted method with chosen arguments and assert its return value or expected error. That approach gives the test author control over setup, fixtures, and examples without adding Suture as a runtime and test dependency.

Suture takes a different approach. It places a recording layer at the call site, captures arguments and results during actual application use, stores them in SQLite, and later replays those observations through `Suture.verify`. This is useful when realistic combinations of inputs are difficult to invent manually or when a developer does not yet understand the legacy path well enough to design representative cases. The cost is an additional dependency, a recording database, and a requirement that the replay environment resemble the environment where the calls occurred.

Direct tests remain easier to inspect when the behavior is small and stable. Suture becomes more relevant when observed cases exceed what the team can conveniently express by hand. The repository includes `test/`, `lib/`, `safe/`, `Rakefile`, and `suture.gemspec`, but the README does not state supported Ruby versions, a compatibility range, or the exact commands used to run its test suite.

Maintenance, Releases, and MIT Licensing

The repository is not archived, and its last push was on 2026-09-18. That indicates recent repository activity, but GitHub lists no releases for the project, so there is no published release history from which to infer an upgrade cadence. Upgrade cost should therefore be evaluated against the local Ruby and gem environment rather than assumed from a release schedule. The repository contains `CHANGELOG.md`, although the README does not summarize its entries.

Suture is identified as an MIT-licensed Ruby gem. That license identifier is relevant when reviewing distribution and reuse terms, but this article does not interpret the license text or provide legal advice. The operational upgrade concern is separate from licensing. Changing gem versions could affect recording storage, verification behavior, or integration with the host application, and the README does not specify a migration procedure.

The README shows a CI badge for the `main` branch, but it does not define a release policy or a support commitment. Before upgrading, a team can inspect the version in `suture.gemspec`, dependency constraints in `Gemfile.lock`, and entries in `CHANGELOG.md`. The repository layout confirms where to look, but it does not establish that upgrades are backward compatible.

Editorial conclusion

Adopt Suture when a Ruby code path is difficult to characterize by reading alone and can be exercised with reproducible inputs. Do not choose it for isolated code that already has clear tests, or for paths whose external dependencies cannot be reproduced in the test environment. Verify first that the seam is callable independently, that recorded data can be replayed safely, and that the SQLite database does not contain information that cannot move into the test environment.

Frequently asked questions

How do I install and use Suture in a Ruby project?

The README identifies Suture as a Ruby gem but does not provide an installation command or Bundler configuration. Its walkthrough starts by extracting a callable seam, wrapping it with `Suture.create`, and enabling recording with `SUTURE_RECORD_CALLS=true bundle exec rails s`.

What does Suture record from a legacy Ruby method?

With `SUTURE_RECORD_CALLS` set to a truthy value, Suture records the arguments sent to the legacy callable and its return value. The README also says that raised errors can represent observed behavior, and that recordings are stored in a SQLite database.

Can Suture record behavior in production?

The README says production recording may be useful when realistic calls are difficult to generate locally. It warns that replay may require access to the same external data stores and that transferring the SQLite database may not be feasible or appropriate in every environment.

Does Suture prove that a replacement is correct?

Suture verification is based on recorded behavior. `Suture.verify` compares the subject's result with the recorded expectation, so it checks compatibility with the legacy path rather than establishing whether that behavior meets the application's business requirements.

Official sources

  1. Issues
  2. License: MIT
  3. README
  4. testdouble/suture on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/testdouble-suture.svg)](https://hysenlabs.com/projects/testdouble-suture)

Was this useful?