How do I mock a Session in Ratpack with RequestFix

2019-08-04 04:52发布

问题:

What I'm trying to do is test an authentication handler, but my problem boils down to having no Session instance in the registry.

An example test:

package whatever

import groovy.transform.CompileStatic
import ratpack.groovy.handling.GroovyChainAction
import ratpack.groovy.test.handling.GroovyRequestFixture
import ratpack.http.Status
import ratpack.session.Session
import spock.lang.Specification

class SessionChainTest extends Specification {
    GroovyChainAction sessionChain = new GroovyChainAction() {
        @Override
        @CompileStatic
        void execute() throws Exception {
            get('foo') {
                Session s = get Session
                // Stuff using session here
            }
        }
    }

    def "should get session"() {
        given:
        def result = GroovyRequestFixture.handle(sessionChain) {
            uri 'foo'
            method 'GET'
        }

        expect:
        result.status == Status.OK

        // If the server threw, rethrow that
        Throwable t = result.exception(Throwable)
        if (t) throw t // <<< Throws NotInRegistryException because no Session in registry
    }

}

(The extra rethrow is in there to allow us to see the exception thrown within the ratpack test, because by default it is caught and stashed in the result.)

I know that in principle I could create a Session instance and add it to the registry with a registry { add <Session instance> } block, but I've delved into the Ratpack code, and creating a Session object requires getting a lot of disparate other components and passing them to SessionModule#sessionAdaptor (or the DefaultSession constructor). I can't find any examples of that being done, it appears this call is handled by Guice dependency-injection magic I can't unpick.

The usual way to do it in an application is to use a bind { module SessionModule } block but this isn't accessible from the context of RequestFixture#execute.

As sessions are bread and butter for any web application, my hunch is that this may be an easily solved problem, I just haven't found the right way to do it?

回答1:

You can access Registry through GroovyRequestFixture.handle(handler, closure) method call and you can e.g. register mocked Session object:

GroovyRequestFixture.handle(sessionChain) {
    uri 'foo'
    method 'GET'
    registry { r ->
        r.add(Session, session)
    }
}

Take a look at following example:

import groovy.transform.CompileStatic
import ratpack.exec.Promise
import ratpack.groovy.handling.GroovyChainAction
import ratpack.groovy.test.handling.GroovyRequestFixture
import ratpack.http.Status
import ratpack.jackson.internal.DefaultJsonRender
import ratpack.session.Session
import spock.lang.Specification

import static ratpack.jackson.Jackson.json

class SessionChainTest extends Specification {

    Session session = Mock(Session) {
        get('test') >> Promise.value(Optional.of('Lorem ipsum'))
    }

    GroovyChainAction sessionChain = new GroovyChainAction() {
        @Override
        @CompileStatic
        void execute() throws Exception {
            get('foo') {
                Session s = get Session

                s.get('test').map { Optional<String> o ->
                    o.orElse(null)
                }.flatMap { value ->
                    Promise.value(value)
                }.then {
                    render(json([message: it]))
                }
            }
        }
    }

    def "should get session"() {
        given:
        def result = GroovyRequestFixture.handle(sessionChain) {
            uri 'foo'
            method 'GET'
            registry { r ->
                r.add(Session, session)
            }
        }

        expect:
        result.status == Status.OK

        and:
        result.rendered(DefaultJsonRender).object == [message: 'Lorem ipsum']

    }
}

In this test I mock Session object for key test to store Lorem ipsum text. When running this test, both assertions pass.

Alternative approach: registering Guice.registry()

If you don't want to use mocked Session object you can try replacing default Ratpack's Registry with a Guice registry object. Firstly, initialize a function that creates Guice registry and add SessionModule via bindings:

static Function<Registry, Registry> guiceRegistry = Guice.registry { bindings ->
    bindings.module(new SessionModule())
}

Next inside execute() method of GroovyChainAction you can replace the default registry by calling:

register(guiceRegistry.apply(registry))

No mocks anymore, but in this case you can't access Session object outside request scope, so you wont be able to add anything to the session in preparation stage of your test. Below you can find full example:

import groovy.transform.CompileStatic
import ratpack.exec.Promise
import ratpack.func.Function
import ratpack.groovy.handling.GroovyChainAction
import ratpack.groovy.test.handling.GroovyRequestFixture
import ratpack.guice.Guice
import ratpack.http.Status
import ratpack.jackson.internal.DefaultJsonRender
import ratpack.registry.Registry
import ratpack.session.Session
import ratpack.session.SessionModule
import spock.lang.Specification

import static ratpack.jackson.Jackson.json

class SessionChainTest extends Specification {

    static Function<Registry, Registry> guiceRegistry = Guice.registry { bindings ->
        bindings.module(new SessionModule())
    }

    GroovyChainAction sessionChain = new GroovyChainAction() {
        @Override
        @CompileStatic
        void execute() throws Exception {
            register(guiceRegistry.apply(registry))

            get('foo') {
                Session s = get Session

                s.get('test').map { Optional<String> o ->
                    o.orElse(null)
                }.flatMap { value ->
                    Promise.value(value)
                }.then {
                    render(json([message: it]))
                }
            }
        }
    }

    def "should get session"() {
        given:
        def result = GroovyRequestFixture.handle(sessionChain) {
            uri 'foo'
            method 'GET'
        }

        expect:
        result.status == Status.OK

        and:
        result.rendered(DefaultJsonRender).object == [message: null]

    }
}

Hope it helps.