Verdict
No
Do not write tests for a toString() that only feeds log lines and the debugger; generate it, and test it only when users, stored data or other code read its output.
Why
- Blast radiusinternal
- Change frequencyrarely
- Detectabilityeventually
- Reversibilitytrivial
- Test costmoderate
Do not write tests for a toString() that only feeds log lines and the debugger. For that typical case, Blast radius is internal, because only developers read the output. Change frequency is rarely: the method changes when the class gains a field. Detectability is eventually, since a missing field shows up only when someone reads a log line during an incident. Reversibility is trivial, because the next deploy fixes the output and no data depends on it. Test cost is moderate: an assertion on the full string fails every time someone adds a field. Rule R13 gives Do not test, but the case is borderline: once users, stored data or another program read the string, the decision moves to a test.
| When | Decision | Why |
|---|---|---|
| A screen shows toString() output as a label, as Android's ArrayAdapter and Swing's JComboBox do by default | Test minimally: one test that asserts the label for one object | Blast radius rises to users: a wrong label is text that customers read |
| A JSON library writes an enum or a value through toString(), for clients you cannot update | Test minimally: one serialization test that asserts the written value | Blast radius rises to users and Reversibility to with-effort: a changed string reaches installed clients that cannot read it |
| Code stores toString() output as a cache key or a database value and reads it back later | Test minimally: one test that asserts the exact string for one fully populated object | Blast radius rises to users and Reversibility to with-effort: a changed format stops matching the keys and values already stored, which then need a migration |
| The class holds a password, a token or personal data, and a record, a data class or Lombok prints every field | Test mandatory: assert that toString() output does not contain the secret value | Blast radius rises to safety-or-legal, Detectability to never and Reversibility to impossible: a secret copied into log storage cannot be taken back |
| Two entities reference each other, and a generated toString() on each side prints the other | Test minimally: link both sides in a unit test and call toString() on each | Blast radius rises to users, Detectability to same-day and Test cost falls to trivial: a request that logs either entity fails with a StackOverflowError |
What breaks if you don't test
For debug output, a missing test costs little. An engineer reads Order[id=42] in a log during an incident, sees that the status is missing, and queries the database for it. The incident takes a few minutes longer, and nobody outside the team notices. The conditions table lists the failures that reach users.
What you lose if you over-test
An exact-string test for toString() repeats the field list of the class. When a class gains a field every quarter, the test fails four times a year, and each fix pastes the new output into the expected value. Under a coverage gate, toString() tests are among the cheapest lines to cover, so they fill the quota that should point at untested logic.
What to do instead
Generate the method so that no hand-written body can drift from the fields: Java records, Kotlin data classes, Lombok @ToString, or the IDE generator. Mark fields that hold secrets with @ToString.Exclude, as the Lombok toString guide shows. Give callers getters for every value in the string, so that no code parses toString() output. For a screen label, write a separate method such as displayName(), so that a change to debug output cannot change what customers read.
When the answer changes
- A screen, a JSON library, a cache or a file format starts to read the string.
- A field with a password, a token or personal data joins the class. The OWASP Logging Cheat Sheet lists the data to keep out of logs.
- The class gains a reference back to its parent, and both classes generate toString().
Code example
The record that logs a password
A Java record generates a toString() that prints every component with its name:
record LoginRequest(String email, String password) {
@Override
public String toString() {
// The generated version prints: LoginRequest[email=ana@example.com, password=hunter2]
return "LoginRequest[email=" + email + ", password=***]";
}
}
@Test
void toStringHidesThePassword() {
var request = new LoginRequest("ana@example.com", "hunter2");
assertFalse(request.toString().contains("hunter2"));
}
Without the override, log.info("Login attempt: {}", request) writes the password into the logs and their backups. The test checks that the secret is absent instead of comparing the full string, so a new component does not break it. I write this toString() test in every codebase that keeps credentials in a record.
Related questions
FAQ
- Should I test toString() with JUnit?
No, a toString() that only feeds logs and the debugger does not need a JUnit test, because a wrong string costs your team a few minutes and no customer sees it. Write a JUnit test when users, stored data or another program read the output.
- Should I test a toString() generated by Lombok or a record?
No, a generated toString() has no hand-written body to get wrong, so a test would only repeat the field list. Test it when the class holds a secret that the generated method prints, or when two classes print each other until a StackOverflowError.
- How do I keep passwords out of toString()?
Mark the field with
@ToString.Excludein Lombok, or override toString() in a Java record or a Kotlin data class. Then add one unit test that builds the object with a known password and asserts that toString() output does not contain it.- Should I test toString() to reach a coverage target?
No, toString() tests raise coverage without catching failures that matter, because a wrong debug string reaches no customer. Exclude generated methods from the report instead: JaCoCo 0.8.2 and later skips methods marked with an annotation named
Generated, which Lombok adds whenlombok.addLombokGeneratedAnnotation = trueis set.