0

Testing FnProject Functions illustrates how to use the FnTestingRule to simulate function invocations and interrogate their results. That part is working as expected. However, I am also trying to use Mockito to verify that the function called a certain collaborator with certain arguments during the invocation; in fact there are quite a few tuples T of invocation requests and collaborator expectations that I want to test identically. I cannot figure out a good approach.

The issue is that the pattern for composing an FnProject unit test is to supply the function class to the framework (a JUnit @Rule), and the framework instantiates that class, via reflection, using the default no-arg constructor, in a proprietary classloader. So for each T, I have to somehow communicate either the mock collaborator or T between the test harness and the ephemeral function instance (across classloaders), and I have to do it without recourse to anything but a no-arg constructor.

I couldn't see any seam for this purpose using FnTestingRuleFeature.

So far the best, but still pretty ugly, approach that has worked has been to create a nested wrapper class around a static final mock collaborator and share the wrapper class with FnTestingRule::addSharedClass during each test. If I tried creating the static mock collaborator at the testing class level, I ended up having to share way more classes with the FnTestingRule to get it working. And I never figured out what to share in order to get an AtomicReference wrapper to work instead of a custom one.

@RunWith( Parameterized.class )
public class TestHarness {
    public static class MockCollaboratorWrapper {
        // a class level mock that has to be reset between tests is a
        // nauseating code smell; but how else to convey between a
        // reflection-instantiated TestableFunction (for actual invocations)
        // and the harness (for calls to verify)?
        private static final Collaborator MOCK_COLLABORATOR = Mockito.mock( Collaborator.class );
        public Collaborator getCollaborator() { return MOCK_COLLABORATOR; }
    }

    // this class is what we pass to the FnTestingRule so that we can
    // inject a mock collaborator despite reflection calling no-arg constructor
    private static class TestableFunction extends MyFunction {
        public TestableFunction() {
            // protected constructor for testing, allows parameters;
            // this type of testing seam in MyFunction smells bad too
            super( MockCollaboratorWrapper.getCollaborator() );
        }
    }

    @Parameters
    public static Iterable< TestCaseTuple > cases() {
        return List.of(
            // all the test case combinations go here...
        );
    }

    // JUnit runner runs one test for each of the above cases
    // injecting the particular TestCaseTuple into this field
    @Parameter
    public TestCaseTuple tctEach;

    @Rule
    FnTestingRule testing = FnTestingRule.createDefault();

    @Test
    public void runOneTest() {
        // prepare static mock for this test, ick
        Mockito.reset( MockCollaboratorWrapper.getCollaborator() );
        Mockito.doReturn( tctEach.getCollabResult() ).
            when( MockCollaboratorWrapper.getCollaborator() ).myMethod( Mockito.any() );

        // this is the evil magic which shares the mock instance
        // across the ClassLoader chasm
        testing.addSharedClass( MockCollaboratorWrapper.class );

        // run the test as shown at FnProject docs
        testing.givenEvent().
            withHeader( "Fn-Http-Request-Uri", tctEach.getURL() ).
            // ...
            enqueue();
        testing.thenRun( TestableFunction.class, "handleRequest" );
        FnResult result = testing.getOnlyResult();

        // check status code
        assertEquals(
            Integer.valueOf( tctEach.getExpectedStatus() ),
            result.getHeaders().get( "Fn-Http-Status" ).
                map( Integer::valueOf ).
                orElseGet( result.getStatus()::getCode )
        );
        // check other things about the result...

        // check the interaction with the collaborator
        // we could set up the stubbing to verify this directly, but then the
        // failure would happen inside the function invocation and get
        // translated into console output and a 502 result code
        Mockito.verify( MockCollaboratorWrapper.getCollaborator() ).
            myMethod( tctEach.getCollabArg() );
    }
}

Surely there is a better way? What say you, infinite-loopers?

Judge Mental
  • 5,209
  • 17
  • 22

0 Answers0