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

QuarkusComponentTest: offer a convenient way of mocking interceptors #34086

Closed
mkouba opened this issue Jun 16, 2023 · 5 comments · Fixed by #34239
Closed

QuarkusComponentTest: offer a convenient way of mocking interceptors #34086

mkouba opened this issue Jun 16, 2023 · 5 comments · Fixed by #34239
Labels
area/testing kind/enhancement New feature or request
Milestone

Comments

@mkouba
Copy link
Contributor

mkouba commented Jun 16, 2023

Description

Currently, if an interceptor binding is declared in a tested component and we would like to reflect the binding in the test, then an mock interceptor class must be registered as an additional component, e.g. something like:

@QuarkusComponentTest(SimpleInterceptor.class)
public class InterceptorMockingTest {

    @Inject
    TheComponent theComponent;

    @Test
    public void testPing() {
        assertEquals("ok", theComponent.ping());
    }

    @Singleton
    static class TheComponent {

        @SimpleBinding
        String ping() {
            return "true";
        }

    }
    @Priority(1)
    @SimpleBinding
    @Interceptor
    static class SimpleInterceptor {

        @AroundInvoke
        Object aroundInvoke(InvocationContext context) throws Exception {
            return "ok";
        }
    }
}

Moreover, this interceptor class may cause problems in regular @QuarkusTests, because all classes in src/test/java are discovered by default. So the %test.quarkus.arc.exclude-types config property would need to be used in order to exclude the interceptor class from discovery.

Implementation ideas

I was thinking that we could support "interceptor methods" declared direcly on the test class.

@QuarkusComponentTest
public class InterceptorMockingTest {

    @Inject
    TheComponent theComponent;

    @Test
    public void testPing() {
        assertEquals("ok", theComponent.ping());
    }

    @Singleton
    static class TheComponent {

        @SimpleBinding
        String ping() {
            return "true";
        }

    }

    @SimpleBinding
    @AroundInvoke
    Object aroundSimpleBinding(InvocationContext context) throws Exception {
            return "ok";
    }
}

That would of course require support of synthetic interceptors in ArC (not supported at the moment)

@mkouba mkouba added kind/enhancement New feature or request area/testing labels Jun 16, 2023
@mkouba
Copy link
Contributor Author

mkouba commented Jun 16, 2023

FYI @Ladicek @holly-cummins

@Ladicek
Copy link
Contributor

Ladicek commented Jun 16, 2023

A few alternative options, not saying any of them is better, just something that crossed my mind.

  1. If we actually followed the specification and left interceptors without @Priority disabled, the first example from the description above would actually work without nasty side effects if we had a way to enable components just for a single test. Something like:

    @QuarkusComponentTest
    @EnableForTest(SimpleInterceptor.class) // this annotation would apply to alternatives too
    public class InterceptorMockingTest {
        @Inject
        TheComponent theComponent;
    
        @Test
        public void testPing() {
            assertEquals("ok", theComponent.ping());
        }
    
        @Singleton
        static class TheComponent {
            @SimpleBinding
            String ping() {
                return "true";
            }
        }
    
        // note no `@Priority` here, which should mean disabled, but doesn't, so this example is unusable
        @SimpleBinding
        @Interceptor
        static class SimpleInterceptor {
            @AroundInvoke
            Object aroundInvoke(InvocationContext context) throws Exception {
                return "ok";
            }
        }
    }

    A possibly nice thing about this is, as I mention in the comment, that it would also apply to alternatives.

  2. We could still make something pretty close the first example from the description above work if we used a different mechanism for turning a class into interceptor. For example, we could add a @TestInterceptors annotation on the test class that turns given clases to interceptors, or even use @TestInterceptor instead of @Interceptor:

    @QuarkusComponentTest
    public class InterceptorMockingTest {
        @Inject
        TheComponent theComponent;
    
        @Test
        public void testPing() {
            assertEquals("ok", theComponent.ping());
        }
    
        @Singleton
        static class TheComponent {
            @SimpleBinding
            String ping() {
                return "true";
            }
        }
    
        @TestInterceptor // instead of `@Interceptor`
        @SimpleBinding
        // no `@Priority` here, but we default the priority for interceptors anyway
        static class SimpleInterceptor {
            @AroundInvoke
            Object aroundInvoke(InvocationContext context) throws Exception {
                return "ok";
            }
        }
    }

The reason why I'm so keen on enabling something like the first example is because it's closest to what you'd naturally write, and I think that's desirable.

@mkouba
Copy link
Contributor Author

mkouba commented Jun 16, 2023

If we actually followed the specification and left interceptors without @Priority disabled, the first example from the description above would actually work without nasty side effects if we had a way to enable components just for a single test.

Except that we don't. And IIRC it was changed based on a user request 🤷.

Another disadvantage of this approach is that you can't test multiple sorted interceptors...

For example, we could add a @TestInterceptors annotation on the test class that turns given clases to interceptors, or even use @TestInterceptor instead of @Interceptor..

This could work but (1) it's another new annotation and (2) I don't see a big benefit in declaring a dedicated class for each interceptor...

@mkouba
Copy link
Contributor Author

mkouba commented Jun 16, 2023

The reason why I'm so keen on enabling something like the first example is because it's closest to what you'd naturally write, and I think that's desirable.

You're right but it could be even more confusing, i.e. you can't use exactly the same code so it may happen that you forget to replace one annotation with another...

@Ladicek One thing we could do is to register all static nested interceptor classes declared on a QuarkusComponentTest test class as additional components and exclude these classes from discovery when running a @QuarkusTest. It's not perfect but covers a lot of use cases.

@Ladicek
Copy link
Contributor

Ladicek commented Jun 16, 2023

I actually thought making all classes nested in the test class part of the system under test would be really nice.

@QuarkusTest automatically discovering "test-scoped" classes is a blessing and a curse :-) I think we generally need a strategy for @QuarkusTest to exclude some "test-scoped" classes from its discovery. Classes nested in @QuarkusComponentTest are a natural candidate, but maybe we could do more than that?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area/testing kind/enhancement New feature or request
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants