The Bare Minimum From Writing Unit Test

The Bare Minimum From Writing Unit Test

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

  • Solitary Unit Tests, Isolate the dependencies by replacing all with mocks/stubs
    • Pros, If the test fails, we know the bug immediately a.k.a fast execution.
    • Cons, A fragile result, the tests can break during internal refactoring even if the current code behave as expected (usually we forgot to change the returned mock response 😆).
  • Sociable Unit Tests. Allow the unit test to interact with real behavior.
    • Pros, Highly resilient to code refactoring as long as the external API contract is respected.
    • Cons, When the used method from downstream change the behavior, it may break lot of our written tests.

The optimal strategy is to prioritize on real behavior which we may use the in-memory dependencies such 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:

  1. 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
  2. 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

What should we do? 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.

What we should do? eliminate the static wait statements like hardcoded sleeps (await wait(2000), and enforced the strict test isolation so that no 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.

What should we do? 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

  • Structure Visually with AAA: Standardize tests using separated Arrange, Act, and Assert blocks to make your test suite readable, maintainable, and self-documenting.
  • Design Multi-Path 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.
  • Adapt Shapes to Scale: Scale your testing suite by matching its shape to your system’s architecture, leveraging the Testing Pyramid for monolithic designs and the Testing Trophy for microservices.
  • 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.
  • Eliminate Diagnostic Waste: Resolve flakiness with dynamic waits and environment isolation, prevent false negatives with strict assertions and contract validation, and eliminate false positives by asserting on public behavior rather than internal mechanics.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *