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

Document that isEqualTo is not for testing equals(), etc. #268

Open
cpovirk opened this issue Dec 13, 2016 · 16 comments
Open

Document that isEqualTo is not for testing equals(), etc. #268

cpovirk opened this issue Dec 13, 2016 · 16 comments
Labels
P3 not scheduled type=documentation Documentation that is other than for an API

Comments

@cpovirk
Copy link
Member

cpovirk commented Dec 13, 2016

Our principle is:

When you call assertThat(foo), you're not testing any of the methods of foo. Rather, you're testing the result of some previous call:

UserId id =
    employeeService.manager("cpovirk");
assertThat(id).isEqualTo(
    UserId.of("jeremymanson"));

So if you use assertThat(foo).containsExactly("a", "b"), you're not testing foo.contains(), foo.containsAll(), or foo.iterator(). With isEqualTo(), it's the same: You're not testing equals(). The only difference between containsExactly() and isEqualTo() is that there's only one way to test equality but many ways to test collection contents. As a result, people assume that they can guess how isEqualTo() is implemented and then rely on it.

(Of course, it turns out that there's more than one way to test equality: You can check == first, as we do.)

We could make this "work." But I fear muddying the waters, encouraging users to depend on more implementation details (even implementation details that aren't what users guess :)). I could also make the case that we'll mildly hurt performance by always calling equals(), but I don't see that as a major concern in the absence of evidence.

@cpovirk cpovirk added the type=documentation Documentation that is other than for an API label Dec 13, 2016
@torbenbru
Copy link

Hey, newbie here who'd love to contribute. Rather seems like you're listing pros and cons of a change to me. Any other way I could contribute?:)

@shtratos
Copy link

I'm a simple library user, but the whole premise "isEqualTo is not for testing equals()" sounds strange to me.

Tl;DR IMO it'll be extremely confusing for many users to change isEqualTo not to use foo.equals() for equality check.

If .isEqualTo() is not calling foo.equals() that would be the least expected behaviour to me.
What other possible implementation it could have?

For years, Java was emphasising the difference between reference (==) equality and .equals() one.
And for tests, testing object equality with .equals() seems most sensible default choice unless you want to verify object identity.

Remember that Truth exists not by its own, but alongside libraries like Mockito and Hamcrest (btw we use both in our projects along with the Truth). I might be too old, but I can't unlearn the conventions set in those libraries that have sameInstance/same and equalTo/eq matchers to differentiate between 2 types of equality checks.

Comparing to contains check, I rely on Collection contract to guarantee consistent implementation of container concept i.e. it should not matter how exactly I check if my collection "contains" an object: with contains(), iterator(), stream() or containsAll(). Similarly, I rely on hashCode/equals contract to guarantee implementation of equality concept. Using some other contract for equality checks would be extremely surprising for me.

@cpovirk
Copy link
Member Author

cpovirk commented Mar 14, 2017

The contains example is a good one: Truth has to pick some way to check containment. If you're using a collection with a bad implementation of contains or iterator, Truth's contains might or might not catch that, since it might use some other method whose implementation is correct. Similarly, if you're using an object with a bad implementation of equals, Truth isEqualTo might or might not catch it.

The idea is: If you want to test method foo, then call method foo, rather than infer which assertions are likely to cause Truth to call foo.

@cpovirk
Copy link
Member Author

cpovirk commented Mar 14, 2017

It is certainly true that the name "isEqualTo" helps to encourage people to use the method in the precise way that I'm discouraging here. We had some good reasons for the name we chose, but I don't think we considered this problem at the time, and we may have made a suboptimal decision. Sorry about that.

@kevinb9n
Copy link
Contributor

Other points:

  • I don't think anyone was ever suggesting changing isEqualTo to be based on == instead of equals
  • The idea that Truth doesn't test things, but just makes assertions about the results of the methods you called yourself, is the very center of its design.
  • Testing whether equals is working correctly is a more complex job; we provide EqualsTester as part of guava-testlib for that.

@jbduncan
Copy link

jbduncan commented Mar 14, 2017

@cpovirk @kevinb9n So am I right to understand that, for example, if we have the following method,

static String getStringOnOption(int option) {
  switch (option) {
    case 1: return "qwerty";
    ...
  }
}

...then we very well should use Truth's isEqualTo to "test"/assert that we get the result we expect, such as in the following example,

assertThat(getStringOnOption(1)).isEqualTo("qwerty"); // passes!

...just like we would if we used JUnit 4's assertEquals("qwerty", getStringOnOption(1));?

But if we have a new data class called NewClass with a custom equals/hashCode,

final class NewClass {
  String f1;
  int f2;
  
  @Override public boolean equals(Object o) {
    ...
  }
  
  @Override public int hashCode() {
    ...
  }
}

...then we shouldn't use...

assertThat(new NewClass()).isEqualTo(new NewClass());

...in a naive attempt to rigorously check that it meets the equals/hashCode contracts, and instead use something like guava-testlib's EqualsTester?

@kevinb9n
Copy link
Contributor

@jbduncan Well said! That's exactly right.

@shtratos
Copy link

Thanks for clarification! 👍 I misunderstood what was the intention.

@dimo414
Copy link
Contributor

dimo414 commented May 23, 2017

@nymanjens has been running into this with MultimapSubject recently, where we use .equals() to check for equality, but .containsExactlyEntriesIn() to actually generate the failure message. If the multimap in question is mis-implemented this can lead to surprising results. It would definitely be good to document this more clearly somewhere, though I'm generally of the opinion that there's no hope in the face of classes that don't obey their own contracts.

@dimo414
Copy link
Contributor

dimo414 commented Sep 30, 2017

@cpovirk with all your SPI refactorings and documentings, maybe now's a good time to address this?

@cpovirk
Copy link
Member Author

cpovirk commented Oct 3, 2017

I am trying to focus on the things that require a bigger chunk of time while I have a bunch of time officially allocated to Truth, so this may continue to slip :( But eventually...

@cpovirk
Copy link
Member Author

cpovirk commented Sep 19, 2018

We have some text for this in an internal doc for the TruthSelfEquals cleanup. I should also find what kevinb wrote long ago.

@cpovirk
Copy link
Member Author

cpovirk commented Sep 19, 2018

More text available in a doc for... the SelfComparison cleanup :)

kevinb had said:

A test invokes the code under test and gets a result. Truth helps the test check that result. The idea of assertThat(Predicate) goes against that: the test hands the code under test off to something else to invoke it. That makes it similar to our *Tester classes and, imho, out of scope for Truth.

@cpovirk
Copy link
Member Author

cpovirk commented Mar 14, 2019

Kevin added some docs in ffde169. I think we could still do more here, including putting something in the FAQ, but I think Kevin's commit is enough to remove this from the 1.0 blockers. (Thanks, Kevin!)

@cpovirk cpovirk removed this from the Release 1.0 milestone Mar 14, 2019
@cpovirk cpovirk added this to the First post-1.0 feature push milestone Jun 14, 2019
cpovirk added a commit to cpovirk/truth that referenced this issue Jun 19, 2019
We've decided that Truth isn't in the business of handling the "act" part of "arrange, act, assert." To test an implementation of equals(), users can call equals() themselves.

But that was the case way back when we originally added this. What really prompted this now is that I realized this change would chip away at "Not true that..."-style messages :) Additionally, I'm looking at making a similar change to IterableSubject, and I figure this is as good a time as any to be consistent in not handling bad equals() methods (which we already don't handle in MultimapSubject).

Also, avoid NPE on null actual value.

Relevant: google#268

[]

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=253994206
cpovirk added a commit that referenced this issue Jun 19, 2019
We've decided that Truth isn't in the business of handling the "act" part of "arrange, act, assert." To test an implementation of equals(), users can call equals() themselves.

But that was the case way back when we originally added this. What really prompted this now is that I realized this change would chip away at "Not true that..."-style messages :) Additionally, I'm looking at making a similar change to IterableSubject, and I figure this is as good a time as any to be consistent in not handling bad equals() methods (which we already don't handle in MultimapSubject).

Also, avoid NPE on null actual value.

Relevant: #268

[]

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=253994206
@raghsriniv raghsriniv added the P3 not scheduled label Jun 24, 2019
copybara-service bot pushed a commit that referenced this issue Mar 23, 2023
- Don't let users declare `class FooSubject extends ComparableSubject<Foo>` if `Foo` is a raw type. (But, as [the tests show](https://github.com/google/truth/blob/f1f0a9aad6e8fb4f5d68ad2e5850a2c5ca2478fa/core/src/test/java/com/google/common/truth/ComparableSubjectTest.java#L221), you can still write `Truth.assertThat(foo).isLessThan(...)` and similar for a raw type `Foo`.)
- Don't let users pass a null value to `isGreaterThan` and similar. `null` is forbidden by the contract of `compareTo`, but some implementation may have tried to support it anyway. Anyone who wants to test `compareTo` should be using `ComparatorTester`, anyway, not `isGreaterThan` and friends. (Compare [our policy on using `isEqualTo` (rather than `EqualsTester`) for testing `equals`](#268).)

RELNOTES=n/a
PiperOrigin-RevId: 518568376
copybara-service bot pushed a commit that referenced this issue Mar 23, 2023
- Don't let users declare `class FooSubject extends ComparableSubject<Foo>` if `Foo` is a raw type. (But, as [the tests show](https://github.com/google/truth/blob/f1f0a9aad6e8fb4f5d68ad2e5850a2c5ca2478fa/core/src/test/java/com/google/common/truth/ComparableSubjectTest.java#L221), you can still write `Truth.assertThat(foo).isLessThan(...)` and similar for a raw type `Foo`.)
- Don't let users pass a null value to `isGreaterThan` and similar. `null` is forbidden by the contract of `compareTo`, but some implementation may have tried to support it anyway. Anyone who wants to test `compareTo` should be using `ComparatorTester`, anyway, not `isGreaterThan` and friends. (Compare [our policy on using `isEqualTo` (rather than `EqualsTester`) for testing `equals`](#268).)

RELNOTES=n/a
PiperOrigin-RevId: 518896343
@cpovirk
Copy link
Member Author

cpovirk commented Nov 17, 2023

Another factor here is that, even if we change isEqualTo in as many Subject implementations as we can, we won't be able to change every custom Subject in the wild. As a result, a given assertThat(foo).isEqualTo(foo) might test Foo.equals today, but it might stop doing so as soon as you import FooSubject.assertThat(Foo) (or as soon as someone adds an assertThat(Foo) overload to a class whose assertThat methods you import already).

Now, it's fair to say that any custom Subject can have any arbitrary bug, so the sorts of changes I'm talking about can always cause problems. But our policy is that a Subject implementation doesn't need to check that equals works, so a custom Subject that omits such checking isn't "buggy." The only principled way to go here would be to state that every Truth subject should check the implementation of equals (which might mean arbitrarily complex things, like EqualsTester or MapEqualsTester). (Note that we can't even make such a change today without inconveniencing people, since plenty of tests have come to rely on the exact current implementations of isEqualTo!)

We don't have to be principled: We could still check things opportunistically. But that will encourage people to depend on those checks, and those checks might stop working (or at least switch to working in a different set of cases than they used to), and then people may silently lose test coverage. (This has happened already!)

So we just call equals testing out of scope. That at least lets us give clear advice to users and place no further burdens on authors of custom Subject implementations.

copybara-service bot pushed a commit to google/guava that referenced this issue Jul 3, 2024
This addresses some https://errorprone.info/bugpattern/JUnit3FloatingPointComparisonWithoutDelta warnings, and it moves us off the bug-prone 3-`double`-arg `assertEquals` overload.

In some cases, it results in a slightly awkward mix of JUnit and Truth assertions. I think it's still worthwhile for the benefits, but obviously we can consider migrating to even more Truth assertions in the future. (We would have to be a little careful about mass migration of arbitrary assertions, since migration might [replace `assertEquals` with `isEqualTo` for testing `equals` implementations](google/truth#268), etc. But we could look at migrating, say, all equality assertions on `int` and similar types—assuming that we never use those assertions to differentiate between, say, `Integer(0)` and `Long(0)`, which Truth treats as equivalent....)

PiperOrigin-RevId: 648713259
copybara-service bot pushed a commit to google/guava that referenced this issue Jul 3, 2024
This addresses some https://errorprone.info/bugpattern/JUnit3FloatingPointComparisonWithoutDelta warnings, and it moves us off the bug-prone 3-`double`-arg `assertEquals` overload.

In some cases, it results in a slightly awkward mix of JUnit and Truth assertions. I think it's still worthwhile for the benefits, but obviously we can consider migrating to even more Truth assertions in the future. (We would have to be a little careful about mass migration of arbitrary assertions, since migration might [replace `assertEquals` with `isEqualTo` for testing `equals` implementations](google/truth#268), etc. But we could look at migrating, say, all equality assertions on `int` and similar types—assuming that we never use those assertions to differentiate between, say, `Integer(0)` and `Long(0)`, which Truth treats as equivalent....)

PiperOrigin-RevId: 649135877
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
P3 not scheduled type=documentation Documentation that is other than for an API
Projects
None yet
Development

No branches or pull requests

7 participants