I wrote code without tests that ran in production without defects, and I wrote buggy code with TDD (Test Driven Development). Time to look back at 35 years of coding and when tests help, and when there is something better. And especially, what these better things are.
In this part, we look at how to make manual testing easier and why it matters.
Make it easy to test manually
Our stack
Our production system runs on the Azure cloud.
Web-Apps respond to requests from clients and provide an HTTP API to other systems. Azure Functions execute asynchronous tasks. There is also a dedicated Identity Provider Service running.
Data is stored in SQL Server, blob storage, or table storage. A Redis cache is also used.
Communication between different processes is either HTTP or goes over a Service Bus.
Finally, we use Telemetry to see what’s going on in our system.

Using all of these Azure services is great for us because they provide out-of-the-box, dynamic, load-based scaling and are easy enough for a small team like ours to run.
Why simple local testing is important
The downside is that spinning up test systems on the same stack is rather tricky, especially for a quick exploratory testing session or for running a backend while working on frontend code.
That’s why we decided to be able to run almost the whole system locally. Ideally, in a way that makes debugging easy. The easiest way to debug code is when it all runs inside a single process.
Now we can quickly spin up the system on a local machine, do exploratory testing, or hunt down a bug. This ability makes our daily developer lives much simpler. The most important effect, however, is that we do not have to test everything with automated tests. Manual testing is so simple that it is good enough for many scenarios. This saves a lot of time that would otherwise be spent on automated tests. Keep in mind that we write automated tests for regression purposes. But that is only relevant if the code will change, to ensure we don’t break it. With our extreme slicing approach, most code doesn’t change for a very long time after release. And if a slice changes, chances are high that the automated test has to be changed as well – and, therefore, it isn’t a true regression test anymore.
Of course, we can’t test the infrastructure, like the communication over the bus, this way, but most testing is for business-logic code, not infrastructure, in our context. Infrastructure testing occurs in a dedicated test environment or in the production system. We use feature toggles on test tenants to enable testing new infrastructure components without risk for real tenants.
Making it run locally in a single process
Of course, we can’t use a real service bus or Azure functions. So we decided to fake all the storages and the service bus.
We created a console application that hosts the backend as a Web App. Instead of using a real service bus sender, we inject a fake that short-circuits the sender to the handler, keeping it in the same process. We do the same for HTTP calls to Azure functions. The calls are not sent over HTTP, but the fake calls the handler code directly.
For table storage, we use the Azurite emulator. And we can either run with a real local SQL Database or a hand-written simulator (we just keep lists of data in-memory).
Next time
In the next and last post, I’ll talk a bit about LLMs and their impact on our testing strategy and conclude this series.
[…] To test, or not to Test? Part 4 – Make it easy to test locally (Urs Enzler) […]