BlogDocsProduct Log in Start free
Behind Hawzu

Shared Steps Need a Version

Reusing a step across 300 test cases is easy. The hard part is what happens to the runs already in flight when someone improves it — which is why a usage in Hawzu is a reference plus a version pin.

One shared step definition with several pinned versions preserved beside it, each held by a different run.

Every test management tool has shared steps. Write “log in as an admin” once, use it in three hundred test cases, fix it in one place. It’s the most obviously good idea in the category.

Then someone improves it. They add a step for the new consent dialog, because the old sequence has been failing since Tuesday. Three hundred cases now say something different than they did this morning — and forty runs are in flight, being worked through by people who read those steps twenty minutes ago.

Reuse is the easy half of shared steps. Versioning is the half that decides whether reuse is safe.

Reuse without a version is a promise you can’t keep

Think about what a shared step claims. It claims that everywhere it appears means the same thing. That’s what makes it worth having — one definition, one meaning, one place to fix.

The moment it’s editable, that claim only holds going forward. Everything already recorded against the old text is now described by the new text. A run finished last month says a tester followed six steps; today it renders eight. The tester didn’t skip two. They never existed.

A shared step that can be edited in place quietly rewrites every run that ever used it.

The tempting fix is to forbid editing, or to make every edit a new shared step. Both destroy the feature. If improving a shared step is painful, people stop improving them, and you end up with Login, Login v2, Login (new) and Login - USE THIS ONE.

The pin belongs to the run, not the case

So the answer isn’t to stop the three hundred cases from changing. A test case references a shared step live, and it should — reaching every case that uses it is the entire point of writing it once.

The pin sits one level down. When a case is pulled into a run, the run records not just which shared steps it uses but which version of each, and that exact version is copied into storage that keeps every version any run has ever needed, permanently, keyed by the shared step and the version number. Parameters work identically. Datasets are pinned by content.

Which splits the original problem cleanly: the three hundred cases genuinely do say something new this morning, because that was the improvement. The forty runs in flight say what they said twenty minutes ago.

The consequence is that improving a shared step can’t damage anything already recorded. It creates a new version. Runs in flight keep reading the version they pinned. The tester working through a case right now sees the same six steps they saw twenty minutes ago, because the run isn’t reading the live definition at all.

What they do see is a badge saying the source has moved on, and an explicit action to pull the change in — with a step-level diff first. Nothing updates underneath anyone.

Two collections, on purpose

The live definitions and the pinned versions are separate stores, and that separation gets questioned every time someone new reads the schema. It looks like duplication.

It isn’t, because the two have opposite requirements. The live definition needs to be editable, searchable, and singular — one row per shared step, the current truth. The pinned versions need to be immutable, complete, and plural — every version that any run has ever depended on, kept forever, uniquely keyed by shared step and version so the same version is stored exactly once no matter how many runs pinned it.

Trying to serve both from one collection means either the live row can’t change or the history isn’t kept. There’s no third option, which is why there are two collections.

Deleting one forces you to choose

The version pin makes editing safe. Deletion is the case it can’t cover — a version that no longer exists is gone from the map the drift check compares against, so nothing downstream can catch it.

Which is why deleting a shared step isn’t a confirmation dialog. It’s a question with two answers, and you have to pick one.

Both do two more things in the same write. They recompute which files each case references — inlining content brings its attachments along, and without that the inlined files become untracked orphans nobody can find or clean up. And they bump each case’s version, in the same operation that changes it, so the change is atomic with the record of it. Deleting a shared step used by 500 cases is one round trip, not 501.

The scope rule that looks like a bug

One last decision, which reliably reads as an oversight until you think about it.

A shared step can be workspace-level — defined once, used by test cases across several projects. When one is deleted, the lookup for affected cases deliberately ignores project scope.

That looks wrong. You’re in a project; surely you’re acting on that project. But rewriting only the current project’s cases would leave every other project’s cases pointing at something that no longer exists. The scope of the cleanup has to match the scope of the thing, not the scope of the person doing it.

That’s the same instinct behind the version pin itself. A shared step is shared — which means the interesting question is never what it does to the case in front of you. It’s what it does to the three hundred you can’t see, and the runs that already recorded it.

Which is the deciding factor in how much detail a test case should carry in the first place: reuse is only worth having if improving it is safe.

Reuse is a promise about meaning. A version is how you keep it.

Continue learning