Fundamentals 11 min read

Five Common Pitfalls in Unit Testing and How to Avoid Them

The article explains why unit tests are essential for software quality, identifies five typical pitfalls—testing per function instead of per feature, chasing code‑coverage metrics, over‑reliance on mocks, writing tests that never fail, and handling nondeterministic behavior—and provides practical recommendations to overcome each issue.

DevOps
DevOps
DevOps
Five Common Pitfalls in Unit Testing and How to Avoid Them

Unit testing aims to ensure that a system continues to work as expected over time, improving code quality and freeing developers to focus on higher‑level tasks.

The article outlines five traps that render unit tests ineffective and shows how to fix them.

1. Write a unit test for each function

Testing a single function is not enough; tests should target a single behavior of that function, often expressed as a single assertion. For example, instead of test_calculate_average , a better test is test_calculate_average_return_0_for_empty_list , which documents expected behavior.

Write a unit test for each functional unit, not each code unit.

Focusing on external behavior keeps tests resilient to internal refactoring.

2. Write tests only to boost code‑coverage

Code coverage is merely a measurement tool; 100% coverage does not guarantee all edge cases are exercised. The article shows a simple Python example where high coverage masks missing edge‑case checks.

def average(elements: List[int]):
  return sum(elements) / len(elements)

def test_average_returns_average_of_list:
  result = average([1,3,5,7])
  assert result == 4

Instead of chasing coverage, focus testing effort on risky code as suggested by Martin Fowler.

You should concentrate your testing effort on risk points. — Martin Fowler, Refactoring

3. Over‑reliance on mocks

Heavy use of mocks can indicate a function is too complex. The article provides a Python example with extensive mocking of middleware, suggesting refactoring the code to simplify testing.

# custom_middleware.py ####################################
class CustomHeaderMiddleware(BaseHTTPMiddleware):
    async def dispatch(self, request, call_next):    
        response = await call_next(request)
        response.headers["CustomField"] = "bla"
        return response

# test_custom_middleware.py ###############################
async def endpoint_for_test(_):
    return PlainTextResponse("Test")

middleware = [Middleware(CustomHeaderMiddleware)]
routes = [Route("/test", endpoint=endpoint_for_test)]
app = Starlette(routes=routes, middleware=middleware)

@pytest.mark.asyncio
async def test_middleware_sets_field():
    client = TestClient(app)
    response = client.get("/test")
    assert response.headers["CustomField"] == "bla"

When many mocks are needed, consider refactoring the code to make it more testable.

4. Write tests that never fail

Tests that cannot fail give a false sense of security. An example shows a test that only verifies a mocked response, which would not catch errors in the production code.

def get_film(id: str):
    data = {"query": QUERY, "variables": json.dumps({"id": id})}
    response = requests.post(URL, data=data)
    return response.json()["data"]["film"]

def test_get_film_returns_successfully():
    mock_response = {
        "data": {
            "film": {
                "title": "a New Test",
                "id": "testId",
                "episodeID": 4
            }
        }
    }
    with requests_mock.Mocker() as mock:
        mock.post(URL, json=mock_response)
        result = get_film("foo")
        assert result == {
            "title": "a New Test",
            "id": "testId",
            "episodeID": 4
        }

Ensure tests can fail by asserting real behavior rather than just printing or relying on mocks.

5. Use unit tests for nondeterministic behavior

Tests that depend on the current time, date, or random data become flaky. The article recommends controlling time flow (e.g., using Python's freeze‑gun ) and avoiding randomness in test inputs.

By eliminating nondeterminism, tests become reliable indicators of code health.

Conclusion

To write effective unit tests, focus on testing each functional aspect, prioritize risky code over coverage percentages, minimize mock usage, ensure tests can fail, and keep nondeterministic factors out of tests.

code coveragesoftware qualityUnit Testingmockingtest designtest pitfalls
DevOps
Written by

DevOps

Share premium content and events on trends, applications, and practices in development efficiency, AI and related technologies. The IDCF International DevOps Coach Federation trains end‑to‑end development‑efficiency talent, linking high‑performance organizations and individuals to achieve excellence.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.