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

EDGFQM-8 Add controller pass-through test #9

Merged
merged 1 commit into from
Sep 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,12 @@
<version>${edge-common.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.flextrade.jfixture</groupId>
<artifactId>jfixture</artifactId>
<version>2.7.2</version>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package org.folio.fqm.edge.controller;

import com.flextrade.jfixture.JFixture;
import org.folio.fqm.edge.client.EntityTypesClient;
import org.folio.fqm.edge.client.QueryClient;
import org.folio.fqm.edge.service.EntityTypesService;
import org.folio.fqm.edge.service.QueryService;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.boot.test.mock.mockito.SpyBean;

import java.lang.reflect.InvocationTargetException;
import java.util.Set;
import java.util.stream.Stream;

import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;

@SpringBootTest
class EndpointPassThroughTest {

@Autowired
private EntityTypesController entityTypesController;
@SpyBean
private EntityTypesService entityTypesService;
@MockBean
private EntityTypesClient entityTypesClient;

@Autowired
private QueryController queryController;
@SpyBean
private QueryService queryService;
@MockBean
private QueryClient queryClient;

@BeforeEach
void setUp() {
}

@Test
void testEntityTypesPassThrough() throws InvocationTargetException, NoSuchMethodException, IllegalAccessException {
// Skip getRequests() in the entity type controller, since it's only there to resolve a name conflict in its
// two parent interfaces
verifyPassThrough(entityTypesController, entityTypesService, entityTypesClient, "getRequest");
}

@Test
void testQueryPassThrough() throws InvocationTargetException, NoSuchMethodException, IllegalAccessException {
verifyPassThrough(queryController, queryService, queryClient);
}

/**
* Verify that all methods in controller pass straight through to the service and client.
* <p>
* Note: There's a build-in assumption here that the controller, service, and client have the same basic interface; as
* in, if a method exists in the controller, then both the corresponding service and client should have methods with the
* same signature.
*
* @param controller - The controller to verify
* @param service - The backing service for the controller
* @param client - The underlying client that the service uses to retrieve data
* @param methodsToIgnore - Method names to skip
*/
private <C, S, R> void verifyPassThrough(C controller, S service, R client, String... methodsToIgnore) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
var ignoredMethods = Set.of(methodsToIgnore);
for (var controllerMethod : controller.getClass().getMethods()) {
// Skip any non - overridden inherited methods, along with any explicitly skipped ones
if (!controllerMethod.getDeclaringClass().equals(controller.getClass()) || ignoredMethods.contains(controllerMethod.getName())) {
continue;
}
// Given a controller, service, and client method
var serviceMethod = service.getClass().getMethod(controllerMethod.getName(), controllerMethod.getParameterTypes());
var clientMethod = client.getClass().getMethod(controllerMethod.getName(), controllerMethod.getParameterTypes());

// And given a set of random method arguments for the controller method
JFixture fixture = new JFixture();
var args = Stream.of(controllerMethod.getParameterTypes()).map(fixture::create).toArray();
Comment on lines +79 to +80
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I usually really dislike JFixture (I usually prefer something like junit-quickcheck instead for randomized testing), but for something like this, where it's not truly randomized testing and we just need some non-null objects of the right types (non-null, so that we can verify that they can be differentiated), it's pretty sweet


// When we call the controller method with those arguments
controllerMethod.invoke(controller, args);

// Then the service and client methods should get called with the same arguments exactly once
serviceMethod.invoke(verify(service, times(1)), args);
clientMethod.invoke(verify(client, times(1)), args);
Comment on lines +86 to +87
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These took me a while to figure out, since the normal verify(entityTypesClient, times(1)).doSomething() way of using verify() is already pretty magic. Doing it when reflection is involved makes it extra weird and look backwards

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking the same thing when looking at it!

}
}
}