When I started automating tests, the painful part wasn’t the assertion. It was the trip to get there. I’d launch the application, log in, click through menus, enter data, and finally reach the behavior I needed to check. One wrong menu choice could send me back. An interruption could make me forget what I had entered. Small differences in the setup produced different results.

I began moving business logic out of the user interface so I could exercise it without walking through the whole application. That made tests easier to run. It also showed me how hard it is to test or change behavior that’s buried inside a screen or button.

Automating tests changed how I designed software.

Describe the behavior before the implementation

I shared these ideas in a talk about testing in Agile with a client team. Colleagues from Improving were part of the group. I try to describe the behavior we want before implementation begins, rather than treating testing as a phase that follows it.

Arrange-Act-Assert gave me a useful starting point for structuring tests: arrange the context, act on the system, and assert the result. I still think of it as training wheels. I eventually preferred Given-When-Then because it makes the behavior easier to discuss:

  • Given a particular context
  • When someone does something
  • Then we expect an observable result

That structure works at different levels. A user story might describe a person’s journey through a feature. A lower-level test might describe a business rule. They don’t have to be identical, and a story doesn’t have to map one-to-one to a single test. The important thing is that both explain behavior in language people can understand.

This changes the conversation from “Which button should the test click?” to “What is the person trying to accomplish?” A person wants to register for a class, withdraw money, or send a campaign to unique addresses. The buttons and APIs are implementation details. The intended outcome is what we need to agree on first.

Make the tests readable by people

I used to think test code mattered less than production code. I was wrong. Tests are documentation of how the system behaves today. If a test passes but nobody can understand its assumptions, it isn’t giving the team much confidence.

A test that buries the important behavior under a hundred lines of setup makes it hard to see what the system is supposed to do. Domain-specific helpers can hide the plumbing and let the test read more like a statement of the rule. Test names can do the same. I use names that are easy for a person to scan, even when that means bending the naming conventions I use in production code.

Readability matters because tests are part of the conversation between developers, QA, product owners, business analysts, and stakeholders. A clear specification lets people question the assumptions before those assumptions become code. If we can’t explain the expected behavior in plain language, we probably don’t understand the problem well enough to implement it.

That also changes how I think about user stories. A story that says, “As a system, I want to remove duplicate records,” describes an implementation task. It doesn’t explain who benefits or why. Starting with the person and the business outcome gives the team a better way to decide which behavior matters and how to test it.

Cover the risks that matter

Code coverage can tell us which lines ran during tests. It can’t tell us whether we tested the behavior that matters to the business.

I once saw a coverage policy require exactly 92 percent. Nobody could explain why that number was better than 90 or 95. The target looked precise, but it wasn’t connected to the value or risk of any feature.

I start by looking at the cost of failure. Some features protect revenue or prevent expensive mistakes for customers. Others are rarely used administrative screens with a reasonable workaround. That difference helps the team decide where additional automated coverage is worth the effort.

Code coverage is a tool, not a goal. I care more about whether we test the features and business rules whose failure would hurt people or the business.

Design the system so it can be tested

Testing every behavior through the whole system is slow and brittle. I use an airplane analogy: if we had to board a plane and take off to test its entertainment system, we wouldn’t test it often. Software has the same problem. We shouldn’t have to start every service and connect every external dependency just to check one business rule.

I separate the test responsibilities. A browser test can verify that a person completes a journey through the interface, while substituting responses at the API boundary. Back-end tests can cover business rules and API contracts. A smaller set of integration or smoke tests can check whether the real services connect as expected.

One project I worked on had a little over 10,000 automated tests across the back end, front-end components, and end-to-end journeys, plus a small number of live back-end smoke tests. That mix wasn’t a formula for other teams. It reflected where our business logic lived and which parts were costly to exercise through the browser.

I put each check at the narrowest layer that can verify it, then keep a smaller number of broader tests for the connections between components. The suite runs faster, failures are easier to diagnose, and tightly coupled code becomes a visible design problem.

QA and development share the work

When QA sees a feature only after development is done, the team has already lost time and context. I want developers and QA discussing the story together, identifying scenarios and edge cases, and deciding what to automate before the work is finished.

With three developers for every QA person, I wouldn’t ask everyone to pair on every task. I’d start with one developer and one feature for a sprint. Put a focused session on the board, agree on what you want to learn, then talk about what worked in the retrospective. The team can adjust and expand from there.

Pairing also uncovers repetitive work. A developer might learn that preparing test data takes QA two hours and write a script to reduce that setup. QA might point out an edge case missing from the story. The developer contributes code; QA contributes testing knowledge. The team shares responsibility for quality.

AI can help, but the behavior still comes first

AI has made it easier to generate unit tests, browser tests, and documentation. I expect an agent to follow the same red-green-refactor loop I use: write a failing test from the expected behavior, implement enough to pass, then refactor.

When an agent sees only the implementation, it may write tests that repeat the implementation’s assumptions. I give it the story and the Given-When-Then scenarios so it has a clearer description of the behavior we intend. The agent can then express that behavior in the test framework we’re using.

I’ve also used stories and tests to generate stakeholder-facing feature documentation. The team still has to review it, but readable tests give the agent material that describes the feature in terms people outside the codebase can follow.

Quality starts with a shared understanding

Automated testing taught me to pause before implementation and get clear about the problem, the people affected, and the behavior we expect. It has made my code easier to test and helped me work better with people who don’t write code.

I still care about the tools, but I start with the behavior. If we can describe it clearly together, we have a better chance of building the right thing.

Pasted image 20261010213203.png

Leave a Reply

Trending

Discover more from Claudio Lassala's Blog

Subscribe now to keep reading and get access to the full archive.

Continue reading