1

I have a controller like this:

class NotificationApiController {

    def countService

    def getCount() {
        def results = countService.getCount()

        render results as JSON
    }
}

And the controller test like this:

Closure doWithSpring() {{ ->
        countService(CountService)
    }}

CountService countService

def setup() {
}

def cleanup() {
}

void "test getCount method"() {
        given:
        def countService = Mock(CountService) {
            1 * getCount(_) >> [count: 2]
        }
        when:
        def y = controller.getCount()

        then:
        y == [count: 2]
    }

It appears it always calls into the actual CountService injected in Closure doWithSpring(), not my mock countService, but without the definition of Closure doWithSpring()..., I will get this error

Cannot invoke method getCount() on null object
java.lang.NullPointerException: Cannot invoke method getCount() on null object

The documentation on unit testing in 4.0 is really limited and I am not exactly sure how should I do this. I see some samples in Grails 2.3 or 3.3. version, but they all seems not working for me, mostly due to the difference of Spock and Mixin framework I guess. Any suggestions on how to do this?

Chris Ji
  • 153
  • 1
  • 4
  • It isn't clear if you want an instance of `CountService` to be in play or if you want to create a mock. The mock you are creating inside the test method is a local variable that you never do anything with so it won't be relevant to anything. In your `doWithSpring` method you are asking that an instance of `CountService` be created. Is that the one you want to use? – Jeff Scott Brown Dec 09 '19 at 15:43
  • Also isnt clear why you want `CountService countService` in the test. The test isn't doing anything with that. – Jeff Scott Brown Dec 09 '19 at 15:45
  • This isn't really related to your question but your test is checking the return value of the controller's `getCount()` method and expecting it to be a `Map`, which it will not be. The controller method is going to return `null` in this case. You should be checking the body of the response. – Jeff Scott Brown Dec 09 '19 at 15:50
  • Another item not related to the problem in the question is that it isn't a great idea to have a controller action with a name like `getCount()` because the `get` makes that a property accessor which will be invoked in some circumstances that are surprising. You should call the action `count` or something that isn't a bean property accessor. – Jeff Scott Brown Dec 09 '19 at 16:28
  • Sorry, I was just copy some samples from the documentation to see what I can do. I do not want to use the existing CountService, I already have unit test for that service. I only want to test the controller, which is internally calling that CountService, I want to mock this service and especially it's method the controller will call, I don't want to duplicate the whole service setup to test the controller, feels redundant. – Chris Ji Dec 09 '19 at 18:49

1 Answers1

2

You have omitted some details that might affect the recommendation but the project at https://github.com/jeffbrown/chrisjiunittest shows 1 way to go about this.

https://github.com/jeffbrown/chrisjiunittest/blob/a59a58e3b6ed6b47e1a8104f3e4d3bdb138abacc/src/test/groovy/chrisjiunittest/NotificationApiControllerSpec.groovy

package chrisjiunittest

import grails.testing.web.controllers.ControllerUnitTest
import spock.lang.Specification

class NotificationApiControllerSpec extends Specification implements ControllerUnitTest<NotificationApiController> {

    void "test something"() {
        setup:
        // whether or not this is the right thing to do
        // depends on some other factors, but this is
        // an example of one approach...
        controller.countService = Mock(CountService) {
            getCount() >> [count: 2]
        }

        when:
        controller.getCount()

        then:
        response.json == [count: 2]
    }
}

Another option:

package chrisjiunittest

import grails.testing.web.controllers.ControllerUnitTest
import spock.lang.Specification

class NotificationApiControllerSpec extends Specification implements ControllerUnitTest<NotificationApiController> {

    Closure doWithSpring() {
        // whether or not this is the right thing to do
        // depends on some other factors, but this is
        // an example of one approach...
        { ->
            countService MockCountService
        }
    }

    void "test something"() {
        when:
        controller.getCount()

        then:
        response.json == [count: 2]
    }
}

class MockCountService {
    Map getCount() {
        [count: 2]
    }
}
Jeff Scott Brown
  • 26,804
  • 2
  • 30
  • 47
  • Excellent. I am glad I could help. – Jeff Scott Brown Dec 09 '19 at 18:45
  • Note that the way that mock is setup with `getCount() >> [count: 2]`, that means any time the `getCount()` method is invoked it will return the `Map` defined there. If in your test you are really wanting to assert that the `getCount()` method is called once and only once, `1 * getCount() >> [count: 2]` will make more sense. – Jeff Scott Brown Dec 09 '19 at 19:24
  • Thanks Jeff, I had this exactly in my test with 1 * getCount(), really appreciated your help!!! – Chris Ji Dec 10 '19 at 02:57
  • @beechovsky "In neither of the examples above are you referencing the service in the method call - you're directly accessing the method from the test's controller object. Is this accurate?". It is correct that the service is never referenced, in the method call or otherwise. That is because the test is to mocking out the service so there is no real service when the unit test runs. A method is invoked on the mock at https://github.com/jeffbrown/chrisjiunittest/blob/a59a58e3b6ed6b47e1a8104f3e4d3bdb138abacc/grails-app/controllers/chrisjiunittest/NotificationApiController.groovy#L10. – Jeff Scott Brown Aug 24 '21 at 13:10
  • The comment I quoted there has since been removed by the author @beechovsky . Sorry for the confusion. – Jeff Scott Brown Aug 24 '21 at 13:12
  • @JeffScottBrown Thanks! I realized it didn't need to be accessed after posting. – beechovsky Aug 24 '21 at 13:22