Junit4: Intruduce BiMatcher hierarchy

Created on 8 Feb 2018  ·  9Comments  ·  Source: junit-team/junit4

A good feature would be introducing BiMatcher hierarchy (analog of org.hamcrest.Matcher, but consuming 2 args). Alternatively standard Comparator can be used for this purpose. Also BiPredicate can be used (this will tie JUnit to Java-8, but will add out-of-the-box AND/OR/NOT logic).
A couple of notes for all further code:

  • BiMatcher, BiPredicate and Comparator names will be used interchangeably.
  • All the code is not final and just a general idea.

Main methods to add in Assert class:

  • assertThat(T expected, T actual, Comparator<T> biMatcher);
  • assertThat(String message, T expected, T actual, Comparator<T> biMatcher); (maybe not necessary, see below).
  • Multiple assertArray() methods (see below).

The motivation: sometimes you need to do a lot of similar checks against batch of objects. Existing functionality allows it, but with a lot of additional code (loops and conditions). With Comparator it can be done with less code and with higher flexibility.

Real example (from which I actually started to think about this feature): need to test all objects in the array are the same or not the same (depending on a boolean flag).

With current JUnit abilities it will look like:
shorter:

for (int i = 0; i < elements.length - 1; i++) {
  if (checkSame) { // Same
    assertSame("elements must be the same.", elements[i], elements[i + 1]);
  } else { // Not same
    assertNotSame("elements must be not the same.", elements[i], elements[i + 1]);
  }
}

better performance (but who cares about performance in tests):

if (checkSame) { // Same
  for (int i = 0; i < elements.length - 1; i++) {
    assertSame("elements must be the same.", elements[i], elements[i + 1]);
  }
else { // Not same
  for (int i = 0; i < elements.length - 1; i++) {
    assertNotSame("elements must be not the same.", elements[i], elements[i + 1]);
  }
}

Also we can implement own Comparator and use assertTrue()/assertFalse(), but the code for creating comparators will also be big (unless we use lambdas).

With new approach code will look much shorter and cleaner as for me, like this:

Comparator<T> comparator = checkSame ? BiMatchers.same() : BiMatchers.notSame();
String message = checkSame ? "elements must be the same." : "elements must be not the same.";
for (int i = 0; i < elements.length - 1; i++) {
  assertThat(message, elements[i], elements[i + 1], comparator);
}

To make it even shorter, we can extend Comparator to MessagedComparator with optional message property, so JUnit will take it from there somehow. Like this:

MessagedComparator<T> comparator = checkSame ? BiMatchers.same("elements must be the same.") : BiMatchers.notSame("elements must be not the same.");
for (int i = 0; i < elements.length - 1; i++) {
  assertThat(message, elements[i], elements[i + 1], comparator);
}

Consequences are very far-reaching:
1) The Assert class will be very flexible in general, because you can use any standard or own comparator without much code.
2) As a result of previous, lambdas can be used more easily to inject any conditions.
3) Additional logic cascading (AND, OR, NOT) can be added out-of-the-box (something similar to org.hamcrest.CoreMatchers). Currently for any new mini-feature a new method required, e.g. assertNotSame() next to assertSame() etc. Or again we need assertTrue()/assertFalse() methods.
4) The whole Assert class can be refactored to unified form. Just one example:

public static void assertSame(String message, Object expected, Object actual) {
  assertThat(message, expected, actual, BiMatchers.same())
}

public static void assertThat(String message, T expected, T actual, Comparator<T> biMatcher) {
...
}

or as already mentioned:

public static void assertSame(String message, Object expected, Object actual) {
  assertThat(expected, actual, BiMatchers.same(message))
}

public static void assertThat(T expected, T actual, MessagedComparator<T> biMatcher) {
...
}

5) assertArray() methods can be added (thus old methods refactored). E.g.:

public static void assertArrayEquals(boolean[] expecteds, boolean[] actuals) {
  assertArray(expecteds, actuals, BiMatchers.equals());
}

public static void assertArray(boolean[] expecteds, boolean[] actuals, Comparator<Boolean>) {
...
}

So for example you can check all elements in 2 arrays are same as easy as:

Assert.assertArray(array1, array2, BiMatchers.same());

Also array-length or other checks can be controlled separately. Pseudocode:

Assert.assertArray(array1, array2, and(sameLength(), same()));

All 9 comments

Thanks for raising this issue.

I would prefer that JUnit 4.x not add more assertion DSLs. It's really hard to get a DSL correct and flexible, and I am not sure it makes sense to spend time and effort on that in JUnit when there are good third-party assertion frameworks like Truth or Fest.

Also, features requiring Java 8 should go to http://github.com/junit-team/junit5

@gitIvanB Thanks for opening the issue. Like @kcooney, I also think this would best be addressed by an assertion library, e.g. AssertJ.

@kcooney , @marcphilipp , thanks for hinting. Looks like usage of assertion libs is a best practice here.
One additional question. Can I use as a rule of thumb that usage of Assert is deprecated (or not advised)? Especially keeping in mind future possible upgrade from JUnit-4 to JUnit-5.

@gitIvanB The question is a bit not clear. If developing against junit4 one uses org.junit.Assert, if developing against junit5 then org.junit.jupiter.api.Assertions

The recommended approach is statically importing methods from it, to avoid having Assert. on every line.

@gitIvanB You should definitely not use junit.framework.Assert, but org.junit.Assert is fine. When you do upgrade, you can use an IDE to convert JUnit 4 to JUnit Jupiter assertions (IDEA supports it already). The biggest difference is that the optional message parameter comes first in JUnit 4 but last in Jupiter. Alternatively, you can decide to use a different assertion library now and keep using that when migrating from Vintage to Jupiter.

@panchenko , @marcphilipp So as I understood JUnit-4's org.junit.Assert is sealed and will not be enriched with new methods. In JUnit-5 I need to use either org.junit.jupiter.api.Assertions (as a replacement of org.junit.Assert) or some assertion lib.
I need some time to learn JUnit-5, but from first glance into org.junit.jupiter.api.Assertions looks like its approach in general is similar to org.junit.Assert. So it is a big chance that I need to create similar ticket for JUnit-5 :) I believe JUnit-5 should either provide fully-fledged assertions or fully delegate to assertion libs.

@gitIvanB JUnit's philosophy is to provide a good, extensible framework for writing tests for Java in Java. Being an extensible framework, we often see other open source projects extend the framework and/or provide rich testing libraries that can be used with JUnit.

For assertions, there are several projects that provide a richer set of assertions, sometimes in a way that is extensible.
For example, there is Google Truth (https://github.com/google/truth), FEST (https://github.com/alexruiz/fest-assert-2.x), AssertJ (http://joel-costigliola.github.io/assertj/) and Hamcrest (http://hamcrest.org/JavaHamcrest/).

Because there are so many amazing assertion libraries out there I have been reluctant to accept new feature requests for org.junit.Assert. I wouldn't consider it frozen (though I would consider junit.framework.Assert frozen). In fact, 4.13 will introduce assertThrows. But since we will never have as rich a set of assertions as those projects we feel comfortable providing the basics.

Put another way, heathy communities have formed around the use and maintenance of those projects, so I think we should embrace and support them.

I can't speak for JUnit5, but I imagine the original developers felt they couldn't release the next generation of JUnit without a good foundation of assertion methods (or perhaps they felt like the best way to ensure that the new framework was functional and polished was to test JUnit5 using JUnit5). New programming practices caused them to make different decisions on what a basic assertion API would look like than Kent, Erich and David did, so you will see some differences. But you will likely never see in JUnit as rich a set of assertions as with Hamcrest or an extensible DSL like Truth.

I leave it to the JUnit5 team to decide what types of changes they will consider. If you feel strongly that the
methods you are proposing are important as a foundation of writing good tests feel free to create an issue there.

When we started work on JUnit 5 (codenamed "JUnit Lambda" at the time), we discussed the scope of what we wanted to achieve. Writing a new kind of Assertion library was explicitly decided not to be in scope.

Or, as the JUnit 5 User Guide puts it:

Even though the assertion facilities provided by JUnit Jupiter are sufficient for many testing scenarios, there are times when more power and additional functionality such as matchers are desired or required. In such cases, the JUnit team recommends the use of third-party assertion libraries such as AssertJ, Hamcrest, Truth, etc. Developers are therefore free to use the assertion library of their choice.

So, it's not frozen either, but the goal is similar as in JUnit 4, namely to provide a basic set of assertions to get users started. We even use AssertJ for our own unit tests.

Was this page helpful?
0 / 5 - 0 ratings