The Anatomy of a Unit Test

Search for a command to run...

No comments yet. Be the first to comment.
1. Sạch Phải công nhận là Sing sạch, đi đâu cũng thấy có người đang quét dọn, tỉa cành, gom rác, cắt cỏ,… Chi phí để duy trì môi trường cảnh quan chắc cũng không hề nhỏ. 2. Giao thông công cộng Bên này chủ yếu đi bằng tàu điện (MRT) và xe bus, chi ph...
In the previous article, I covered the basic concepts and introduced a 5-step process for applying DDD in practice. Today, I will bring you a bigger challenge. In this article, we will work through an

I. Why DDD matters? A Bigger Picture Over the years, as business needs have grown increasingly complex, our application systems have evolved - from monoliths to SOA, and now to microservices. This evolution demands a rational approach to component de...

This is a nice feedback from my Singaporean Scrum Master for 2024. According to Vietnamese beliefs, 2024 marked the final year of a challenging three-year period (Tam Tai) for those born in 1996, a time filled with uncertainties and difficulties. Al...
Behind every robust software system lies a suite of well-structured unit tests. But what defines a great unit test? In this article, we’ll examine its anatomy and best practices to ensure your tests are both reliable and effective.

Over the past five years, CI/CD has become a fundamental practice for modern software engineers.
According to Martin Fowler:
Continuous Integration is a software development practice where each member of a team merges their changes into a codebase together with their colleagues changes at least daily. Each of these integrations is verified by an automated build (including test) to detect integration errors as quickly as possible.
Yes, as quickly as possible. You could rely on manual testing, but it’s slow, prone to human error. To fully support Continuous Integration, automated testing is essential - something that runs and verifies your code every time you make a change.
Automated tests act as an early warning system, catching bugs before they reach production. With a solid suite of tests in place, you gain confidence that your changes won’t break existing functionality.
In automated testing, we commonly categorize tests into three main types:
Unit Tests – Focus on testing individual components in isolation.
Integration Tests – Verify interactions between multiple components or services.
End-to-End (E2E) Tests – Simulate real user flows to ensure the entire system works as expected.

The test pyramid provides a clear guideline on how to distribute tests across different layers. At the base, you should write a lot of small, fast unit tests. In the middle, you have integration tests, which are fewer but validate interactions between components. At the top, you write only a few high-level end-to-end (E2E) tests, which ensure the system works as a whole.
Lower levels focus on isolated code behaviors, while higher levels validate component integration and overall system functionality.
In this article, I’ll focus on the foundation of the pyramid - unit tests and explore what makes them effective.
A unit test is an automated test that:
Verifies a small piece of code (also known as a unit, often a method)
Runs quickly
Executes in isolation from dependencies or external systems as databases, APIs,…
A unit test follows a simple but effective pattern, often called the AAA pattern:
Arrange – prepare the dependencies and data of a scenario
Act – Call the testing method and capture the return value (if any)
Assert – Verify the outcome

And remember: never write a test without an assertion! Unless, of course, you just want to beautify your test report. 😆
Proper naming helps you understand what the test verifies. A good name encourages you to focus on the behavior of the code instead of the implementation details.

You can also use a very common convention is: MethodUnderTest_Scenario_ExpectedBehavior

A test double is a term that describes fake dependencies in tests. There are five variations of test doubles: dummy, stub, spy, mock, and fake.

Mock: Emulates and verifies outgoing interactions - checking whether a method was called, how many times, and with what parameters. These interactions are calls the SUT (System under test) makes to its dependencies to change their state (e.g., sending an email, calling external services).

Stub: Emulates incoming interactions by providing predefined responses/exceptions. These interactions involve the SUT calling its dependencies to retrieve input data (e.g., fetching data from a database or external system).

Spy: Partially mocks an object while keeping some real behavior intact.

Dummy: A simple, hardcoded value (e.g., null or a placeholder string) used only to satisfy method parameters but never actually utilized.

Fake: A lightweight implementation of a dependency, such as an in-memory database or HashMap, used as a replacement for a real system (e.g., replacing an actual database)

A test that shows good protection against regression depends on:
a. Amount of code executed during the test
Generally, the larger the amount of code that gets executed, the higher the chance that the test will reveal a bug.
b. Complexity and domain significance
Code that represents complex business logic is more important than boilerplate code. Therefore, our tests should focus on core domain components.
A good unit test provides fast feedback, allowing developers to catch issues early in the development cycle. When a test fails immediately after a bug is introduced, fixing it is quick and inexpensive.
The cost increases exponentially when the bug is not discovered early. A fast and reliable test suite helps maintain development speed while preventing costly bugs.
Keep your unit tests small and readable.
A single test should not take a lot to arrange and too many assertions.
Independent of out-of-process dependencies: databases, APIs, message queues,…
If a test breaks due to refactoring of underlying code without any change in behavior, it indicates poor test design.
Imagine you want to buy a scooter with:
✅ A blue handle
✅ Illuminating wheels
✅ A red bell


The constructor receives custom settings to configure the scooter, then configuration() method publishes the configuration for user to see.
Now, consider the following unit test:
It initializes the Scooter class with custom settings.
It asserts that the Scooter contains:
The first instance of the Handle class.
The second instance of the Wheel class.
The third instance of the Bell class.

The Problem
However, the test is looking at the implementation details of the class. It will fail if
We alter the order of part initialization to configure Wheel first, instead of Handle
We refactor the code to say that Bell is a sub-part of Handle
The Solution
Instead of asserting the exact instance positions, a refactoring-resistant test should verify:
✅ The Scooter and Bell has a handle with correct color.
✅ The Wheels have the expected illumination feature.

A better test focuses on observable behavior. Refactoring the underlying code like moving around the objects, or changing the composition won’t impact the test.

If you are working with Clean Architecture, the observable behavior should reside in Domain and Use case layers, where the core business logic is implemented.

If you are working with Controller-Service-Repository projects, the observable behavior should reside in the Service layers.
A unit test without assertions is not really a test - it’s more like “just writing code for coverage”. A unit test should always assert that the code behaves as expected. If your test doesn’t verify the result, you’re not testing anything at all.
One of the most commonly asked questions is how to test a private method. The short answer: you shouldn’t.
Testing private methods violates an important principle mentioned earlier: test only observable behavior. Exposing private methods couples tests to implementation details, making them fragile and reducing their resistance to refactoring.
However, talk is cheap. Sometimes the codebase is just too bad 😆 to strictly follow this principle. That’s why PowerMock exists - it allows developers to mock private methods and more.
A large arrangement section in a unit test can be a sign of problems. If your setup code is lengthy and repetitive, it makes tests harder to read and maintain.

When the arrangement code is large and repeated across multiple tests, consider refactoring:
Use helper methods to set up commonly used objects or scenarios with parameters.
Use factories to create reusable setups.
Consider mocking or using a framework like Mockito to simplify the setup.

If you need to invoke more than one method in your act statements:

Please encapsulate implementation details in your code, don't couple them with unit tests.

We should write one unit test per scenario, meaning each unit test should have only a single act statement.
Avoid writing tests with multiple Act statements, as shown below, as it makes the test unclear and harder to understand the scenario being tested.

Attribute test

Instance test

Such tests are insignificant to the system and lower resistance to refactoring.
Separate conditions into different unit tests instead of using conditional statements within a single test.

There is no value if we try asserting the stub data that we emulated in the Arrange phase.

Don’t let your tests hold domain knowledge. Tests should focus on verifying behavior.


Phewwww! We've just covered some basic concepts and practices. I hope they help you get started with unit testing and make your software more reliable.
This is an interesting topic and might spark some controversial discussions. If you have any comments, feel free to share!