Search code examples
javaunit-testinggroovymockingspock

Mocking with Spock returns null for nested mocking and stubbing


I am writing unit tests with Spock for a series of nested objects. The code I'm writing tests for is quite legacy and doesn't use dependency injection. However it is also quite mission-critical so I'd rather not touch it unless I really need to.

Here is the constructor of the class I'm trying to test:

public SqlTable(Connection conn, String query) throws Exception {
    this.statement = conn.createStatement();
    this.resultSet = statement.executeQuery(query);

    meta = resultSet.getMetaData();
    int n = meta.getColumnCount();
    columns = new Column[n];

    for (int c = 0; c < n; c++) {
        columns[c] = new Column(meta.getColumnName(c+1));
        // ...
    }
}

In the tests I stub the nested mocks in a .groovy file, like this:

def "initialising a SQL table"() {
    given:
    def COL_NAME = "someColumnName"
    def mockResSetMeta = Mock(ResultSetMetaData) {
        getColumnCount() >> 1
        getColumnName(_ as int) >> COL_NAME
    }

    and:
    def mockResSet = Mock(ResultSet) {
        getMetaData() >> mockResSetMeta
    }

    and:
    def mockStatement = Mock(Statement) {
        executeQuery(_ as String) >> mockResSet
    }

    and:
    def mockConn = Mock(Connection) {
        createStatement() >> mockStatement
    }

    when: "SqlTable object"
    def table = new SqlTable(mockConn, "some query")

    then: "the table contains the categorical column"
    table.columns[0].getName() == COL_NAME
}

However the test fails. By debugging, I found that, in the SqlTable constructor, the mock for the ResultSetMetaData object, when getColumnName() is called, always returns null.

I did some digging, and it seems like this is due to how stubbing and mocking are handled together by Spock. I found two promising answers on SO:

However for the life of me I wasn't able to modify the test in order to make it work.


Solution

  • The Problem lies in this line getColumnName(_ as int) >> COL_NAME it works when you change it to getColumnName(_ as Integer) >> COL_NAME or just getColumnName(_) >> COL_NAME.

    My current assumptions as to why this is happening, is that the actual arguments in the mock are passed as an Object[] and that can't contain primitive types.

    Here is a runnable reproducer

    import spock.lang.*;
    import java.sql.*;
    
    class ASpec extends Specification {
        def "initialising a SQL table"() {
            given:
                def COL_NAME = "someColumnName"
                def mockResSetMeta = Mock(ResultSetMetaData) {
                    getColumnCount() >> 1
                    getColumnName(_ as Integer) >> COL_NAME
                }
    
            and:
                def mockResSet = Mock(ResultSet) {
                    getMetaData() >> mockResSetMeta
                }
    
            and:
                def mockStatement = Mock(Statement) {
                    executeQuery(_ as String) >> mockResSet
                }
    
            and:
                def mockConn = Mock(Connection) {
                    createStatement() >> mockStatement
                }
    
            when: "SqlTable object"
            def table = new SqlTable(mockConn, "some query")
    
            then: "the table contains the categorical column"
            table.columns[0].getName() == COL_NAME
        }
    }
    
    @groovy.transform.Canonical
    class Column {
        String name
    }
    
    class SqlTable {
        def columns
        public SqlTable(Connection conn, String query) throws Exception {
            def statement = conn.createStatement();
            def resultSet = statement.executeQuery(query);
    
            def meta = resultSet.getMetaData();
            int n = meta.getColumnCount();
            columns = new Column[n];
    
            for (int c = 0; c < n; c++) {
                columns[c] = new Column(meta.getColumnName(c+1));
                // ...
            }
        }
    }
    

    Try it in the Groovy Web Console

    For future reference, I easily detect the issue by actually performing mocking and not just stubbing.

    then:
    1 * mockResSetMeta.getColumnName(_ as int) >> COL_NAME
    

    will print

                   1 * mockResSetMeta.getColumnName(_ as int) >> COL_NAME   (0 invocations)
                
                   Unmatched invocations (ordered by similarity):
                
                   1 * mockResSetMeta.getColumnName(1)
                   One or more arguments(s) didn't match:
                   0: argument instanceof int
                      |        |          |
                      |        false      int
                      1 (java.lang.Integer)