{"id":13394903,"url":"https://github.com/testdouble/suture","last_synced_at":"2025-05-14T20:10:26.280Z","repository":{"id":10512895,"uuid":"66006871","full_name":"testdouble/suture","owner":"testdouble","description":"🏥 A Ruby gem that helps you refactor your legacy code","archived":false,"fork":false,"pushed_at":"2023-09-29T00:37:51.000Z","size":385,"stargazers_count":1411,"open_issues_count":11,"forks_count":29,"subscribers_count":23,"default_branch":"main","last_synced_at":"2025-04-13T16:53:31.458Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/testdouble.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null}},"created_at":"2016-08-18T15:04:54.000Z","updated_at":"2025-03-31T19:49:24.000Z","dependencies_parsed_at":"2022-08-08T08:15:31.035Z","dependency_job_id":"be8c928a-08fe-47a8-8d0e-4e934df454d1","html_url":"https://github.com/testdouble/suture","commit_stats":{"total_commits":256,"total_committers":9,"mean_commits":"28.444444444444443","dds":0.0546875,"last_synced_commit":"66e80c73e6b6b03992d229ed76f3bd48800fbb42"},"previous_names":[],"tags_count":12,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/testdouble%2Fsuture","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/testdouble%2Fsuture/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/testdouble%2Fsuture/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/testdouble%2Fsuture/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/testdouble","download_url":"https://codeload.github.com/testdouble/suture/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":254219374,"owners_count":22034397,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-07-30T17:01:35.687Z","updated_at":"2025-05-14T20:10:26.260Z","avatar_url":"https://github.com/testdouble.png","language":"Ruby","readme":"# Suture 🏥\n\n[![Build Status](https://travis-ci.org/testdouble/suture.svg?branch=main)](https://travis-ci.org/testdouble/suture) [![Code Climate](https://codeclimate.com/github/testdouble/suture/badges/gpa.svg)](https://codeclimate.com/github/testdouble/suture) [![Test Coverage](https://codeclimate.com/github/testdouble/suture/badges/coverage.svg)](https://codeclimate.com/github/testdouble/suture/coverage)\n\nA refactoring tool for Ruby, designed to make it safe to change code you don't\nconfidently understand. In fact, changing untrustworthy code is so fraught,\nSuture hopes to make it safer to completely reimplement a code path.\n\nSuture provides help to the entire lifecycle of refactoring poorly-understood\ncode, from local development, to a staging environment, and even in production.\n\n# Video\n\nSuture was unveiled at Ruby Kaigi 2016 as one approach that can make\nrefactors less scary and more predictable. You can watch the 45 minute screencast\nhere:\n\n[\u003cimg width=\"803\" alt=\"screen shot 2016-09-20 at 9 44 43 am\" src=\"https://cloud.githubusercontent.com/assets/79303/18653743/e669d304-7f16-11e6-84e7-c86b8f88132c.png\" alt=\"A presentation on Suture\"\u003e](http://blog.testdouble.com/posts/2016-09-16-surgical-refactors-with-suture.html)\n\n# Walk-through guide\n\nRefactoring or reimplementing important code is an involved process! Instead of\nlisting out Suture's API without sufficient exposition, here is an example that\nwill take you through each stage of the lifecycle.\n\n## Development\n\nSuppose you have a really nasty worker method:\n\n``` ruby\nclass MyWorker\n  def do_work(id)\n    thing = Thing.find(id)\n    # … 99 lines of terribleness …\n    MyMailer.send(thing.result)\n  end\nend\n```\n\n### 1. Identify a seam\n\nA seam serves as an artificial entry point that sets a boundary around the code\nyou'd like to change. A good seam is:\n\n* easy to invoke in isolation\n* takes arguments, returns a value\n* eliminates (or at least minimizes) side effects (for more on side effects, see [this tutorial](https://semaphoreci.com/community/tutorials/isolate-side-effects-in-ruby))\n\nThen, to create a seam, typically we create a new unit to house the code that we\nexcise from its original site, and then we call it. This adds a level of\nindirection, which gives us the flexibility we'll need later.\n\nIn this case, to create a seam, we might start with this:\n\n``` ruby\nclass MyWorker\n  def do_work(id)\n    MyMailer.send(LegacyWorker.new.call(id))\n  end\nend\n\nclass LegacyWorker\n  def call(id)\n    thing = Thing.find(id)\n    # … Still 99 lines. Still terrible …\n    thing.result\n  end\nend\n```\n\nAs you can see, the call to `MyMailer.send` is left at the original call site.\n`MyMailer.send` is effectively a void method being invoked for its side effect,\nwhich would make it difficult to test. By creating `LegacyWorker#call`, we can\nnow express the work more clearly in terms of repeatable inputs (`id`) and\noutputs (`thing.result`), which will help us verify that our refactor is working\nlater.\n\nSince any changes to the code while it's untested are very dangerous, it's\nimportant to minimize changes made for the sake of creating a clear seam.\n\n### 2. Create our seam\n\nNext, we introduce Suture to the call site so we can start analyzing its\nbehavior:\n\n``` ruby\nclass MyWorker\n  def do_work(id)\n    MyMailer.send(Suture.create(:worker, {\n      old: LegacyWorker.new,\n      args: [id]\n    }))\n  end\nend\n```\n\nWhere `old` can be anything callable with `call` (like the class above, a\n  method, or a Proc/lambda) and `args` is an array of the args to pass to it.\n\nAt this point, running this code will result in Suture just delegating to\nLegacyWorker without taking any other meaningful action.\n\n### 3. Record the current behavior\n\nNext, we want to start observing how the legacy worker is actually called. What\narguments are being sent to it and what value does it returns (or, what error\ndoes it raise)? By recording the calls as we use our app locally, we can later\ntest that the old and new implementations behave the same way.\n\nFirst, we tell Suture to start recording calls by setting the environment\nvariable `SUTURE_RECORD_CALLS` to something truthy (e.g.\n`SUTURE_RECORD_CALLS=true bundle exec rails s`). So long as this variable is set,\nany calls to our seam will record the arguments passed to the legacy code path\nand the return value.\n\nAs you use the application (whether it's a queue system, a web app, or a CLI),\nthe calls will be saved to a sqlite database. Keep in mind that if the legacy code\npath relies on external data sources or services, your recorded inputs and\noutputs will rely on them as well. You may want to narrow the scope of your\nseam accordingly (e.g. to receive an object as an argument instead of a database\nid).\n\n#### Hard to exploratory test the code locally?\n\nIf it's difficult to generate realistic usage locally, then consider running\nthis step in production and fetching the sqlite DB after you've generated enough\ninputs and outputs to be confident you've covered most realistic uses. Keep in\nmind that this approach means your test environment will probably need access to\nthe same data stores as the environment that made the recording, which may not\nbe feasible or appropriate in many cases.\n\n### 4. Ensure current behavior with a test\n\nNext, we should probably write a test that will ensure our new implementation\nwill continue to behave like the old one. We can use these recordings to help us\nautomate some of the drudgery typically associated with writing\n[characterization tests](https://en.wikipedia.org/wiki/Characterization_test).\n\nWe could write a test like this:\n\n``` ruby\nclass MyWorkerCharacterizationTest \u003c Minitest::Test\n  def setup\n    super\n    # Load the test data needed to resemble the environment when recording\n  end\n\n  def test_that_it_still_works\n    Suture.verify(:worker, {\n      :subject =\u003e LegacyWorker.new\n      :fail_fast =\u003e true\n    })\n  end\nend\n```\n\n`Suture.verify` will fail if any of the recorded arguments don't return the\nexpected value. It's a good idea to run this against the legacy code first,\nfor two reasons:\n\n* running the characterization tests against the legacy code path will ensure\nthat the test environment has the data needed to behave the same way as when it was\nrecorded (it may be appropriate to take a snapshot of the database before you\nstart recording and load it before you run your tests)\n\n* by generating a code coverage report\n  ([simplecov](https://github.com/colszowka/simplecov) is a good one to start\n  with) from running this test in isolation, we can see what `LegacyWorker` is\n  actually calling, in an attempt to do two things:\n  * maximize coverage for code in the LegacyWorker (and for code that's\n  subordinate to it) to make sure our characterization test sufficiently\n  exercises it\n  * identify incidental coverage of code paths that are outside the scope of\n  what we hope to refactor. This will help to see if `LegacyWorker` has\n  side effects we didn't anticipate and should additionally write tests for\n\n### 5. Specify and test a path for new code\n\nOnce the automated characterization test of our recordings is passing, then we\ncan start work on a `NewWorker`. To get started, we update our Suture\nconfiguration:\n\n``` ruby\nclass MyWorker\n  def do_work(id)\n    MyMailer.send(Suture.create(:worker, {\n      old: LegacyWorker.new,\n      new: NewWorker.new,\n      args: [id]\n    }))\n  end\nend\n\nclass NewWorker\n  def call(id)\n  end\nend\n```\n\nNext, we specify a `NewWorker` under the `:new` key. For now,\nSuture will start sending all of its calls to `NewWorker#call`.\n\nNext, let's write a test to verify the new code path also passes the recorded\ninteractions:\n\n``` ruby\nclass MyWorkerCharacterizationTest \u003c Minitest::Test\n  def setup\n    super\n    # Load the test data needed to resemble the environment when recording\n  end\n\n  def test_that_it_still_works\n    Suture.verify(:worker, {\n      subject: LegacyWorker.new,\n      fail_fast: true\n    })\n  end\n\n  def test_new_thing_also_works\n    Suture.verify(:worker, {\n      subject: NewWorker.new,\n      fail_fast: false\n    })\n  end\nend\n```\n\nObviously, this should fail until `NewWorker`'s implementation covers all the\ncases that we recorded from `LegacyWorker`.\n\nRemember, characterization tests aren't designed to be kept around forever. Once\nyou're confident that the new implementation is sufficient, it's typically better\nto discard them and design focused, intention-revealing tests for the new\nimplementation and its component parts.\n\n### 6. Refactor or reimplement the legacy code.\n\nThis step is the hardest part and there's not much Suture can do to make it\nany easier. How you go about implementing your improvements depends on whether\nyou intend to rewrite the legacy code path or refactor it. Some comments on each\napproach follow:\n\n#### Reimplementing\n\nThe best time to rewrite a piece of software is when you have a better\nunderstanding of the real-world process that it models than the original authors did\nwhen they first wrote it. If that's the case, it's likely you'll think of more\nreliable names and abstractions than they did.\n\nAs for workflow, consider writing the new implementation like you would any other\nnew part of the system. The added benefit is being able to run the\ncharacterization tests as a progress indicator and a backstop for any missed edge\ncases. The ultimate goal of this workflow should be to incrementally arrive at a\nclean design that completely passes the characterization test run by running\n`Suture.verify`.\n\n#### Refactoring\n\nIf you choose to refactor the working implementation, though, you should start\nby copying it (and all of its subordinate types) into the new, separate code\npath. The goal should be to keep the legacy code path in a working state, so\nthat `Suture` can run it when needed until we're supremely confident that it can\nbe safely discarded. (It's also nice to be able to perform side-by-side\ncomparisons without having to check out a different git reference.)\n\nThe workflow when refactoring should be to take small, safe steps using well\nunderstood [refactoring patterns](https://www.amazon.com/Refactoring-Ruby-Addison-Wesley-Professional/dp/0321984137)\nand running the characterization test suite frequently to ensure nothing was\naccidentally broken.\n\nOnce the code is factored well enough to work with (i.e. it is clear enough to\nincorporate future anticipated changes), consider writing some clear and clean\nunit tests around new units that shook out from the activity. Having good tests\nfor well-factored code is the best guard against seeing it slip once again into\npoorly-understood \"legacy\" code.\n\n## Staging\n\nOnce you've changed the code, you still may not be confident enough to delete it\nentirely. It's possible (even likely) that your local exploratory testing didn't\nexercise every branch in the original code with the full range of potential\narguments and broader state.\n\nSuture gives users a way to experiment with risky refactors by deploying them to\na staging environment and running both the original and new code paths\nside-by-side, raising an error in the event they don't return the same value.\nThis is governed by the `:call_both` to `true`:\n\n``` ruby\nclass MyWorker\n  def do_work(id)\n    MyMailer.send(Suture.create(:worker, {\n      old: LegacyWorker.new,\n      new: NewWorker.new,\n      args: [id],\n      call_both: true\n    }))\n  end\nend\n```\n\nWith this setting, the seam will call through to **both** legacy and refactored\nimplementations, and will raise an error if they don't return the same value.\nObviously, this setting is only helpful if the paths don't trigger major or\ndestructive side effects.\n\n## Production\n\nYou're _almost_ ready to delete the old code path and switch production over to\nthe new one, but fear lingers: maybe there's an edge case your testing to this\npoint hasn't caught.\n\nSuture was written to minimize the inhibition to moving forward with changing\ncode, so it provides a couple features designed to be run in production when\nyou're yet unsure that your refactor or reimplementation is complete.\n\n### Logging errors\n\nWhile your application's logs aren't affected by Suture, it may be helpful for\nSuture to maintain a separate log file for any errors that are raised by the\nrefactored code path.\n\nSuture has a handful of process-wide logging settings that can be set at any\npoint as your app starts up (if you're using Rails, then your\nenvironment-specific (e.g. `config/environments/production.rb`) config file\nis a good choice).\n\n``` ruby\nSuture.config({\n  :log_level =\u003e \"WARN\", #\u003c-- defaults to \"INFO\"\n  :log_stdout =\u003e false, #\u003c-- defaults to true\n  :log_io =\u003e StringIO.new,      #\u003c-- defaults to nil\n  :log_file =\u003e \"log/suture.log\" #\u003c-- defaults to nil\n})\n```\n\nWhen your new code path raises an error with the above settings, it will\npropagate and log the error to the specified file.\n\n### Custom error handlers\n\nAdditionally, you may have some idea of what you want to do (i.e. phone home to\na reporting service) in the event that your new code path fails. To add custom\nerror handling before, set the `:on_error` option to a callable.\n\n``` ruby\nclass MyWorker\n  def do_work(id)\n    MyMailer.send(Suture.create(:worker, {\n      old: LegacyWorker.new,\n      new: NewWorker.new,\n      args: [id],\n      on_error: -\u003e (name, args) { PhonesHome.new.phone(name, args) }\n    }))\n  end\nend\n```\n\n### Retrying failures\n\nSince the legacy code path hasn't been deleted yet, there's no reason to leave\nusers hanging if the new code path explodes. By setting the `:fallback_on_error`\nentry to `true`, Suture will rescue any errors raised from the new code path and\nattempt to invoke the legacy code path instead.\n\n``` ruby\nclass MyWorker\n  def do_work(id)\n    MyMailer.send(Suture.create(:worker, {\n      old: LegacyWorker.new,\n      new: NewWorker.new,\n      args: [id],\n      fallback_on_error: true\n    }))\n  end\nend\n```\n\nSince this approach rescues errors, it's possible that errors in the new code\npath will go unnoticed, so it's best used in conjunction with Suture's logging\nfeature. Before ultimately deciding to finally delete the legacy code path,\ndouble-check that the logs aren't full of rescued errors!\n\n## Public API Summary\n\n* `Suture.create(name, opts)` - Creates a seam in your production source code\n* `Suture.verify(name, opts)` - Verifies a callable subject can recreate recorded calls\n* `Suture.config(config)` - Sets logging options, as well global defaults for other properties\n* `Suture.reset!` - Resets all Suture configuration\n* `Suture.delete!(id)` - Deletes a recorded call by `id`\n* `Suture.delete_all!(name)` - Deletes all recorded calls for a given seam `name`\n\n## Configuration\n\nLegacy code is, necessarily, complex and hard-to-wrangle. That's why Suture comes\nwith a bunch of configuration options to modify its behavior, particularly for\nhard-to-compare objects.\n\n### Setting configuration options\n\nIn general, most configuration options can be set in several places:\n\n* Globally, via an environment variable. The flag `record_calls` will translate\nto an expected ENV var named `SUTURE_RECORD_CALLS` and can be set from the\ncommand line like so: `SUTURE_RECORD_CALLS=true bundle exec rails server`, to\ntell Suture to record all your interactions with your seams without touching the\nsource code. (Note: this is really only appropriate if your codebase only has one\nSuture seam in progress at a time, since using a global env var configuration\nfor one seam's sake will erroneously impact the other.)\n\n* Globally, via the top-level `Suture.config` method. Most variables can be set\nvia this top-level configuration, like\n`Suture.config(:database_path =\u003e 'my.db')`. Once set, this will apply to all your\ninteractions with Suture for the life of the process until you call\n`Suture.reset!`.\n\n* At a `Suture.create` or `Suture.verify` call-site as part of its options hash.\nIf you have several seams, you'll probably want to set most options locally\nwhere you call Suture, like `Suture.create(:foo, { :comparator =\u003e my_thing })`\n\n### Supported options\n\n#### Suture.create\n\n`Suture.create(name, [options hash])`\n\n* _name_ (Required) - a unique name for the seam, by which any recordings will be\nidentified. This should match the name used for any calls to `Suture.verify` by\nyour automated tests\n\n* _old_ - (Required) - something that responds to `call` for the provided `args`\nof the seam and either is the legacy code path (e.g.\n`OldCode.new.method(:old_path)`) or invokes it (inside an anonymous Proc or\nlambda)\n\n* _args_ - (Required) - an array of arguments to be passed to the `old` or `new`\n\n* _new_ - like old, but either references or invokes the code path designed to\nreplace the `old` legacy code path. When set, Suture will default to invoking\nthe `new` path at the exclusion of the `old` path (unless a mode flag like\n`record_calls`, `call_both`, or `fallback_on_error` suggests differently)\n\n* _database_path_ - (Default: `\"db/suture.sqlite3\"`) - a path relative to the\ncurrent working directory to the Sqlite3 database Suture uses to record and\nplayback calls\n\n* _record_calls_ - (Default: `false`) - when set to true, the `old` path is called\n(regardless of whether `new` is set) and its arguments and result (be it a return\nvalue or an expected raised error) is recorded into the Suture database for the\npurpose of more coverage for calls to `Suture.verify`. [Read\nmore](#3-record-the-current-behavior)\n\n* _call_both_ - (Default: `false`) - when set to true, the `new` path is invoked,\nthen the `old` path is invoked, each with the seam's `args`. The return value\nfrom each is compared with the `comparator`, and if they are not equivalent, then\na `Suture::Error::ResultMismatch` is raised. Intended after the `new` path is\ninitially developed and to be run in pre-production environments. [Read\nmore](#staging)\n\n* _fallback_on_error_ - (Default: `false`) - designed to be run in production after\nthe initial development of the new code path, when set to true, Suture will\ninvoke the `new` code path. If `new` raises an error that isn't an\n`expected_error_type`, then Suture will invoke the `old` path with the same args\nin an attempt to recover a working state for the user. [Read more](#production)\n\n* _raise_on_result_mismatch_ - (Default: `true`) - when set to true, the\n`call_both` mode will merely log incidents of result mismatches, as opposed to\nraising `Suture::Error::ResultMismatch`\n\n* _return_old_on_result_mismatch_ - (Default: `false`) - when set to true, the\n`call_both` mode will return the result of the `old` code path instead of the\n`new` code path. This is useful when you want to log mismatches in production\n(i.e. when you're very confident it's safe and fast enough to use `call_both` in\nproduction), but want to fallback to the `old` path in the case of a mismatch\nto minimize disruption to your users\n\n* _comparator_ - (Default: `Suture::Comparator.new`) - determines how return\nvalues from the Suture are compared when invoking `Suture.verify` or when\n`call_both` mode is activated. By default, results will be considered equivalent\nif `==` returns true or if they `Marshal.dump` to the same string. If this\ndefault isn't appropriate for the return value of your seam, [read\non](#creating-a-custom-comparator)\n\n* _expected_error_types_ - (Default: `[]`) - if the seam is expected to raise\ncertain types of errors, don't consider them to be exceptional cases. For\nexample, if your `:widget` seam is known to raise `WidgetError` objects in\ncertain cases, setting `:expected_error_types =\u003e [WidgetError]` will result in:\n  * `Suture.create` will record expected errors when `record_calls` is enabled\n  * `Suture.verify` will compare recorded and actual raised errors that are\n    `kind_of?` any recorded error type (regardless of whether `Suture.verify` is\n    passed a redundant list of `expected_error_types`)\n  * `Suture.create`, when `fallback_on_error` is enabled, will allow expected\n    errors raised by the `new` path to propagate, as opposed to logging \u0026\n    rescuing them before calling the `old` path as a fallback\n\n* _disable_ - (Default: `false`) - when enabled, Suture will attempt to revert to\nthe original behavior of the `old` path and take no special action. Useful in\ncases where a bug is discovered in a deployed environment and you simply want\nto hit the brakes on any new code path experiments by setting\n`SUTURE_DISABLE=true` globally\n\n* _dup_args_ - (Default: `false`) - when enabled, Suture will call `dup` on each\nof the args passed to the `old` and/or `new` code paths. Useful when the code\npath(s) mutate the arguments in such a way as to prevent `call_both` or\n`fallback_on_error` from being effective\n\n* _after_new_ - a `call`-able hook that runs after `new` is invoked. If `new`\nraises an error, it is not invoked\n\n* _after_old_ - a `call`-able hook that runs after `old` is invoked. If `old`\nraises an error, it is not invoked\n\n* _on_new_error_ - a `call`-able hook that is invoked after `new` raises an\nunexpected error (see `expected_error_types`).\n\n* _on_old_error_ - a `call`-able hook that is invoked after `old` raises an\nunexpected error (see `expected_error_types`).\n\n#### Suture.verify\n\n`Suture.verify(name, [options hash])`\n\nMany of the settings for `Suture.verify` mirror the settings available to\n`Suture.create`. In general, the two methods' common options should be configured\nidentically for a given seam; this is necessary, because the `Suture.verify` call\nsite doesn't depend on (or know about) any `Suture.create` call site of the same\nname; the only resource they share is the recorded calls in Suture's database.\n\n* _name_ - (Required) - should be the same name as a seam for which some number\nof recorded calls exist\n\n* _subject_ - (Required) - a `call`-able that will be invoked with each recorded\nset of `args` and have its result compared to that of each recording. This is\nused in lieu of `old` or `new`, since the subject of a `Suture.verify` test might\nbe either (or neither!)\n\n* _database_path_ - (Default: `\"db/suture.sqlite3\"`) - as with `Suture.create`, a\ncustom database path can be set for almost any invocation of Suture, and\n`Suture.verify is no exception`\n\n* _verify_only_ - (Default: `nil`) - when set to an ID, Suture.verify` will only\nrun against recorded calls for the matching ID. This option is meant to be used\nto focus work on resolving a single verification failure\n\n* _fail_fast_ - (Default: `false`) - `Suture.verify` will, by default, run against\nevery single recording, aggregating and reporting on all errors (just like, say,\nRSpec or Minitest would). However, if the seam is slow to invoke or if you\nconfidently expect all of the recordings to pass verification, `fail_fast` is an\nappropriate option to set.\n\n* _call_limit_ - (Default: `nil`) - when set to a number, Suture will only verify\nup to the set number of recorded calls. Because Suture randomizes the order of\nverifications by default, you can see this as setting Suture.verify to sample a\nrandom smattering of `call_limit` recordings as a smell test. Potentially useful\nwhen a seam is very slow\n\n* _time_limit_ - (Default: `nil`) - when set to a number (in seconds), Suture will\nstop running verifications against recordings once `time_limit` seconds has\nelapsed. Useful when a seam is very slow to invoke\n\n* _error_message_limit_ - (Default: `nil`) - when set to a number, Suture will only\nprint up to `error_message_limit` failure messages. That way, if you currently\nhave hundreds of verifications failing, your console isn't overwhelmed by them on\neach run of `Suture.verify`\n\n* _random_seed_ - (Default: it's random!) - a randomized seed used to shuffle\nthe recordings before verifying them against the `subject` code path. If set to\n`nil`, the recordings will be invoked in insertion-order. If set to a specific\nnumber, that number will be used as the random seed (useful when re-running a\nparticular verification failure that can't be reproduced otherwise)\n\n* _comparator_ - (Default: `Suture::Comparator`) - If a custom comparator is used\nby the seam in `Suture.create`, then the same comparator should probably be\nused by `Suture.verify` to ensure the results are comparable. [Read\nmore](#creating-a-custom-comparator) on creating custom comparators\n)\n\n* _expected_error_types_ - (Default: `[]`) - this option has little impact on\n`Suture.verify` (since each recording will either verify a return value or an\nerror in its own right), however it can be set to squelch log messages warning\nthat errors were raised when invoking the `subject`\n\n* _after_subject_ - a `call`-able hook that runs after `subject` is invoked. If\n`subject` raises an error, it is not invoked\n\n* _on_new_subject_ - a `call`-able hook that is invoked after `subject` raises an\nunexpected error (see `expected_error_types`)\n\n### Creating a custom comparator\n\nOut-of-the-box, Suture will do its best to compare your recorded \u0026 actual results\nto ensure that things are equivalent to one another, but reality is often less\ntidy than a gem can predict up-front. When the built-in equivalency comparator\nfails you, you can define a custom one—globally or at each `Suture.create` or\n`Suture.verify` call-site.\n\n#### Extending the built-in comparator class\n\nIf you have a bunch of value types that require special equivalency checks, it\nmakes sense to invest the time to extend built-in one:\n\n``` ruby\nclass MyComparator \u003c Suture::Comparator\n  def call(recorded, actual)\n    if recorded.kind_of?(MyType)\n      recorded.data_stuff == actual.data_stuff\n    else\n      super\n    end\n  end\nend\n```\n\nSo long as you return `super` for non-special cases, it should be safe to set an\ninstance of your custom comparator globally for the life of the process with:\n\n``` ruby\nSuture.config({\n  :comparator =\u003e MyComparator.new\n})\n```\n\n#### Creating a one-off comparator\n\nIf a particular seam requires a custom comparator and will always return\nsufficiently homogeneous types, it may be good enough to set a custom comparator\ninline at the `Suture.create` or `Suture.verify` call-site, like so:\n\n``` ruby\nSuture.create(:my_type, {\n  :old =\u003e method(:old_method),\n  :args =\u003e [42],\n  :comparator =\u003e -\u003e(recorded, actual){ recorded.data_thing == actual.data_thing }\n})\n```\n\nJust be sure to set it the same way if you want `Suture.verify` to be able to\ntest your recorded values!\n\n``` ruby\nSuture.verify(:my_type, {\n  :subject =\u003e method(:old_method),\n  :comparator =\u003e -\u003e(recorded, actual){ recorded.data_thing == actual.data_thing }\n})\n```\n\n#### Comparing two ActiveRecord objects\n\nLet's face it, a massive proportion of legacy Ruby code in the wild involves\nActiveRecord objects to some extent, and it's important that Suture be equipped\nto compare them gracefully. If Suture's default comparator (`Suture::Comparator`)\ndetects two ActiveRecord model instances being compared, it will behave\ndifferently, by this logic:\n\n1. Instead of comparing the objects with `==` (which returns true so long as the\n`id` attribute matches), Suture will compare the objects' `attributes` hashes\ninstead\n2. The built-in `updated_at` and `created_at` will typically differ when code\nis executed at different times and are usually not meaningful to application\nlogic, Suture will ignore these attributes by default\n\nOther attributes may or may not matter (for instance, other timestamp fields,\nor the `id` of the object), in those cases, you can instantiate the comparator\nyourself and tell it which attributes to exclude, like so:\n\n``` ruby\nSuture.verify :thing,\n  :subject =\u003e Thing.new.method(:stuff),\n  :comparator =\u003e Suture::Comparator.new(\n    :active_record_excluded_attributes =\u003e [\n      :id,\n      :quality,\n      :created_at,\n      :updated_at\n    ]\n  )\n```\n\nIf `Thing#stuff` returns an instance of an ActiveRecord model, the four\nattributes listed above will be ignored when comparing with recorded results.\n\nIn all of the above cases, `:comparator` can be set on both `Suture.create` and\n`Suture.verify` and typically ought to be symmetrical for most seams.\n\n## Examples\n\nThis repository contains these examples available for your perusal:\n\n* [A Rails app of the Gilded Rose kata](example/rails_app)\n\n## Troubleshooting\n\nSome ideas if you can't get a particular verification to work or if you keep\nseeing false negatives:\n\n  * There may be a side effect in your code that you haven't found, extracted,\n    replicated, or controlled for. Consider contributing to [this\n    milestone](https://github.com/testdouble/suture/milestone/3), which specifies\n    a side-effect detector to be paired with Suture to make it easier to see\n    when observable database, network, and in-memory changes are made during a\n    Suture operation\n  * Consider writing a [custom comparator](#creating-a-custom-comparator) with\n    a relaxed conception of equivalence between the recorded and observed results\n  * If a recording was made in error, you can always delete it, either by\n    dropping Suture's database (which is, by default, stored in\n    `db/suture.sqlite3`) or by observing the ID of the recording from an error\n    message and invoking `Suture.delete!(42)`\n","funding_links":[],"categories":["Code Analysis and Metrics","Ruby","Programming Languages","Day 2"],"sub_categories":["Fearlessly Refactoring Legacy Ruby"],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftestdouble%2Fsuture","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftestdouble%2Fsuture","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftestdouble%2Fsuture/lists"}