robfletcher / grails-gson

Provides alternate JSON (de)serialization for Grails using Google's Gson library
Apache License 2.0
45 stars 34 forks source link

Controller unit tests fail when using the as GSON syntax #28

Closed gregopet closed 11 years ago

gregopet commented 11 years ago

When using the render xyz as GSON syntax, I cannot seem to be able to unit test these controllers. Running the unit tests (using Spock) I get the following exception: org.springframework.beans.factory.NoSuchBeanDefinitionException: No bean named 'gsonBuilder' is defined

I have defined the gsonBuilder bean using the defineBeans directive and in fact I can unit test the controller if I use the alternative syntax (injecting the gsonBuilder bean, creating a gson object and then calling .toJson(xyz) on it. As the as GSON syntax is shorter and more expressive, I would prefer it over the currently testable one.

In case there is a way to make unit tests work and I just don't know how, maybe the documentation could mention it?

robfletcher commented 11 years ago

I will need to implement some better support for this. Right now you need to do the defineBeans step in setupSpecnot setup. I'd like unit test support to be seamless so you don't need to register anything and as GSON just works.

gregopet commented 11 years ago

That would be great, thanks for all your work!

Quick question (I don't expect an answer unless it's obvious to you at first glance): I wanted to simplify my tests by having my tests extend a class in which the GSON bean is defined, so I wrote something like this:

@TestMixin(GrailsUnitTestMixin)
class ApiControllerSpec extends Specification {
    void setupSpec() {
        //make GSON work in tests
        defineBeans {
            proxyHandler DefaultEntityProxyHandler
            domainSerializer GrailsDomainSerializer, ref('grailsApplication'), ref('proxyHandler')
            domainDeserializer GrailsDomainDeserializer, ref('grailsApplication')
            gsonBuilder(GsonBuilderFactory) {
                pluginManager = ref('pluginManager')
            }
        }
    }

    void setup() {
        controller.gsonBuilder = applicationContext.getBean('gsonBuilder', GsonBuilder)
    }
}

But when I have my actual specification extend it, I get the following exception: org.springframework.beans.factory.NoSuchBeanDefinitionException: No unique bean of type [org.codehaus.groovy.grails.support.proxy.ProxyHandler] is defined: expected single bean but found 2: grailsProxyHandler,proxyHandler

Any clues? :)

robfletcher commented 11 years ago

Looks like (although I'd need to confirm this by digging into the code) that the Grails unit test framework is injecting a bean called grailsProxyHandler into the application context. Something else (and what that is should be obvious from the stack trace) is then retrieving beans with the type ProxyHandler and asserting that there is only one.

This is at odds with "reality" in a running Grails app where the default ProxyHandler bean injected by Grails is called proxyHandler so the Gson plugin simply overrides it. Unfortunately this kind of thing is pretty common, it often seems that things behave in subtly different ways between unit test land and reality. I'm really not a fan of some of what goes on in the Grails unit test support as bootstrapping a complex Spring application context is not exactly promoting good unit isolation.

I'm going to add some unit tests to the test app (currently it just has functional tests) so I can try to improve the unit test support provided by the plugin.

robfletcher commented 11 years ago

The reason for the NoSuchBeanDefinitionException is that in unit tests the property applicationContext is not the same instance as grailsApplication.mainContext. Beans created with defineBeans are present in the former but not the latter. That seems to me like a bug with Grails; it just makes no sense at all.

robfletcher commented 11 years ago

Looks like I've found a solution. Looking into the converters plugin I see that it automatically injects the application context into the converter if it implements ApplicationContextAware. See https://github.com/grails/grails-core/blob/master/grails-plugin-converters/src/main/groovy/org/codehaus/groovy/grails/web/converters/ConverterUtil.java#L85

This means I can just implement that interface in GSON and stop using the static ApplicationHolder.

robfletcher commented 11 years ago

I have a working unit test example in 4f32874. I'm going to work on extracting the common parts out as a test mixin.

robfletcher commented 11 years ago

I have released a 1.1.2-SNAPSHOT containing the test mixin. It would be great if you could give it a try and see if it helps. You should just need to add @TestMixin(GsonUnitTestMixin) to your test/spec class & you can then remove all the defineBeans stuff.

As well as wiring up the GSON converter properly the mixin adds render(GSON) to controllers, <init>(JsonObject) and setProperties(JsonObject) to domains and getGSON() to HttpServletRequest. All of those are the same as in a running app. You can also use response.GSON to get a JsonElement from the response in controller unit tests.

If everything is behaving correctly for you I'll release 1.1.2 ASAP.

gregopet commented 11 years ago

Unfortunately, sometimes it works and sometimes it doesn't. If I run the unit tests by invoking grails run-app then I'd say I get an exception in about 1/3 of all runs. There seem to be two distinct stacktaces appearing at random:

org.springframework.beans.factory.NoSuchBeanDefinitionException: No unique bean of type [org.codehaus.groovy.grails.support.proxy.ProxyHandler] is defined: expected single bean but found 2: grailsProxyHandler,proxyHandler
at grails.test.mixin.web.ControllerUnitTestMixin.configureGrailsWeb(ControllerUnitTestMixin.groovy:216)
at org.spockframework.util.ReflectionUtil.invokeMethod(ReflectionUtil.java:138)
at org.spockframework.runtime.extension.builtin.JUnitFixtureMethodsExtension$FixtureType$FixtureMethodInterceptor.intercept(JUnitFixtureMethodsExtension.java:145)
at org.spockframework.runtime.extension.MethodInvocation.proceed(MethodInvocation.java:84)
at org.spockframework.util.ReflectionUtil.invokeMethod(ReflectionUtil.java:138)

and

org.springframework.beans.factory.NoSuchBeanDefinitionException: No bean named 'gsonBuilder' is defined
at grails.plugin.gson.test.GsonUnitTestMixin.enhanceApplication(GsonUnitTestMixin.groovy:31)
at org.spockframework.util.ReflectionUtil.invokeMethod(ReflectionUtil.java:138)
at org.spockframework.runtime.extension.builtin.JUnitFixtureMethodsExtension$FixtureType$FixtureMethodInterceptor.intercept(JUnitFixtureMethodsExtension.java:145)
at org.spockframework.runtime.extension.MethodInvocation.proceed(MethodInvocation.java:84)
at org.spockframework.util.ReflectionUtil.invokeMethod(ReflectionUtil.java:138)
at org.spockframework.util.ReflectionUtil.invokeMethod(ReflectionUtil.java:138)
at org.spockframework.util.ReflectionUtil.invokeMethod(ReflectionUtil.java:138)

Both of them prevent any tests from that specification from being run. If I start a grails interactive shell and then issue test-app, however, then the tests seem to always run successfully - though perhaps I've just been always winning some Grails bootstrap lottery every time I started up the interactive mode...

robfletcher commented 11 years ago

@gregopet not sure what's going on there. I'm not seeing the problem with the duplicate ProxyHandler beans in the unit test I've added.

The second one is very odd as the exception is getting thrown from an @Before method and complaining that a bean defined in an @BeforeClass method is not there. I wonder if the order the mixins are executed is non-deterministic and my definitions are sometimes getting overridden by those in the standard Grails mixins.

So much black magic, it's very hard to debug :(

robfletcher commented 11 years ago

Damn, I just got the proxyHandler thing too.

gregopet commented 11 years ago

Grails does seem like black magic sometimes, especially if one doesn't have a strong Java & Spring background.

If I can help you narrow these down in any way, please let me know - these bugs that only happen sometimes are damned annoying.

robfletcher commented 11 years ago

I've updated the snapshot. Can you see if that fixes the problem. I was able to recreate both the problems you were having and I think I've now resolved them.

gregopet commented 11 years ago

It works!

I re-ran the tests over 20 times just now without a single problem. And the init/properties/response.GSON/render worked for me as well.

Thank you!

robfletcher commented 11 years ago

Great. I'll get the release done so you can stop using a snapshot.

Thanks for your help tracking everything down.

mlem commented 11 years ago

So how do you make this tests now work? Do I have to use the codesnippet from above? (I modifed it to run with JUnit with @BeforeClass and @Before) I'm using grails 2.2.2

mlem commented 11 years ago

I'm getting this stacktrace:

| Failure:  <my.test.class>
|  groovy.lang.MissingPropertyException: No such property: DefaultEntityProxyHandler for class: <my.test.class>
    at <my.test.class>$_setupSpec_closure1.doCall(TicketControllerTests.groovy:24)
    at grails.spring.BeanBuilder.invokeBeanDefiningClosure(BeanBuilder.java:757)
    at grails.spring.BeanBuilder.beans(BeanBuilder.java:584)
    at grails.test.mixin.support.GrailsUnitTestMixin.defineBeans(GrailsUnitTestMixin.groovy:74)
    at <my.test.class>.setupSpec(TicketControllerTests.groovy:23)
| Failure:  <my.test.class>
|  java.lang.NullPointerException: Cannot invoke method isActive() on null object
    at grails.test.mixin.support.GrailsUnitTestMixin.shutdownApplicationContext(GrailsUnitTestMixin.groovy:234)

my code:

@BeforeClass
    static void setupSpec() {
        //make GSON work in tests
        defineBeans {
            proxyHandler DefaultEntityProxyHandler
            domainSerializer GrailsDomainSerializer, ref('grailsApplication'), ref('proxyHandler')
            domainDeserializer GrailsDomainDeserializer, ref('grailsApplication')
            gsonBuilder(GsonBuilderFactory) {
                pluginManager = ref('pluginManager')
            }
        }
    }

    @Before
    void setup() {
        controller.gsonBuilder = applicationContext.getBean('gsonBuilder', GsonBuilder)
    }
robfletcher commented 11 years ago

@mlem looks like you're missing an import for DefaultEntityProxyHandler

ashok84munna commented 11 years ago

I'm trying to write a unit test for a simple controller that takes GSON as input and writes to DB. GSON because, the input has nested objects.

Controller implementation: def create() {

    def artist = new Artist(request.GSON)

    if (!artist.save(flush: true)) {
        artist.errors.each {
            log.error("Error while creating Artist " + it)
        }
        render status: 500, layout: null
        return
    }
    response.status = 201
    render artist as GSON
}

Unit test:

@TestMixin(GsonUnitTestMixin) @TestFor(ArtistController) @Mock(Artist) class ArtistControllerTests {

void testCreate() {
    request.GSON = "{guid:123,"+
                                           "name: 'Akon',"+
                    "albums: ["+
                    "{guid:1,"+
                    "name:'album 1'"+
                    "}]}"
    controller.create()
    assert response.status == 201
}

}

Exception: Cannot get property 'manyToOne' on null object at def artist = new Artist(request.GSON) in the controller

Can someone please help me?