Unit testing behave as the smallest testable parts from the application, typically a single function in procedural code, or a class method in object-oriented code. This is not a manual testing that relying on QA, which sometimes is slow response, might not that accurate due to human error, and has feedback loops.
In this modern AI-driven development, the bare minimum for developer is to took the responsibility to write and manage the unit tests from their application. Usually it runs during the development and used as a safety net against regressions before jump in to the staging environment
A Must Have Habit
The Arrange-Act-Assert (AAA) pattern is the industry standard for structuring highly readable and maintainable tests. It divides the test method into three visually separated phases using empty lines:
- Arrange, Set up the test dependencies (i.e. configure mock)
- Act, Execute the specific target function or method being validated
- Assert, Verify the actual output matches the expectation
test('should apply 10% discount to order total', () => {
// Arrange
const calculator = new DiscountCalculator();
const orderAmount = 100.00;
const expectedTotal = 90.00;
// Act
const actualTotal = calculator.applyDiscount(orderAmount, 0.10);
// Assert
expect(actualTotal).toBe(expectedTotal);
});
Scenario of Testing
To write thorough unit tests, these are the common things that requires a cover than just a ideal flow:
- Happy Path, Define a clean, baseline user journey using standard inputs to verify the core business logic executes perfectly
- Negative Path, Uncover the implicit assumptions behind the happy path and flip them one by one to ensure the app return the expected response
- Edge Cases, Apply the boundary value to test inputs directly (below or above the thresholds) so we could confident if the app properly handle the unexpected behavior
Design Choices
When start testing, we must choose on how our unit tests interacts with its dependencies, in general we have two approaches to eliminate it by using stubs or in-memory dependencies
| Pros | Cons | |
| Solitary | If the test fails, we know the bug immediately a.k.a fast execution | A fragile result, the tests can break during internal refactoring even if the current code behave as expected |
| Sociable | Highly resilient to code refactoring as long as the external API contract is aligned | When the used method from downstream change the behavior, it may break lot of our written tests. |
The optimal strategy is to prioritize on mimicking the real behavior which we’re using the in-memory dependencies like testcontainers, then only use the mocks/stubs approach for having the isolation such when communicating with external API.
Make it Scale
Scaling a test suite requires a deliberate strategy to keep execution speed fast and maintenance overhead low as the codebase grows. To prevent the test suite from slowing down development pipelines, teams must implement two key architectural strategies:
- Define Testing Shape
It’s depends on your app condition whether it’s a monolith or a microservices. For a monolith we could use Testing Pyrmaid while microservices or API-driven might use a Testing Trophy - Prioritize Behavioral Over Structural Testing
Start to write a test that asserting the public interface inputs and outputs rather than coupling tests to internal class structures.
Common Mistakes
Writing unit tests is only took half or even a quarter journey, maintaining them over time requires avoiding structural anti-patterns. When a test suite is poorly managed, it stops acting as a safety net and instead becomes one of the friction.
Common errors like sharing global states without cleanup, or writing slow tests that touch databases directly degrade the maintainability and value of the suite. However, the most critical mistakes is when a team fail to manage isolation boundaries, leading to improper mocking, or diagnostic failures that degrade pipeline trust.
Over-Mocking
Over-mocking occurs when developers replace most or every single dependency even from a lightweight dependency with a mock or stub. This tightly couples tests to implementation details rather than public behavior, creating brittle “change-detector” tests that break during routine internal refactoring
How can we avoid this? Mix the setup of test case to use the in-memory and mock approaches. Use the in-memory for a simple and deterministic dependency while use the mock to close the gap while integrating with external parties such external API or file system operations
Flakiness
A flaky test is one that yields inconsistent, nondeterministic outcomes (both passing and failing) under identical code and execution environments, without any changes to the underlying codebase.
How can we avoid this? eliminate the regular wait statements such setTimeout, and make sure to enforce the strictness of test isolation so we aren’t having the shared mutable object that used by multiple running test in the same time.
False Negatives
A false negative occurs when a test incorrectly passes a “false pass” despite a real defect or regression existing in the production code. This is the most dangerous failure mode because it gives a false sense of security while allowing critical software bugs.
How can we avoid this? Write a prominent assertions on every methods, keep in mind to apply the 3 scenarios above (happy, negative, and edge case), also if using mock ensure to match the output behavior in real-world use case.
To Sum It Up
These days, the standard developer must acknowledge the importance of having unit tests, and raise their standards to always be hands-on with unit testing, regardless of the dependencies or the test runner used
- Structure Visually with AAA: Standardize tests using separated Arrange, Act, and Assert blocks to make your test suite readable, maintainable, and self-documenting.
- Design Multiple Scenarios: Ensure comprehensive coverage by checking the happy path, flipping implicit business assumptions for negative flows, and probing threshold boundaries to prevent off-by-one errors.
- Choose Real Over Mocks: Promote refactor-friendly sociable tests using real, in-memory helpers, and restrict solitary mock stubs to volatile system edges like databases and external network calls.
