Verdict
Yes
Write one test that your main error path logs the failure with its request ID, and do not assert on ordinary info and debug log lines.
Why
- Blast radiusinternal
- Change frequencyregularly
- Detectabilityeventually
- Reversibilitywith-effort
- Test costmoderate
Test logging minimally: one test for the log line an incident depends on, none for the rest. For diagnostic logging, Blast radius is internal, because a missing line slows your team down and no customer sees it. Detectability is eventually: nobody notices a missing line until someone searches for it during an incident. Change frequency is regularly, since log lines move with the code around them, and Reversibility is with effort, because the fix is a hotfix and the entries from before it never exist. Test cost is moderate, since a log assertion needs a capture fixture and breaks when someone rewords a message.
| When | Decision | Why |
|---|---|---|
| The log is an audit trail that a regulation or a contract requires | Test mandatory: assert that every covered action writes one audit entry | Blast radius rises to safety-or-legal and Reversibility to impossible, because a missing audit entry cannot be written later |
| The request logger can see passwords, tokens or card numbers | Test mandatory: assert that the logger redacts passwords, tokens and card numbers | Blast radius rises to safety-or-legal, Detectability to never and Reversibility to impossible, because a leaked secret raises no error and cannot be taken back |
| An alert or a metric matches the text of a log line | Test the log line: assert its level and the event name that the alert depends on | Blast radius rises to users and Detectability to never, because a reworded message silences the alert |
| The log lines are debug output that production filters out | Do not test debug log lines; delete the ones nobody reads | Blast radius falls to none, because nobody but the developer sees debug output |
| The logger is a static global that a test cannot capture without a refactor | Test logging differently: alert when an expected log event stops appearing in staging or production | Test cost rises to heavy, so a signal from the running system is cheaper |
What breaks if you don't test
A refactor moves a try block, and the except branch that logged the payment provider's error code now swallows the exception silently. CI stays green. During the next outage, the on-call engineer opens the logs and finds a gap where the error code should be. The incident runs longer, because the team has to add the line back, deploy, and wait for the failure to happen again.
What you lose if you over-test
Asserting on every log statement ties the suite to wording. A test that expects Processing order 42 for user 7 fails when someone fixes a typo, and the fix is to paste the new string into the test, which then checks nothing new. Across hundreds of log lines, each refactor becomes a round of test edits. Tests that mock the logger and verify each call also bind you to one logging API, so a move to structured logging rewrites the tests too.
How to test
Test at the unit level with the capture tool your stack already has: the pytest caplog fixture in Python, or a Logback ListAppender in Java. The minimum set is one test for the error path an incident depends on: trigger the failure, then assert on the level and on the fields an engineer needs, such as the request ID. Assert on an event name or structured fields instead of message prose, so rewording does not break the test. Add a regression test when a missing log line has already cost you an incident.
When the answer changes
- Someone builds an alert or a dashboard on a log line, which turns that line into an interface for another system.
- Your logs start to hold user data, or an auditor asks for them. Follow the OWASP Logging Cheat Sheet and add a redaction test.
- An error tracker already reports every exception with its stack trace, so the error log stops being the only place a failure shows.
Real incident + Code example
The alert that matched a reworded message
On a payments service I worked on, the on-call alert counted log lines containing card declined by issuer. During a refactor, a developer changed that message to Issuer declined the card. No test covered the line, and the alert matched nothing for nine days. We found out when an issuer outage declined a batch of renewals and customers wrote to support first. The test we added checks an event name, which nobody rewrites for style:
import logging
def test_declined_charge_logs_event(caplog):
caplog.set_level(logging.WARNING, logger="payments")
charge(order_id="ord_42", card=DECLINED_CARD)
events = [r for r in caplog.records
if getattr(r, "event", None) == "payment_declined"]
assert len(events) == 1
assert events[0].levelname == "WARNING"
assert events[0].order_id == "ord_42"
The production code passes extra={"event": "payment_declined", "order_id": order_id} to logger.warning, and the alert now matches the event field. Anyone can reword the message; only renaming the event fails the build.
Related questions
FAQ
- Should we unit test logging?
Unit test logging only where someone depends on the output: the error path an incident relies on, audit entries, and lines that feed alerts. Ordinary info and debug lines do not repay a test, because a missing one costs your team time and no customer sees it.
- Should log statements be tested?
A log statement needs a test when a person or a system reads it as a contract, such as an auditor or an alert rule. Assert on an event name or structured fields, so that rewording a message does not break the suite.
- Should I test that passwords are not logged?
A test that passwords and tokens never reach the logs is mandatory whenever your request logging can see them, because a secret in log storage gets copied into backups and cannot be taken back. Send a request with a fake password through the logging middleware and assert that the captured output does not contain it.