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

Riptide Client Testing #301

Merged
merged 6 commits into from Nov 28, 2017
Merged

Riptide Client Testing #301

merged 6 commits into from Nov 28, 2017

Conversation

lukasniemeier-zalando
Copy link
Member

@lukasniemeier-zalando lukasniemeier-zalando commented Nov 24, 2017

Introducing @RiptideClientTest - a new spring-boot meta-annotation to support testing riptide-spring-boot-starter generated Riptide Http clients.

Currently testing is a bit tedious and repetitive. It can be shorter:

@Component
public class GatewayService {

    @Autowired
    @Qualifier("example")
    private Http http;

    void remoteCall() {
        http.get("/bar").dispatch(status(), on(OK).call(pass())).join();
    }
}

@RunWith(SpringRunner.class)
@RiptideClientTest(GatewayService.class)
public class RiptideTest {

    @Autowired
    private GatewayService client;

    @Autowired
    private MockRestServiceServer server;

    @Test
    public void shouldAutowireMockedHttp() throws Exception {
        server.expect(requestTo("https://example.com/bar")).andRespond(withSuccess());
        client.remoteCall()
        server.verify();
    }
}

@RiptideClientTest is based on @RestClientTest.

Currently @RiptideClientTest and its facilities are tied to riptide-spring-boot-starter. In order to allow usage with riptide-core only, it may make sense to extract the components to a dedicated artifact. Instead of modifying the RiptideRegistar directly, one would need to make sure that properly named AsyncClientHttpRequestFactory are registered beforehand (expose Registry#generateBeanName?). Added benefit is that we wouldn't need any provided testing dependencies anymore. On the other hand I am not sure whether anybody wants to / needs to use this without the starter...

public MockRestServiceServer mockRestServiceServer(final RequestExpectationManager manager) throws Exception {
final Constructor<MockRestServiceServer> ctr = MockRestServiceServer.class.getDeclaredConstructor(RequestExpectationManager.class);
ctr.setAccessible(true);
return ctr.newInstance(manager);
Copy link
Member Author

Choose a reason for hiding this comment

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

Hello Darkness My Old Friend

@lukasniemeier-zalando lukasniemeier-zalando changed the title Introduce @RiptideClientTest to support testing of starter generated … Riptide Client Testing Nov 24, 2017
@@ -23,6 +23,11 @@
private Defaults defaults = new Defaults();
private GlobalOAuth oauth = new GlobalOAuth();
private Map<String, Client> clients = new LinkedHashMap<>();
private Boolean mocked = false;

public void setMocked(final Boolean mocked) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

You should get this for free.

Copy link
Member Author

Choose a reason for hiding this comment

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

Top Level of the Settings currently only has setters generated.

Copy link
Collaborator

Choose a reason for hiding this comment

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

You mean getters, right? Isn't the constructor enough?

Copy link
Member Author

Choose a reason for hiding this comment

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

I did not get it working without this setter somehow. He does not find a proper "writable" property path.

@@ -23,6 +23,11 @@
private Defaults defaults = new Defaults();
private GlobalOAuth oauth = new GlobalOAuth();
private Map<String, Client> clients = new LinkedHashMap<>();
private Boolean mocked = false;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why not boolean?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Putting this here technically allows users to do this:

riptide:
  mocked: true

Copy link
Member Author

Choose a reason for hiding this comment

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

We can unbox - no particular reason to use Boolean

*
* @return the components to test
*/
@AliasFor(annotation = RestClientTest.class, attribute = "value")
Copy link
Collaborator

Choose a reason for hiding this comment

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

Any other attributes that are worth having as an alias?

Copy link
Member Author

Choose a reason for hiding this comment

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

I haven't used any other on @RestClientTest, but we may decided to just forward all of it?

@RestClientTest
@AutoConfigureMockRestServiceServer(enabled = false) // see RiptideClientAutoConfiguration
@TestPropertySource(properties = {
"riptide.mocked: true"
Copy link
Collaborator

Choose a reason for hiding this comment

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

There it is.

Copy link
Member Author

Choose a reason for hiding this comment

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

Also referencing your comment above: any other method to inject the "mock mode" is obviously fine - this has been just the fastest way 😁

import java.io.IOException;
import java.net.URI;

public class ExpectingHttpRequestFactory implements AsyncClientHttpRequestFactory, ClientHttpRequestFactory {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can't we use the existing implementation?

Copy link
Member Author

Choose a reason for hiding this comment

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

I think it is private. Yeah I know I am already using the private constructor of the server, but this guy here is 10 straight forward lines of code

@whiskeysierra
Copy link
Collaborator

Summary of this PR contains RiptideGatewayTest rather than RiptideClientTest.

@whiskeysierra
Copy link
Collaborator

Instead of modifying the RiptideRegistar directly, one would need to make sure that properly named AsyncClientHttpRequestFactory are registered beforehand (expose Registry#generateBeanName?). Added benefit is that we wouldn't need any provided testing dependencies anymore. On the other hand I am not sure whether anybody wants to / needs to use this without the starter...

I think that would be cleaner, even without the dedicated artifact. Leaving the Registrar purely for production would be easier to understand for future changes.

@jhorstmann
Copy link
Contributor

Are OAuth access tokens also mocked? Would be nice to avoid any external calls in the tests.

@whiskeysierra
Copy link
Collaborator

Are OAuth access tokens also mocked? Would be nice to avoid any external calls in the tests.

Not explicitly. The AccessTokens instance will try to generate tokens, but they will never be used, since the interceptor that would inject them is not being used (it's for the real Apache HTTP client).

@whiskeysierra
Copy link
Collaborator

The test setup could just register a AccessTokens bean that does nothing.

@lukasniemeier-zalando
Copy link
Member Author

I've just pushed an alternative implementation that relies on the original trick from the README and does not use a property to modify the behavior of the RiptideRegistar.

The example above changes slightly, as there have to be one mocked server per client:

@Component
public class GatewayService {

    @Autowired
    @Qualifier("example")
    private Http http;

    void remoteCall() {
        http.get("/bar").dispatch(status(), on(OK).call(pass())).join();
    }
}

@RunWith(SpringRunner.class)
@RiptideClientTest(GatewayService.class)
public class RiptideTest {

    @Autowired
    private GatewayService client;

    @Autowired
    @Qualifier("example") // this is now needed
    private MockRestServiceServer server;

    @Test
    public void shouldAutowireMockedHttp() throws Exception {
        server.expect(requestTo("https://example.com/bar")).andRespond(withSuccess());
        client.remoteCall()
        server.verify();
    }
}

One important (?) thing to note is that the RestTemplate won't be redirected to the mock request factory. In order to do so we'd have to register another dedicated MockRestServiceServer - I guess this is not worth it as then we'd have multiple MockRestServiceServer beans with the same client qualifier.

@lukasniemeier-zalando
Copy link
Member Author

The test setup could just register a AccessTokens bean that does nothing.

👍

@lukasniemeier-zalando
Copy link
Member Author

The test setup could just register a AccessTokens bean that does nothing.

👍

Actually this is not needed. The AccessTokens integration works via interceptors of the HttpClient of the request factory. And the latter we mock.

}

private String registerAsyncRestTemplate(final String id) {
return registry.registerIfAbsent(id, AsyncRestTemplate.class, () -> {
Copy link
Collaborator

Choose a reason for hiding this comment

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

That will incorrectly replace the AsyncRestTemplate that we would register in the DefaultRegistrar. It would miss plugins as well as the base url.

Copy link
Member Author

Choose a reason for hiding this comment

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

Fixed

});
}

private String registerMockRestServiceServer(final String id, final String templateId) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why don't we register this once as a singleton?

Copy link
Collaborator

Choose a reason for hiding this comment

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

... and share it across clients?

Copy link
Member Author

Choose a reason for hiding this comment

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

Hm I remember that we talked about having this as a singleton vs. per-client and somehow I was under the impression that I gave in to have it per-client 😬 I believe the singleton route is more intuitive either way!

@Autowired
public TestService(@Qualifier("example") final Http http, @Qualifier("example") final RestTemplate restTemplate) {
this.http = http;
this.restTemplate = restTemplate;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Not used?

Copy link
Member Author

Choose a reason for hiding this comment

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

Fixed

@whiskeysierra
Copy link
Collaborator

Actually this is not needed. The AccessTokens integration works via interceptors of the HttpClient of the request factory. And the latter we mock.

Access tokens are never used, but the default implementation will spawn a thread and poll tokens in the background. That could be disabled.

@Documented
@Inherited
@RestClientTest
@AutoConfigureMockRestServiceServer(enabled = false) // will be registered per client
Copy link
Collaborator

Choose a reason for hiding this comment

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

Comment is out-dated. Can you describe here why we need this?

Copy link
Member Author

Choose a reason for hiding this comment

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

Ups will do

settings.getClients().forEach((id, client) -> registerAsyncClientHttpRequestFactory(id, templateId, serverId));
}

private String registerMockAsyncRestTemplate() {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Couldn't you register the template and the server using traditional @Bean methods in the RiptideTestAutoConfiguration?

Copy link
Member Author

Choose a reason for hiding this comment

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

Technically yes, but I haven't found a cool way to still reference these beans in here on registering the request factories. Just by some explicit bean name? Any idea? I feel those 2 + (1 * n) beans should go together.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Explicit bean name should work. I'd argue that the configuration binds those 2+n beans together. The Registrar is just our way to dynamically register the n beans.

}

@Bean(name = SERVER_BEAN_NAME)
MockRestServiceServer mockRestServiceServer(@Qualifier("_mockAsyncRestTemplate") final AsyncRestTemplate template) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

The qualifier could use the TEMPLATE_BEAN_NAME.

Copy link
Member Author

Choose a reason for hiding this comment

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

Ups forgot to replace

private final Registry registry;
private final RiptideSettings settings;

private final String asyncRestTemplateName;
Copy link
Collaborator

Choose a reason for hiding this comment

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

I'd be fine with accessing the same constant.

@whiskeysierra
Copy link
Collaborator

👍

@whiskeysierra whiskeysierra merged commit aca9f62 into master Nov 28, 2017
@whiskeysierra whiskeysierra deleted the gateway-testing branch November 28, 2017 14:05
@lukasniemeier-zalando
Copy link
Member Author

In a follow-up I will add a paragraph to the READMEs

@RohanNg
Copy link

RohanNg commented Jun 17, 2019

Hi,

We had a use case where in remoteCall multiple requests are dispatched to a http endpoint. We have a problem writing test because the ordering of the http call cannot be relaxed, while it is desirable to ignore the ordering.

This could be probably be done by MockRestServiceServer.bindTo(restTemplate).ignoreExpectOrder(true).build(). However we don't know how to get the produced server instance into use.

Could you provide some hint how to do that?

@whiskeysierra
Copy link
Collaborator

No easy way to do that right now. Options that I see:

  1. Use reflection to manipulate the expectationManager and replace it with an UnorderedRequestExpectationManager
  2. Open a pull request and introduce an option (property?) to change the behavior of this line:
  3. Change the line mentioned above, introduce a @ConditionalOnBean(MockRestServiceServer.class) and then override it in your auto configuration. Properly the cleanest solution: ✔️

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants