50% OFF - The Complete TDD course
Are you ready to master clean code, testing and Test-Driven Development (TDD)?
I recently launched a complete TDD course containing everything you need to craft high-quality software.
Now there is a 50% OFF for the course
Get instant access by clicking here.
Class is NOT a Unit
Many people believe that a "unit" in unit tests means a single function or class. That’s a dangerous myth.
When we define units based on code structure, we create fragile tests that break during refactoring. When our tests break during refactoring, they don’t have much value.
This mindset comes from the London School of TDD, which isolates classes with heavy mocking, often at the cost of test stability.
The Problems with Small Units
One of the biggest pitfalls in unit testing is thinking smaller is always better. But you end up with:
🚨 Excessive test doubles – Mocking everything leads to brittle tests
🚨 Tests per class – If your test structure mirrors your code, class changes can often break your tests, even when behavior stays the same
🚨 Encapsulation violations – Testing private methods ties tests to implementation details, again leading to fragile tests.
So What is a UNIT?
A unit isn’t a class or function—it’s a single, observable business behavior exposed through public APIs. This approach comes from the Chicago School of TDD, which prioritizes testing behavior over isolated components.
Here are 3 tips on how I write quality unit tests:
✅ Focus on how the system behaves from the outside, not how it is implemented internally.
✅ Target public entry points and write tests at the highest level possible
✅ Keep your tests fast by using mocks and fakes at the edge of your system
Public APIs
A public API is the set of methods, functions, or interfaces that other parts of the system or external users use to interact with the system.
For example, in a Web API, the HTTP methods serve as the public interface. I write my unit tests against this public API, while mocking only slow external dependencies.
By doing so, it doesn’t matter how I implement my Web API or whether I use CQRS, DDD patterns, or anything else—these are irrelevant to the tests, and that’s exactly the goal.
Frameworks Don’t Define Unit Tests
Frameworks and dependencies don’t define unit tests. There are many misconceptions like:
❌ A Cucumber test is not a unit test
❌ A JUnit or NUnit test is always a unit test
❌ A test uses external dependencies is not a unit test
They are all incorrect.
⚠️ If you're using JUnit but your tests are slow, they’re not unit tests.
⚠️ If you're using Cucumber and your tests are fast, they might be unit tests.
⚠️ If your app has a stable dependency where you can write fast tests for, they can still be unit tests.
The framework doesn’t matter—only the test’s speed, scope, and isolation do.
Conclusion
Unit testing is about verifying behavior, not code structure. Behavior-driven tests lead to a robust test suite that supports refactoring.
To learn more about writing high-level tests with the Chicago School of TDD, check out my recently launched complete TDD course, which includes:
The fundamentals of Test-Driven Development
Three real-world TDD examples in C#, TypeScript and Rust
The two schools of testing with the 5 types of mocks
Using TDD to design high-quality software
Testing legacy code
Refactoring best practices
Writing tests has a compounding effect. You slow down temporarily, but you'll be faster weeks or months in the future.
Great article, friend!
TDD is about short cycles. Normally, I code using TDD from the inside (core business rule) to the outside, so my last test implementation will be the highest test possible.
Do you mind sharing your thoughts on that as well?