0
votes

I'm trying to write an integration test for my Grails service class using Spock.

To get some data to drive the test, I have a query like this: "select column1, column2, column3 from table where column1 = ?"

I get the results from that query using a custom service that we wrote. Basically, it wraps up Groovy's Sql class and handles opening and closing database connections for me. I can inject that into my integration test just fine using Grails' typical dependency injection. However, I can't seem to use it to retrieve data for a data-driven test. Here's my test class:

class MyServiceSpec extends Specification {
    def myService
    def sqlService
    @Shared tsQuery
    @Shared ts

    def setupSpec() {
        tsQuery = "select column1, column2, column3 from table where column1 = ?"
        ts = sqlService.rows(tsQuery, ['someSuch'])
    }

    def "test a feature"() {
        setup:
            def ts = sqlService.rows(tsQuery)
            def results = myService.getTimesheets(thing1, thing2, thing3)
        expect: 
            //some appropriate expectations based on the data here
        where:
            timeSheet << ts
            thing1 = timeSheet.column1
            thing2 = timeSheet.column2
            thing3 = timeSheet.column3
    }
}

That throws an error that I'm not allowed to call sqlService in the setupSpec() block:

Only @Shared and static fields may be accessed from here
       ts = sqlService.rows(tsQuery)
            ^

The problem is, if I make sqlService @Shared, Grails dependency injection doesn't work and it just creates a null object. Same thing if I try making it static (as in static def sqlService).

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

Workarounds I've Tried

A new SqlService Instance in setupSpec

I tried just initalizing a new SqlService instance, like this in my setupSpec block:

    def setupSpec() {
        tsQuery = "select column1, column2, column3 from table where column1 = 'someSuch'"
        def sql = new SqlService()
        ts = sql.rows(tsQuery)
    }

That just gives an error

| Error 2014-06-13 14:52:10,932 [main] ERROR common.SqlService - Error in sqlQuery: Ambiguous method overloading for method groovy.sql.Sql#. Cannot resolve which method to invoke for [null] due to overlapping prototypes between: [interface javax.sql.DataSource] [interface java.sql.Connection]

Defining sqlService in the setup method

If, instead of using setupSpec() I just use setup(), and put a def sqlService in it, then the sqlService object is null and thus I get a "Cannot invoke method rows() on null object" error.

If I instead def sqlService = new SqlService(), then I get the same error as if I did that in setupSpec(), saying that there are overlapping prototypes between javax.sql.Datasource and java.sql.Connection.

Bottom Line

Anyone know how I can use another service class as a data provider in a Spock test?

3

3 Answers

2
votes

So, this is what my working test class finally looks like:

package edu.missouristate.employee.dts

import spock.lang.*
import static org.junit.Assert.*
import org.junit.*
import groovy.sql.Sql
class MyServiceSpec extends Specification {
    def myService
    def dataSource
    @Shared tsQuery
    @Shared ts

    def setup() {
        tsQuery = '''select column1, column2, column3 
            from table where column1 = ?
            and rownum < 6'''
        def sql = new Sql(dataSource)
        ts = ts ?: sql.rows(tsQuery, ['someSuch'])
        sql.close()
    }

    def "dummy test"() { //This is just to get the spec initialized.
        expect:
            1==1
    }

    def "test a feature"() {
        setup:
            def results = myService.getTimesheets(timeSheet.thing1, timeSheet.thing2, timeSheet.thing3)
        expect: 
            //some appropriate expectations based on the data here, e.g.
            results[0].someProperty == timeSheet.correspondingDatabaseColumn
        where:
            timeSheet << ts
    }
}

In short, I gave up on using my own sqlService class, and just went for the straight Groovy Sql class.

Other notes: I'm effectively reusing the ts object across test methods and iterations, since I only really want to pull the data from the database once. I also added a rownum limit in my query, so that it would only do so many iterations.

Since the setup block has to reference dataSource (which, in turn, needed to be declared with def dataSource), it needed to be a setup() and not setupSpec(). However, since Spock actually starts to evaluate the where: block before the setup() fixture, I had to add a dummy test method in order to populate the ts variable. Props to @Mario David for pointing me in the right direction on that one.

1
votes

it should work, if you define your service within the setup() method, not within the setupSpec() Method. setup() is called before every testcase within this specification, where as setupSpec() is only called once before the whole execution of this specification (further information about this).

The question is, do you really want the SqlService just created once (i don't see a reason to do this)? If not, use setup() and your errors should disappear.

0
votes

If you really do want database integration, save it for the integration test. Your other services won't be injected for unit tests.

If you want just a unit test, then you should really be mocking the SqlService and providing stub data to the service under test.

Also, consider parameterising your SQL query. It will make your production code safer, and makes it easier to mock the rows method:

tsQuery = "select column1, column2, column3 from table where column1 = ?"
def columnParam = 'column1'
ts = sqlService.rows(tsQuery, columnParam)

So you could use something like this:

import grails.test.mixin.Mock
...
    def setupSpec() {
        sqlService = Mock(SqlService)

        sqlService.rows(_, 'SomeParticularColumnParam') >> [ 'A', 'B', 'C' ]
        sqlService.rows(_, 'SomeOtherValue') >> [ 'X', 'Y', 'Z' ]
        // or as a catch-all
        sqlService.rows(*_) >> [ 'column1', 'column2', 'column3' ]
       ...
    }

Have a look at the Spock documentation on Interaction-based testing to see more examples.