Skip to content

Print object fields via reflection when toString() is not implemented - #3844

Merged
jselbo merged 1 commit into
mockito:mainfrom
asolntsev:feature/print-object-fields-with-reflection
Jul 15, 2026
Merged

jselbo merged 1 commit into
mockito:mainfrom
asolntsev:feature/print-object-fields-with-reflection

Conversation

@asolntsev

@asolntsev asolntsev commented Jul 12, 2026 •

Copy link
Copy Markdown
Contributor

Motivation

In my project, it often happens that value object doesn't have its own toString() method. And comes from a JAR that I cannot easily change.

For such objects, it's highly inconvenient to debug failing tests showing just MyClass@12345 in failure message.

@TimvdLippe @raphw

The problem

In verification failure messages, Mockito uses toString() method to print out the argument object. For objects not having their own toString(), the useless description like "MyClass@12345" was generated (the default Object.toString() implementation).

The solution

Now, ValuePrinter reflects over an object's declared fields when its class doesn't override toString(), thus producing readable description of the object. Too long field values are truncated.

Example of error message:

Argument(s) are different! Wanted:
mock.run(
    Credentials[username=john, password=secret]
);
-> at org.mockitousage.matchers.ReflectivePrintingTest.refEq_printsObjectFieldsUsingReflection(CustomToStringTest.java:53)
Actual invocations have different arguments:
mock.run(
    Credentials[username=usr, password=pwd]
);
-> at org.mockitousage.matchers.ReflectivePrintingTest.refEq_printsObjectFieldsUsingReflection(CustomToStringTest.java:51)

Where

/**
 * Class without own "toString()" method
 */
 class Credentials {
     private String username;
     private String password;
}

Checklist

  • Read the contributing guide
  • PR should be motivated, i.e. what does it fix, why, and if relevant how
  • If possible / relevant include an example in the description, that could help all readers
    including project members to get a better picture of the change
  • Avoid other runtime dependencies
  • Meaningful commit history ; intention is important please rebase your commit history so that each
    commit is meaningful and help the people that will explore a change in 2 years
  • The pull request follows coding style (run ./gradlew spotlessApply for auto-formatting)
  • Mention Fixes #<issue number> in the description if relevant
  • At least one commit should end with Fixes #<issue number> if relevant

@codecov-commenter

codecov-commenter commented Jul 12, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 84.37500% with 5 lines in your changes missing coverage. Please review.
✅ Project coverage is 86.92%. Comparing base (d8638e1) to head (f785fa9).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
...g/mockito/internal/matchers/text/ValuePrinter.java 83.87% 4 Missing and 1 partial ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##               main    #3844      +/-   ##
============================================
+ Coverage     86.84%   86.92%   +0.08%     
- Complexity     3044     3063      +19     
============================================
  Files           344      344              
  Lines          9172     9203      +31     
  Branches       1135     1138       +3     
============================================
+ Hits           7965     8000      +35     
+ Misses          917      916       -1     
+ Partials        290      287       -3     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@asolntsev
asolntsev force-pushed the feature/print-object-fields-with-reflection branch 3 times, most recently from 04792af to 8f62553 Compare July 12, 2026 21:17
}
}

private static String reflectiveToString(Object value) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is probably not nearly as robust as Apache commons ReflectionToStringBuilder but checking the source I don't think it would be trivial to pare down for this repo:
https://github.com/apache/commons-lang/blob/master/src/main/java/org/apache/commons/lang3/builder/ReflectionToStringBuilder.java

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we don't need all the features of ReflectionToStringBuilder.
I would say, it's not "robust", it just allows configuration: "skip null values" etc. We don't need it.

private static Iterable<Field> fields(Class<?> type) {
return Arrays.stream(type.getDeclaredFields())
.filter(field -> !Modifier.isStatic(field.getModifiers()))
.filter(field -> field.getName().indexOf('$') < 0)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use Field.isSynthetic ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, fixed.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like you need to push a new commit with the change

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ooops :)
Pushed.

return Arrays.stream(type.getDeclaredFields())
.filter(field -> !Modifier.isStatic(field.getModifiers()))
.filter(field -> field.getName().indexOf('$') < 0)
.sorted(comparing(Field::getName))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why would the order of type.getDeclaredFields() be non-deterministic? Not sure why sorting is necessary

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Java reflection doesn't guarantee the order of fields/methods.

From Class.getDeclaredFields javadoc:

The elements in the returned array are not sorted and are not in any particular order.

}
}

private static String truncate(String text) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what if the different value gets truncated? This wouldn't be very useful then. Ideally this would have some behavior to report only the differing fields, or ensure the difference isn't truncated

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While it's theoretically possible, it hopefully doesn't happen too often.
Anyway, it will be better than now. :)

Yes, I totally agree that ideally, Mockito should report the differences in the fields. For too long string, probably report the exact position of diff in the string. Maybe in another PR?..

@jselbo

jselbo commented Jul 13, 2026

Copy link
Copy Markdown
Collaborator

This seems reasonable at first glance, and low risk if it's only scoped to refEq. Still, I left some comments

@asolntsev

Copy link
Copy Markdown
Contributor Author

@jselbo It's scoped not only to refEq, but also to eq and same. BUT it's scoped for objects that don't have own toString method. So anyway, it's much better than previously :)

Comment thread .gitignore Outdated
checksums/
/.tool-versions

/.claude/settings.local.json

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please remove

@asolntsev asolntsev Jul 14, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No problems, I've removed the file.

P.S. Though, it's usually a common practice to ignore this file.

assertThatThrownBy(() -> verify(mock).run(expected))
.isInstanceOf(AssertionError.class)
.hasMessageContaining("Argument(s) are different! Wanted:")
.hasMessageContaining("Credentials[password=secret, username=john]")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if there is a privacy/security concern here. Suppose test data has real sensitive information (like reading some secret keys -- probably bad practice but I'm sure it happens). Now Mockito will print the private fields which would otherwise not be exposed if the class doesn't implement toString(). This could be a bad surprise for someone.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Valid concern.
But I would say it should not be a problem in tests.

  1. Mockito is mostly used in unit-tests (and sometimes integration tests). Usually people use test data, not real sensitive data in tests.
  2. And if they use some real passwords in tests, they should be ready for the risk that these passwords will appear in logs, test reports or CI notifications.
  3. Usually Jenkins, GitHub Actions etc. do mask the secure parameters like password.

In verification failure messages, Mockito uses `toString()` method to print out the argument object. For objects not having their own `toString()`, the useless description like "MyClass@12345" was generated (the default `Object.toString()` implementation).
Now, ValuePrinter reflects over an object's declared fields when its class doesn't override `toString()`, thus producing readable description of the object. Too long field values are truncated.
@asolntsev
asolntsev force-pushed the feature/print-object-fields-with-reflection branch from b5b83a9 to f785fa9 Compare July 15, 2026 08:16
@asolntsev
asolntsev requested a review from jselbo July 15, 2026 12:51
@jselbo

jselbo commented Jul 15, 2026

Copy link
Copy Markdown
Collaborator

Ok I'm comfortable merging this. Thanks for this improvement!

@jselbo
jselbo merged commit ee8a330 into mockito:main Jul 15, 2026
20 checks passed
@asolntsev
asolntsev deleted the feature/print-object-fields-with-reflection branch July 16, 2026 15:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants