google/truth

JUnit5 support for the existing soft-assertions support (Expect.create JUnit4 Rule)

astubbs opened this issue · 5 comments

From a purely JUnit5 perspective, am I missing something?

I made this wrapper around the core Expect function:

    public class ThingyThing {
        public static void softly(final SoftAssertions softly) throws Throwable {
            Expect expect = Expect.create();
            expect.apply(new Statement() {
                @Override
                public void evaluate() throws Throwable {
                        softly.apply(expect);
                }
            }, Description.EMPTY)
                    .evaluate();
        }

        public interface SoftAssertions{
            void apply(final Expect expect);
        }
    }

Used like:

        ThingyThing.softly(expector -> {
            expector.that(1).isEqualTo(2);
            expector.that(-1).isGreaterThan(0);
        });

Outputs:

2 expectations failed:
  1. expected: 2
     but was : 1
     	at io.<snip>.ThingTest.lambda$testRemit$0(ThingTest.java:113)
     
  2. expected to be greater than: 0
     but was                    : -1
     	at io.<snip>.ThingTest.lambda$testRemit$0(ThingTest.java:114)

P.s. amazing framework BTW - beautiful. Especially for chained custom objects. Amazing. Wish I'd found it sooner.

Thanks. As you've seen, JUnit 5 is a giant TODO for us, one that we're unlikely to carve off time for in the near future :(

I can say one slightly encouraging thing, which is that your proposal looks useful even to non-JUnit-5 users. (Maybe we've even heard requests for it from such users? I can't seem to find any offhand, though.) Then I can say one more discouraging thing, which is that we have had a proposal open inside Google for something like this for a couple years, and we haven't acted on it. Now, that proposal was more about adding context to a group of assertions:

try (TruthContext ctx = Truth.addFailureMessage("foo %s", bar)) {
  ...
}

If we take into account that such a feature could support all of...

  • context to add to failure messages
  • Expect-style failure collection for...
    • users of JUnit 5 (and other non-JUnit-4 frameworks)
    • users of any framework who simply want to group assertions at a smaller scope than a whole test

...then that provides at least a little more motivation.

Still probably not a big priority for us, given that Truth itself continues not to be a big priority for us, sadly.

I created PR #706 over a year ago which enables

@Test
public void test(StandardSubjectBuilder expect) {
  expect.that(1).isEqualTo(2);
  expect.that(-1).isGreaterThan(0);
}

in JUnit 5. I think that makes sense if you're actually using JUnit 5, but if you want to use it in any test framework, then your ThingyThing seems reasonable.

@ephemient that seems great! Perhaps the two could be merged to enable both options...

@cpovirk have you seen #706 ?

My solution Inspired by ThingyThing.

class ExpectExtension : BeforeEachCallback, AfterEachCallback, InvocationInterceptor,
                        ParameterResolver {

    private var _expect: Expect? = null

    val expect: Expect
        get() = _expect!!

    override fun beforeEach(context: ExtensionContext) {
        _expect = Expect.create()
    }

    override fun interceptTestMethod(
        invocation: InvocationInterceptor.Invocation<Void>,
        invocationContext: ReflectiveInvocationContext<Method>,
        extensionContext: ExtensionContext
    ) {
        _expect?.apply(
            object : Statement() {
                override fun evaluate() {
                    invocation.proceed()
                }
            },
            Description.EMPTY,
        )?.evaluate()
    }

    override fun afterEach(context: ExtensionContext) {
        _expect = null
    }

    override fun supportsParameter(
        parameterContext: ParameterContext,
        extensionContext: ExtensionContext
    ): Boolean {
        val paramType = parameterContext.parameter.type
        return paramType is Type && paramType == Expect::class.java
    }

    override fun resolveParameter(
        parameterContext: ParameterContext,
        extensionContext: ExtensionContext
    ): Any {
        return expect
    }
}

A.

@Test
@ExtendWith(ExpectExtension::class)
fun test(expect: Expect) {
    expect.that<Int>(1).isEqualTo(2)
    expect.that<Int>(1).isEqualTo(3)
    expect.that<Int>(1).isEqualTo(4)
}

B.

@JvmField
@RegisterExtension
val extension = ExpectExtension()

@Test
fun test2() {
    extension.expect.that<Int>(1).isEqualTo(2)
    extension.expect.that<Int>(1).isEqualTo(3)
    extension.expect.that<Int>(1).isEqualTo(4)
}

@Test
fun test3(expect: Expect) {
    expect.that<Int>(1).isEqualTo(2)
    expect.that<Int>(1).isEqualTo(3)
    expect.that<Int>(1).isEqualTo(4)
}

I look forward to any form of Expect Extension for JUnit5 being added in the future!

As belatedly noted on #894, anything that we label as P3 has no timeline for being reviewed :( We hope to eventually schedule some more time for Truth (especially for features that could work well in Kotlin, like this one), but there are no plans yet.

(Another thing we should do: Figure out if this feature request and #266 represent different requests or if they could be merged.)