skip to content

Codebase Structure

Organising the automation code itself: module dependency rules, inheritance versus composition for reuse, extension seams, and migrating a live harness. Interviewers read it as design skill.

on this pageshow

questions

16

A shared base class prepares every case in a suite — what does a case pay for setup it never uses?

level: juniorimportance: must knowfreq 62%

answer

  1. Nobody opted in
  2. The steps run whether needed or not
  3. Time cost multiplied by case count
  4. Unrelated preparation failure reddens the case
  5. One commit at a time, never removed

basics

~10 s

An unused inherited setup step costs runtime and reliability: it still runs on every execution, so the case is slower and can fail for reasons unrelated to what it checks.

solid answer

~50 s

Inherited preparation is unconditional: if the base class signs a user in, seeds account data and starts capture, every descendant pays for all three whether or not it touches them. The direct cost is wall-clock time multiplied by the case count. The larger cost is a widened failure surface — a case that only checks a validation message goes red when the seeding step breaks, and the report names the case, not the step. Diagnosis slows too, because the reason each step exists is not written anywhere the case can see; a reader has to walk the chain upward to learn why an account exists. Ancestors reach this state one commit at a time: each new suite needs one more thing, the shared parent is the cheapest place to put it, and nothing is ever removed.

code

pseudocode · 13 lines
pseudocode
class CaseBase:
    prepare():
        sign_in(default_user)        # added for the checkout suite
        seed_account(plan = "basic") # added for the billing suite
        set_toggle("new_nav", on)    # added for the navigation suite
        start_capture()              # added after a red week

class EmptyEmailIsRejected extends CaseBase:
    run():
        open_signup_form()
        submit()
        expect_message("email is required")
        # inherits four preparation steps, reads none of them

go deeper

for a junior

Be ready to say what your own case inherits and why. Know that preparation defined in a parent class runs whether your case uses it or not, which makes your case slower and gives it more ways to fail.

for a middle

Explain the two distinct costs — runtime multiplied by the case count, and a failure surface that reddens cases which never touched the broken step — and describe how the ancestor accumulated its steps one commit at a time.

for a senior

An interviewer expects you to spot the pattern in review before it hardens: chosen values and guards inside shared preparation, thin subclasses that exist only to skip a step, and preparation that outweighs the behaviour being checked.

for a principal

Own the rule that decides where new shared preparation lands, and argue it in cost terms: the blast radius of a preparation defect equals the size of the subtree beneath it, so implicit reuse prices every future change.

## What inheriting setup actually means Most harnesses put shared preparation in a **base class**: a class every case class extends, holding steps the runner executes before each case. A descendant acquires those steps simply by existing in the chain. It does not opt in, it does not list what it received, and it usually cannot see the steps without opening the ancestor and reading it. That single property — **acquisition without declaration** — is what makes the arrangement so cheap to add to and so expensive to live in. Adding a step to the ancestor is one commit and immediately serves every suite. Nothing in the design pushes back. ## How one ancestor ends up carrying everything The growth is never a decision; it is a sequence of individually reasonable ones: 1. The first suite needs a signed-in user. Sign-in moves into the base class, two suites share it, and the change is an obvious win. 2. A second suite needs seeded account data. Putting it in the shared ancestor is a one-line change; giving that suite its own parent is a refactor with a review attached. The one-liner wins. 3. A third suite needs a feature toggle flipped. Same argument, same outcome. 4. A red week prompts someone to add artefact capture — a screenshot, a log tail, a request trail — to the ancestor, because it is the one place guaranteed to run for everything. 5. The suite that needed step 2 is deleted, but the seeding step stays, because nobody can prove that no remaining case depends on it. Every commit is locally correct. The accumulated result is an ancestor whose steps no individual case can justify, and which nobody feels entitled to touch. ## The four bills a case pays **Runtime, multiplied.** Suppose sign-in costs 3 seconds, seeding 2 and toggling 1, and the suite holds 400 cases. That is 40 minutes of preparation per run, and for a case that only checks a validation message on an anonymous form, its whole share of that is waste. Parallel execution hides the wall clock but not the machine cost. **A widened failure surface.** Every inherited step is another way for an unrelated case to go red. When the seeding call starts returning an error, every case beneath the ancestor fails — including the ones that never read seeded data — and the report names the cases, not the step. The blast radius of a preparation defect equals the size of the subtree beneath it. **Slower diagnosis.** The reason each step exists is not written down anywhere the case can see. A reader who wants to know why an account exists before their case runs must walk the chain upward, and the commit that added the step is usually years old with a message that explains nothing. **Lost isolation.** A case that inherits four preparation steps cannot be lifted into another suite, cannot be run against a target where one of those steps is impossible, and cannot be read on its own. Its real preconditions are unstated, so nothing can check that they still hold. | Property | Setup inherited from an ancestor | Setup named by the case itself | |---|---|---| | How it is requested | implicitly, by extending | explicitly, by naming it | | Where you read it | walk the chain upward | at the top of the case | | Cost when unused | paid on every run | not paid at all | | Failure attribution | the case is blamed | the named step is blamed | | Adding one more | one line, affects everything below | one line, affects one case | ## Symptoms worth naming in review - The ancestor's preparation is longer than the average case body. - A step is guarded by a value descendants set, which is the hierarchy admitting the step is not universal. - Someone has added a thin subclass whose only purpose is to skip part of the ancestor. - Deleting a step is blocked because nobody can enumerate its dependants. - Average case runtime is dominated by preparation rather than by the behaviour under check. ## Keeping the next step out Prevention is much cheaper than cure, and it is a rule about **where new shared code lands** rather than a rewrite: - New shared preparation goes into a small named unit that a case calls by name; the ancestor does not grow. - Anything that is not true of *every* descendant does not belong above them. - A step that needs a value the case chooses is case-specific by definition — parameters are the tell. - Keep the ancestor to work that is genuinely invariant and unconditional, and prefer one level of depth so the effective behaviour is one hop away. Unpicking a step that live cases already rely on is a separate and larger piece of work with its own sequencing. The point here is to stop the pile growing while you still have the choice.

  • How would you measure what the inherited preparation actually costs this suite?
    Time the preparation separately from the case body and emit each step's duration as part of the run's own output. Multiply the per-case total by the case count, then split it by which steps the case actually reads. The gap between those two numbers is the waste, and it is usually large enough to end the argument without appealing to taste.
  • A step in the ancestor is guarded by a value that subclasses set. What is that telling you?
    That the step was never universal, and the hierarchy is being asked to express something it cannot. A guard is an opt-out bolted onto a mechanism whose whole premise is that descendants do not choose. The honest form is a named unit the case invokes when it wants the behaviour; the guard form hides the default and grows a second guard the next time.
  • Why is deleting an inherited step harder than adding one?
    Because addition is verified by the suite going green, while deletion has to prove a negative: that nothing below relies on it. Nothing records which descendants read the state a step produced, so the only evidence is a full run — and if a case fails afterwards, the cause looks like the deletion rather than the declaration that was always missing.

A shared ancestor is like a service charge added to every room in a hotel: each addition was justified by one guest, and everyone pays it forever.

saying these in an interview costs you the question

  • Says shared preparation is always good because it removes duplication
  • Claims unused inherited steps are free because they are fast
  • Blames the failing case rather than the preparation step that broke
  • Treats the base class as the natural home for any new shared step
  • Cannot say which inherited steps their own case actually depends on
open as a page

How do you deprecate a shared business-action signature that hundreds of live cases call, without a flag day?

level: middleimportance: must knowfreq 55%

basics

~20 s

Add the new signature beside the old one, make the old one delegate to it so behaviour cannot fork, move callers in reviewable batches, reject new uses once none are arriving, then delete the old form entirely.

open as a page

Which import direction between an automation suite's cases, business actions and protocol adapters keeps the layering intact?

level: middleimportance: must knowfreq 62%

basics

~20 s

Imports point one way: cases import business actions, actions import protocol adapters, and nothing imports back upward. An adapter that knows about a case, or a case that calls an adapter directly, has broken the layering.

open as a page

Why does a four-level base-class chain stop an automated case from declaring the setup it needs?

level: middleimportance: should knowfreq 54%

basics

~20 s

Depth converts a requirement into a location: what a case gets is decided by where it sits in the chain, not by anything it states. Composition returns the declaration — the case names the units it uses.

open as a page

What makes an extension point in an automated suite different from shared code everyone edits?

level: middleimportance: should knowfreq 46%

basics

~20 s

An extension point is a declared contract the framework itself calls: a named operation, a registration step, and a stated moment in the run. Shared code everyone edits has none of those, so every change reaches every case.

open as a page

Where may an assertion live in a suite layered into cases, shared business actions and protocol adapters?

level: middleimportance: should knowfreq 50%

basics

~20 s

Verdicts about product behaviour belong in the case, which names the claim being proved. A shared business action may only guard its own postcondition, reported as a broken arrangement, and an adapter judges nothing about behaviour.

open as a page

Adding a per-case setup step to a subclass breaks cases that passed before — how does inherited setup ordering explain it?

level: seniorimportance: should knowfreq 40%

basics

~20 s

Preparation spread across a class chain runs in an order no single file states; it comes from the runner's rule and from whether each level invokes its parent. A new step can land ahead of what it assumed.

open as a page

When a suite discovers its extensions by scanning instead of an explicit list, what breaks at debugging time?

level: seniorimportance: should knowfreq 34%

basics

~20 s

Scanning makes the active set invisible: no file names it, call order follows the scan, and a stray implementation on the search path runs everywhere while a missing one goes unnoticed. Publishing the resolved set restores traceability.

open as a page

A new harness runtime runs beside the old one while cases migrate — what must be true for both to stay green?

level: seniorimportance: should knowfreq 42%

basics

~20 s

Both runtimes must call one shared implementation of the business actions and report into a single result store. Only the entry point differs. Copy the behaviour instead of sharing it and the two halves drift apart silently.

open as a page

A protocol detail has leaked into a shared business action's signature in a layered automation suite — what does that cost when the adapter beneath it is replaced?

level: seniorimportance: should knowfreq 44%

basics

~20 s

A leaked detail in an action's signature — a raw response, an element identifier — puts the transport into every caller. Replacing the adapter then changes the signature and every case using it, so the boundary protected nothing.

open as a page

What evidence would convince you that rewriting an automation harness beats continuing to migrate it?

level: principalimportance: should knowfreq 34%

basics

~20 s

A measured burndown that will not finish before the reason for moving expires, a blocking constraint structural in the old design rather than incidental, and a plan that keeps the cases' behaviour. Dislike is not evidence.

open as a page

How do you cut the modules of an automation suite two teams change independently — one shared action library, or per-team modules over a common adapter layer?

level: principalimportance: should knowfreq 38%

basics

~20 s

It is a coupling-versus-duplication call. Share business actions only when both teams mean the same thing by a flow and need changes to land together. Otherwise give each team its own actions over a shared adapter layer.

open as a page

When is a mechanical rewrite across every automated case unsafe, compared with editing each case by hand?

level: middleimportance: nice to knowfreq 24%

basics

~20 s

Mechanical rewrites are safe when the change is purely syntactic and every call site means the same thing. They turn unsafe the moment a site needs judgement: a value to choose, or an intent the old code expressed badly.

open as a page

When is a shared base class still the right reuse mechanism in an automation harness, rather than a defect?

level: seniorimportance: nice to knowfreq 24%

basics

~20 s

Inheritance earns its place when the work is invariant across every descendant, takes no value the case chooses, and no case could ever want it off — failure capture and result reporting qualify. Once an opt-out is thinkable, compose instead.

open as a page

What compatibility promise does a suite's extension point make to the code that plugs into it?

level: seniorimportance: nice to knowfreq 24%

basics

~20 s

Publishing an extension point freezes what the framework passes in, what it accepts back, when it calls and how often, and what a thrown error does. Widening any of those forces every dependant to change at once.

open as a page

How do you recognise and break up a shared utility module that every part of an automation suite imports?

level: seniorimportance: nice to knowfreq 30%

basics

~20 s

Recognise it by its symptoms: a name that names no subject, imports from every layer, and nothing that can be extracted. Break it up by freezing it and moving out one cohesive group at a time.

open as a page