Home > Software engineering >  Mocking with Spock returns null for nested mocking and stubbing
Mocking with Spock returns null for nested mocking and stubbing

Time:01-27

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.

CodePudding user response:

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)
  • Related