Welcome to the deep dive. Today we're peeling back the
layers on a concept that's really foundational if you're
serious about building solid, high quality software.
We're talking about testability. This deep dive.
Well, it's designed to help you understand how to engineer your
code right from the start. So it's not just functional, but
also really easy to verify. And that's, you know, crucial
for a resilient software. We've been digging into software
engineering a modern approach by Marco Tulio Valente.
Our mission today, it's really to pull out but the key insights
on why designing for testability is often, well, maybe even more
critical than just writing the tests after the fact.
We've got some practical examples from the source that I
think really illustrate this. It might change how you think
about development a bit. Ultimately, it's about building
software that doesn't just work, but is also robust, dependable,
and easy to maintain down the road.
OK, so let's unpack this. We talk a lot about writing
tests, sure, but what does testability actually mean?
The source gives us a nice simple definition.
It basically describes how easy it is to test a program.
Simple as that. Now, we've talked before about
good test, right? Following those first
principles, fast, independent, repeatable, self validating,
timely, and aiming for good coverage.
We even mentioned how branch coverage is stricter, more
thorough than just statement coverage, making sure you hit
all the decision points. Right.
But what's really key here, and what the source really hammers
home, is that testability isn't just about writing clever tests
or chasing high coverage numbers.
The fundamental idea is that a big chunk of your effort in
getting effective testing should actually go into the design of
the production code itself, the system under test.
It's a shift in thinking, you know, from just validating after
you build to proactively building for validation from the
get go. It's like an inherent quality of
the software. And here's the good news.
Really. The source points out that code
that already follows good SOLID design principles, things we
discuss all the time, naturally tends to be easier to test.
We're talking about things like high cohesion.
Yeah, keeping related stuff together.
Low coupling so components aren't tangled up.
Single responsibility. Each part doing one job well.
Separating presentation from the core logic.
Dependency inversion, even the law of Demeter.
Exactly. All those things that make code
cleaner and more maintainable they.
Also make it more testable. It all kind of clicks together.
It really does. And if you connect this to the
bigger picture, you see that good design isn't just like an
academic exercise or about making code look pretty.
It has real, practical benefits. Measurable benefits.
It makes your software verifiable.
When code is well designed, decoupled, cohesive, you can
isolate pieces and test them reliably.
It moves you away from just reacting to bugs and towards
proactively building resilient systems.
It makes maintenance easier, debugging faster and builds
confidence confidence in every change.
That makes a lot of sense. You're building in the pathways
for checking things right from the start.
OK, let's make this concrete. Let's look at the examples from
the source. The first one tackles a really
common situation, building a web service.
Imagine a simple Java servlet. Its job is to calculate body
mass index BMI, takes weight, takes height, applies the
formula weight, height, height. Sounds super straightforward.
It does sound simple on the surface, but the source shows
how the initial way you might code this often creates a big
testing problem. The original BMI servlet code as
described has direct dependencies on Java's servlet
package types, specifically Http://servlet request and
Http://servlet response. These are the core Java objects
for handling web requests and responses, and that direct
dependency It creates what's called a dependency chain.
If you want to test just the doggett method, the core logic,
you can't just create the servlet object easily.
You'd need to mock up fake request and response objects,
and maybe their dependencies too.
It gets complicated fast. Right, like needing the whole
engine just to test a single gear.
Exactly. That tight coupling makes the
testability of that initial design really low.
Isolated unit testing becomes almost impractical without a ton
of setup. OK, that definitely sounds like
a testing nightmare. So how does the source suggest
we fix this? How do you untangle that?
The solution proposed is actually quite, and it hinges on
a principle we just talked about.
Extract the core domain logic. The problem is that really the
BMI calculation itself. It's that the calculation is
tangled up with all the web server stuff.
So the fix involves creating a separate class that's called BMI
model. This new class does only one
thing, the BMI calculation. It takes weight and height as
simple numbers, returns the BMI. Crucially, it has 0 dependencies
on any servlet classes, none at all.
This makes the BM model incredibly easy to test in
isolation. You can just create it, feed it
numbers, check the output. No web server, no mocks need it.
Now the source is careful to point out, and this is
important, this refactoring doesn't test everything.
It won't test how the servlet interacts with the actual web
request, for example. But, and this is the key
insight, you are thoroughly testing the most critical part,
the core business logic, the actual calculation.
And verifying that core logic is vastly better than leaving it
untested or relying only on slow, complex integration tests.
You focus your testing up for where it counts.
Most that's a great take away for you listening.
Isolate that core business logic from its environment and boom,
testability goes way up. You get precision.
And interestingly, the same idea, extraction separation,
applies even when you're dealing with a completely different kind
of challenge, like asynchronous code.
Let's shift gears to the second example from the source.
This one looks at asynchronous functions, which are notoriously
tricky to test. Well, the example is an async Pi
function. As you might guess, it
calculates π but it does it in the background, maybe on another
thread, so the result isn't immediate.
Right, and this immediately raises a big question.
Why is async code so hard to test reliably?
The source highlights the main problem.
The result comes from another thread, so when it arrives is
unpredictable, non deterministic.
A common but really problematic way people try to test this is
by putting pauses in their tests using something like thread dot
sleep to just wait and hope the result is ready.
Oh. Yeah, the dreaded thread dot
sleep in tests. That usually leads to flaky
tests, right? Sometimes they pass, sometimes
they fail even if the code didn't change.
Exactly. It leads to non deterministic or
flaky tests. You can't guarantee the
background work will finish in that that fixed pause.
Maybe the system is busy, maybe it takes longer this time.
This inconsistency just destroys confidence in your test suite.
You don't know if a failure is a real bug or just bad timing.
So how do you test something reliably when its timing is
inherently unpredictable? It sounds like a recipe for
frustration. So what's the solution here?
I'm guessing it involves extraction again.
You guessed it, it's the same powerful pattern.
The source proposes extracting the core π calculation logic
into a separate synchronous function.
Let's call it sync Pi. The sync Pi function does the
exact same math to calculate π but it does it immediately right
there, blocking until it's done. It's completely separate from
any threading or asynchronous execution.
This extraction means you can reliably unit test the
calculation itself. You call sync Pi, give it
inputs, get an immediate, predictable result back, and
assert against it. Simple, reliable, fast.
It just reinforces that pattern we saw with the servlet example.
Extracting A testicle function, one that's pure synchronous,
with clear inputs and outputs, free from tricky dependencies or
timing issues, is always better than leaving complex code
untested or using flaky test methods.
It lets you verify the core critical logic with confidence.
That's such a clear through line.
Whether it's a web service handling input or a background
task doing calculations, isolating that core logic is
key. It makes your code predictable
for testing. It absolutely is.
The central message coming out of these examples and from the
source material generally is crystal clear.
Testability isn't some extra thing you tack on at the end.
It's fundamentally about the design of your software.
By sticking to principles like separation of concerns, keeping
different jobs apart, and by actively extracting independent,
testable units, well, you make your code inherently easier to
verify. It's not just about making it
easier to find bugs later. It's really about building
confidence into your software right from the start, making it
robust, maintainable and easier to change safely over time.
It transforms testing from a chore into just a natural
outcome of good design. Precisely.
Well, thank you for joining us on this deep dive into software
testability. And that's our deep dive for
today. Thanks for tuning in and
exploring with us how designing for testability can really make
a difference to the quality and longevity of your software.