This article isn’t about testing. It’s about adopting a workflow that keeps you in control while developing your feature. Tests are just the engine and the happy consequence of this process.
This workflow has completely transformed the way I code, and it never fails to put a smile on my face. My hope is that it does the same for you. By the end, you’ll have a fully developed feature that satisfies all business rules and a test suite that validates it in less than a second.
I used PHP for the demonstration, but this workflow is fully adaptable to any language.
The Workflow and Testing Frustrations
Where do I start my feature?
When it’s time to develop a new feature, it’s often hard to know where to begin. Should you start with the business logic, the controller, or the front end ?
It’s equally tricky to know when to stop. Without a clear process, the only way to gauge your progress is through manual testing. A tedious and error-prone approach.
Don’t be scare, it’s just a 2,000-line file, without any tests 😁
You know the one. That file where unexplainable things happen. You need to change a single line, but every attempt seems to break the entire project. Without a safety net of tests, refactoring feels like walking a tightrope over a canyon.
Tests are painfully slow
To avoid this chaos, you decide to test every feature with end-to-end tests. Great idea ! Until you realize you have enough time to drink five cups of coffee while waiting for the test suite to finish. Productivity ? Out the window.
Tests Break After Every Refactor
In an effort to create instant feedback, you decide to write fine-grained tests for all your classes. However, now every change you make leads to a cascade of broken tests, and you end up spending more time fixing them than working on the actual change. It’s frustrating, inefficient, and makes you dread every refactor.
Let's Dream ! What the perfect test ?
Instant feedback
Feedback is key at every stage of software development. While developing the core of the project, I need instant feedback to know immediately if any of my business rules are broken. During refactoring, having an assistant that informs me whether the code is broken or if I can proceed safely is an invaluable advantage.
Focus on the behaviors
What the project is able to do, the project behaviors, is the most important aspect. These behaviors should be user-centric. Even if the implementation (the code created to make a feature work) changes, the user's intention will remain the same.
For example, when a user places a product order, they want to be notified. There are many ways to notify a user: email, SMS, mail, etc. While the user's intention doesn't change, the implementation often will.
If a test represents the user's intention and the code no longer fulfills that intention, the test should fail. However, during refactoring, if the code still meets the user's intention, the test should not break.
The Copilot
Imagine being guided step by step through the creation of a new feature. By writing the test first, it becomes your guide to coding the correct feature. It might not be entirely clear yet, but I want my test to function as my GPS while I code. With this approach, I don’t need to think about the next step, I simply follow the GPS instructions. If this concept still feels unclear, don’t worry ! I’ll explain it in detail in the workflow section. 😉
Few Requirements before Starting
The Feature and the Acceptance Tests
As I mentioned in the introduction, this article isn’t about testing—it’s about creating a feature. And to do that, I need a feature. More specifically, I need a feature accompanied by acceptance tests defined with examples. For each feature, the team should establish acceptance criteria or rules, and for each rule, create one or more examples.
⚠️ These examples must be user/domain-centric, with no technical aspects described. A simple way to check if your examples are well-defined is to ask yourself: "Would my example work with any implementation (Web front-end, HTTP API, Terminal CLI, Real life, etc.)?"
For instance, if I want to develop the "Add product to basket" feature, my examples could look like this:
.
Behaviors focused tests
To focus on behaviors, I need to structure the tests the right way. First, I describe the state of the system before the user action. Next, I execute the user action, which changes the state of the system. Finally, I assert that the system's state matches my expectations.
By testing this way, the test doesn’t care how the user action is handled by the system; it only checks whether the action succeeds or not. This means that if we change the implementation of the user action, the test won’t break. However, if the implementation fails to update the system’s state as expected, the test will break—and that’s exactly what we want!
Fake the I/O
To achieve instant feedback, mocking certain parts of the system is needed. A domain-centric architecture makes this possible because the domain relies solely on interfaces to interact with external libraries. This makes it incredibly easy to fake those dependencies, allowing the functional tests to run super fast. Of course, the real implementations will also be tested (though not in this article) using integration tests.
The GPS Workflow
In this workflow, the test is my GPS ! I set a destination, let it guide me, and notifies me when I’ve arrived. The test will take shape based on the example the team provides.
Destination reached! 🎉 🍾 🥂
With this workflow, I’ve gamified the developer experience. After the challenge of tackling the red test, seeing it turn green gives you a shot of dopamine 💉🎊
Refactoring Time
To make the test pass, I intentionally went a bit fast. Now, I can take the time to refine the test and implement the code in a better way.
The test
The test translate the example but lost the user intention during the translation. With few functions, I can make it much closer to the original example.
Example :
To simplify testing while preserving the encapsulation of domain models, I introduced the
4 seconds... ⚡
Concepts
This workflow sits at the intersection of several concepts. Let me introduce some of them so you can explore them further if needed.
Example mapping
To describe a feature, the best approach is to provide as many examples as necessary. A team meeting can help you gain a deep understanding of the feature by identifying concrete examples of how it should work.
The Given-When-Then framework is a great tool for formalizing these examples.
To translate these examples into code, the Gherkin language (with Behat in PHP) can be helpful. However, I personally prefer to work directly in PHPUnit tests and name my functions using these keywords.
Go Further :
F.I.R.S.T
The "what do we want ?" part could be summarize with the acronym F.I.R.S.T :
- Fast
- Isolated
- Repeatable
- Self-validating
- Thorough
Now you have a checklist to know if your tests are well made.
Go further
- Robert C. Martin - Clean Code, Chapter 9 : Unit Tests, F.I.R.S.T
Ports and Adapters, Hexagonal, Onion, and Clean Architecture
All these architectures aim to isolate business logic from implementation details. Whether you use Ports and Adapters, Hexagonal, or Clean Architecture, the core idea is to make the business logic framework-agnostic and easy to test.
Once you have this in mind, there is a full spectrum of implementations, and the best one depends on your context and preferences. A major advantage of this architecture is that, by isolating the business logic, it enables much more efficient testing.
Go Further
Go Further
Final Thoughts
We’ve walked through how a domain-focused architecture combined with acceptance test driven development can transform your development experience. Instant feedback, robust tests, and a focus on user intentions make coding not just more efficient but more enjoyable.
Try this workflow, and you might find yourself smiling every time a test turns green ✅ ➡️ 😁
Let me know in the comments what would be your perfect workflow or what would you improve in this one !
↗ Original-Artikel auf dev.to lesenVollständiger Original-BerichtAusführliche Details, Code-Beispiele & Hersteller-Stellungnahme auf dev.to.
SOCIAL SHARE CARD GENERATOR