Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Replace Hamcrest assertions with AssertJ assertions #1076

Merged
merged 1 commit into from
Jan 9, 2024

Conversation

HeikoKlare
Copy link
Contributor

Comprehensive assertions can be defined with different libraries, of which Hamcrest and AssertJ are two of the most popular ones. While Platform tests currently only use Hamcrest at some places, AssertJ has several advantages including:

  • Easier to apply and learn due to fluent API rather than matcher providing comprehensive code completion support
  • Higher expressiveness by easily chaining multiple assertions on the same object
  • Better comprehensibility by chaining instead of nesting calls and avoiding the necessity to start an assertThat statement with a custom message if desired

This change migrates the existing usages of Hamcrest to AssertJ (except for one custom Matcher implementation). Doing that it also makes use of String formatting rather than String concatenation with a potential slight performance improvement.

See also the discussion eclipse-platform/.github#177

Copy link
Contributor

github-actions bot commented Jan 5, 2024

Test Results

   594 files  ±0     594 suites  ±0   42m 19s ⏱️ - 4m 19s
 3 864 tests ±0   3 842 ✅  - 1   22 💤 +1  0 ❌ ±0 
12 198 runs  ±0  12 037 ✅  - 3  161 💤 +3  0 ❌ ±0 

Results for commit ae41a9e. ± Comparison against base commit 4ebb172.

This pull request skips 1 test.
org.eclipse.debug.tests.ui.LaunchConfigurationTabGroupViewerTest ‑ testOnlyDefaultTabInOtherConfigIsActivated

♻️ This comment has been updated with latest results.

@HeikoKlare HeikoKlare force-pushed the hamcrest-to-assertj branch 2 times, most recently from c7abab7 to 05e2a97 Compare January 5, 2024 17:03
@HeikoKlare HeikoKlare marked this pull request as ready for review January 5, 2024 17:41
@HeikoKlare
Copy link
Contributor Author

This pull request skips 1 test.

org.eclipse.debug.tests.ui.LaunchConfigurationTabGroupViewerTest ‑ testOnlyDefaultTabInOtherConfigIsActivated

This PR corrects a faulty assertion in LaunchConfigurationTabGroupViewerTest#testOnlyDefaultTabInOtherConfigIsActivated(), which requires it's temporary disablement as the test fails with the correct assertion, see #1075.

@Bananeweizen
Copy link
Contributor

Just one more recommendation: It's perfectly fine to convert existing tests with error message towards AssertJ as(errorMessage) calls. However, that is not the best way. as("something") just changes the description of the unit under test, but the original error message remains.
assertThat(foo()).as("very long error message").isTrue() will still produce "Expecting 'very long error message' to be true, but it was false". It just changes the name or description. assertThat(foo).withFailMessage(()->"my custom message").isTrue() would really replace the complete message. Now you might be wondering why does the "as" method exist in the first place? Take this example:

assertThat(familyMember.get(0)).as("father").has...
assertThat(familyMember.get(1)).as("mother").has...

A failure in that test will be written "Expecting 'father' to have..."

And let me repeat: Your conversion of the existing tests is fine, no need to change it there. It just allows even more nice error messages in new tests.

Comment on lines 225 to 228
assertThat(longRunningProjects).as(
"all build jobs have started in time although infinitely running builds with conflicting rules exist")
.anySatisfy(longRunningProject -> assertThat(TimerBuilder.getStartedProjectBuilds())
.doesNotContain(longRunningProject));
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
assertThat(longRunningProjects).as(
"all build jobs have started in time although infinitely running builds with conflicting rules exist")
.anySatisfy(longRunningProject -> assertThat(TimerBuilder.getStartedProjectBuilds())
.doesNotContain(longRunningProject));
assertThat(longRunningProjects)
assertThat(TimerBuilder.getStartedProjectBuilds())
.withFailMessage("all build jobs have started in time although infinitely running builds with conflicting rules exist")
.doesNotContainAnyOf(longRunningProjects);

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thank you for your feedback, @Bananeweizen!
I also thought some time about the as(message) calls, in particular because some of the existing failure messages do not really make sense as a description for the assertion. Many of them would totally make sense when passed to withFailureMessage(message), but then the failure report will only contain that message and nothing about the expected/actual value. In many cases it can be helpful to have more detailed information about the actual and expected values. However, for pure boolean assertions it totally makes sense to simply use withFailureMessage(), which is why I will update the PR according.

Regarding your concrete proposal for one assertion: the proposed assertion is semantically different from the original one. The original one checks that "not all long running project builds have started". The proposed on checks that "none of the long running project builds have started". This assertion looked better with Hamcrest, where you can combine matchers to: assertThat(TimerBuilder.getStartedProjectBuilds(), not(containsInAnyOrder(longRunningProjects))). Unfortunately, I did not find an according assertion in AssertJ, which is why I made this strange construct of assertions realizing "there is a long running project build that has not started". If you know a better way to express that, I'd be happy if you can let me know :-)

@HeikoKlare HeikoKlare force-pushed the hamcrest-to-assertj branch 3 times, most recently from cd9f778 to 58b19ac Compare January 7, 2024 16:33
Comment on lines +119 to +122
assertThat(file).matches(IFile::exists, "exists")
.matches(it -> workspace.getRoot().exists(it.getFullPath()), "is contained in workspace")
.matches(it -> folder.findMember(it.getName()).exists(), "is contained in folder: " + folder)
.isEqualTo(workspace.getRoot().findMember(file.getFullPath()));
Copy link
Contributor Author

@HeikoKlare HeikoKlare Jan 7, 2024

Choose a reason for hiding this comment

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

I have now also replaced several pure boolean assertions with matches assertions that improve the output in case of a failure, in particular because they also output the subject under test. This makes it far easier to provide a helpful error message with minimal custom text.
The marked code snipped shows as example for that.

Copy link
Contributor

Choose a reason for hiding this comment

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

That's an interesting technique that I will remember, thanks for that hint. Regarding earlier discussions: You are completely right about as() and withFailureMessage() sometimes not fitting well, therefore in practice it's sometimes more ugly than the nice theory I painted before. Personally I try to use as() as much as possible, if the assertion itself has helpful text (like isNonEmptyDirectory), but switch to withFailMessage, if the default error message is to generic (like isTrue). Since withFailMessage can take format strings and objects, that's also okay performance wise. However, I'll probably use more of your matches(...) version in the future, as that probably simplifies custom error messages a bit.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

As a side note: a method reference used for matching can even be negated easily with Predication.not(), which, when statically imported, can be used like this: assertThat(file).matches(not(IFile::exists), "does not exist").

@HeikoKlare HeikoKlare force-pushed the hamcrest-to-assertj branch 3 times, most recently from e9926f4 to 52ccbb8 Compare January 8, 2024 14:57
Comprehensive assertions can be defined with different libraries, of
which Hamcrest and AssertJ are two of the most popular ones. While
Platform tests currently only use Hamcrest at some places, AssertJ has
several advantages including:
* Easier to apply and learn due to fluent API rather than matcher
providing comprehensive code completion support
* Higher expressiveness by easily chaining multiple assertions on the
same object
* Better comprehensibility by chaining instead of nesting calls and
starting `assertThat` always with the subject under test and not a
custom message in case such a message is desired

This change migrates the existing usages of Hamcrest to AssertJ (except
for one custom Matcher implementation). Doing that it also makes use of
String formatting rather than String concatenation with a potential
slight performance improvement.
@HeikoKlare HeikoKlare force-pushed the hamcrest-to-assertj branch from 52ccbb8 to ae41a9e Compare January 8, 2024 15:35
@Bananeweizen
Copy link
Contributor

👍🏻 LGTM

@HeikoKlare
Copy link
Contributor Author

Thanks for your review and all your comments, @Bananeweizen! Feels like this PR was worth the effort as it improves some validations even semantically, and particularly because I I've learned quite some things about validations / AssertJ.

@HeikoKlare HeikoKlare merged commit 9aa3ddb into eclipse-platform:master Jan 9, 2024
16 checks passed
@HeikoKlare HeikoKlare deleted the hamcrest-to-assertj branch January 9, 2024 15:24
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.

2 participants