Holdify
Hardcoded values suck! Holdify them.
Stop maintaining large expected values in your test/fixture files! Hold them automatically. Update them effortlessly.
Instead of this mess...
it 'generates the series_nav' do
assert_equal("<nav class=\"pagy series-nav\" aria-label=\"Pages\"><a role=\"link\" aria-disabled=\"true\"
aria-label=\"Previous\"><</a><a role=\"link\" aria-disabled=\"true\" aria-current=\"page\">1</a><a
href=\"/path?example=123&page=2\" rel=\"next\">2</a><a href=\"/path?example=123&page=3\">3</a><a href=\"/path?
example=123&page=4\">4</a><a href=\"/path?example=123&page=5\">5</a><a href=\"/path?example=123&page=6\">
6</a><a href=\"/path?example=123&page=7\">7</a><a href=\"/path?example=123&page=8\">8</a><a href=\"/path?
example=123&page=9\">9</a><a role=\"separator\" aria-disabled=\"true\">…</a><a href=\"/path?
example=123&page=50\">50</a><a href=\"/path?example=123&page=2\" rel=\"next\" aria-label=\"Next\">></a></nav>",
@pagy.series_nav)
end
it 'generates the data_hash' do
assert_equal({ url_template: "/path?example=123&page=P ", first_url: "/path?example=123",
current_url: "/path?example=123&page=1", page_url: "/path?example=123&page=1",
next_url: "/path?example=123&page=2", last_url: "/path?example=123&page=50", count: 1000, page: 1,
limit: 20, last: 50, in: 20, from: 1, to: 20, next: 2, options: { limit: 20, limit_key: "limit",
page_key: "page", page: 1, count: 1000 } }, @pagy.data_hash)
endHoldify your tests!
Write the same as:
it 'generates the series_nav' do
assert_hold @pagy.series_nav
end
it 'generates the data_hash' do
assert_hold @pagy.data_hash
endOr if you prefer a more expressive syntax:
it 'generates the series_nav' do
expect(@pagy.series_nav).to_hold
end
it 'generates the data_hash' do
expect(@pagy.data_hash).to_hold
endNote
Of course you can also use the _() or value() with must_hold.
For example: value(anything).must_hold
Why Holdify?
Most snapshot libraries bind stored values to the test name. If you rename a test, your snapshots break or become orphaned. Holdify is different.
- Bound to Code, Not Names: Holdify binds values to the line number of the assertion. You can rename your test methods or describe blocks freely without breaking your snapshots.
- One-to-One Feedback: The YAML store mirrors your file structure. An assertion at line 10 in your code corresponds exactly to L10 in the store file. No guessing which snapshot belongs to which test.
- Surgical Precision:
- Need to update just one value? Add a ! (e.g., assert_hold!).
- Need to see what's being generated without changing anything? Add a ? (e.g., assert_hold?).
- Resilient: Even if you move code around, Holdify's smart indexing (based on line content hashing) keeps track of your values, automatically updating line numbers in the store on the next run.
Installation
Add minitest-holdify to your Gemfile (usually in the :test group).
Minitest < 6.0 loads it automatically. For Minitest >= 6.0, add Minitest.load :holdify to your test_helper.rb.
Options
Holdify supports the following command-line options:
-
--holdify-reconcile: Updates all stored values to match the current output. Use with caution! (Implies--holdify-quiet). -
--holdify-pretty: Forces "GitHub-style" pretty diffs using Git. This is enabled automatically if Git is installed and running in a terminal. -
--holdify-quiet: Suppresses warnings when new values are stored.
How it works
Holdify binds the stored value to the exact line number of your assertion.
-
Capture: The first time a test runs, Holdify captures the returned value and stores it in a
*.yamlfile next to the test. -
Bind: The value is indexed by the test line number (e.g.,
L10) triggering the hold, correlating your code and the stored data. - Assert: On subsequent runs, Holdify checks that the fresh value matches the one "held" at that line.
Tip
Mental Model: Imagine the expected value is written directly in your test file at line X. Holdify simply moves that text into a separate file (indexed by LX) to keep your code clean, but the assertion remains strictly bound to that specific line. You can even inspect the YAML file for one-to-one feedback.
Easy reconciliation
When your code changes intentionally, you need to reconcile the held values with the new values. You have three options:
-
Reconcile: Run tests with the
--holdify-reconcileoption. Holdify will update any value that changed in the run tests.rake test TESTOPTS=--holdify-reconcile -
Delete: Delete the specific
*.yamlfile(s) and re-run the test(s). Ideally suited for when you want to reset specific test files (and deleting is easier than using the ENV variable). -
Selective update: Temporarily append
!to the method statements to reconcile and re-run the test. This forces Holdify to update the value.
-
assert_hold→assert_hold! -
must_hold→must_hold! -
to_hold→to_hold!
Warning
This stores the new value immediately but raises an error intentionally. This ensures you don't accidentally commit the ! method. Revert to the standard method to pass the test.
Inspecting Values
To quickly inspect the actual value from your code without changing anything, append ? to the statement and re-run the test. If the test passes (i.e, the assertion is true), it prints the value to stderr. (If it fails, minitest takes over printing the actual and expected values, so you already have the feedback.)
-
assert_hold→assert_hold? -
must_hold→must_hold? -
to_hold→to_hold?
Note
While ? conventionally denotes a boolean predicate, assertions themselves are kind of predicates with a side effect. Holdify uses it here as a query term ("Hold what value?"). It is designed as a temporary development tool for quick feedback.
Assertions and Expectations
By default, Holdify wraps assert_equal (or assert_nil, depending on the value). You can also specify a different equality assertion (e.g., assert_equal_unordered).
# Standard usage
assert_hold actual
_(actual).must_hold
# With custom assertion logic
assert_hold actual, :assert_equal_unordered
_(actual).must_hold :assert_equal_unordered
expect(actual).to_hold :assert_equal_unordered
# With custom failure message
assert_hold actual, 'Data consistency failed'
_(actual).must_hold 'Data consistency failed'Note
The custom assertion must be a symbol (e.g., :assert_something) not an expectation (e.g., :must_something). The order of arguments (assertion symbol vs message) is flexible.
Store Format
Holdify stores values in a standard YAML file named after your test file (e.g., test_file.rb.yaml). The keys are designed to be human-readable, allowing you to easily correlate stored values with your source code:
-
Line Matching: Keys start with
L<num>corresponding to the line number of the assertion in your test file (e.g.,L10). - Multiple Hits: If a single assertion line is executed multiple times (e.g., inside a loop), Holdify stores them as a list under the same key.
Example:
---
# Simple assertion at line 10
L10 df67...:
- "simple value"
# Loop executing line 15 twice
L15 4b0a...:
- "iteration 1"
- "iteration 2"Labeling Values
You can add context to your stored values by wrapping them in a Hash. This makes the YAML store self-documenting.
it 'checks permissions' do
# Pass a hash with a meaningful key
expect(permissions: user.permissions).to_hold
endThe store file will look like this:
L10 ac3b...:
- :permissions:
- :read
- :writeCaveats
-
Equality: Stored values are verified using standard Minitest assertions, so ensure your custom objects implement
==correctly. -
YAML serialization: Complex objects with
ivarsmight serialize differently across Ruby versions due toPsych(YAML) changes. It is safer to store raw data (hashes/attributes) to avoid this.
Repository Info
Versioning
Follows Semantic Versioning 2.0.0. See the Changelog.
Contributions
Pull Requests are welcome! Ensure tests, Codecov, and Rubocop pass.
Branches
-
master: Latest published release. Never force-pushed. -
dev: Development branch. May be force-pushed.