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?