aver
aver

Reputation: 575

Mocked java class in spock test is not being executed

I'm trying to use spock framework for unit testing some java class. The structure looks like this:

The first class looks something like this:

public class RequestProcessor {

   private String request;

   public RequestProcessor(String aRequest) {
      this.request = request;
   }

   public String processRequest() {
       String response ;
       //do something here

       try {
           if(condition meets) {
              response = executeRequest();
           }
       } catch ( various exceptions... ) {
           System.out.println("something went wrong...");
       }
   }

   private String executeRequest() throws <<exceptions thrown by DatabaseQuery>> {
       //do something here
       DatabaseQuery queryResult = new DatabaseQuery(request)
   }
}

I'm trying to write a Spock test for this RequestProcessor class which has a dependency on DatabaseQuery. I'm thinking of mocking DatabaseQuery class in order to simply test the RequestProcessor class in isolation.

The RequestProcessor's processRequest() method is being called, which relies on another private method. That method will use DatabaseQuery to get the actual query result. This is what my Spock test looks like:

class RequestProcessorSpec extends Specification {

    //Class to be tested
    RequestProcessor requestProcessor

    //Dependencies
    DatabaseQuery dbquery

    def "Given a valid request, dbquery's executeQuery method is called" () {
        given: "a valid request"
            def queryRequest = '{"info1":"value1","info2":"value2","query":"select * from users"}'

        and: "mock the DBQuery class"
            dbquery = Mock(DatabaseQuery)

        and: "create a new request"
            requestProcessor = new RequestProcessor(queryRequest)

        when: "the request is processed"
            requestHandler.processRequest()

        then: "dbquery executeQuery method is called"
            1 * dbquery.executeQuery(_ as String)
    }
 }

This isn't exactly working for me. I'm getting an error:

When I run the test with gradlew test --info for more results, I see a log printed on the console that is captured by try-catch statement in processRequest method.

What am I doing wrong here?

Upvotes: 0

Views: 2193

Answers (1)

kriegaex
kriegaex

Reputation: 67287

Problems in the sample code

First of all, your sample code does not work, even if I simplify it and create my own dummy DatabaseQuery class because you have at least three errors here:

  • In the constructor you have this.request = request (self-assignment), but it should be this.request = aRequest;.
  • In the test you have requestHandler.processRequest(), but is should be requestProcessor.processRequest().
  • Method executeRequest() does not return a String as specified. So I can only speculate that in reality it calls another method upon DatabaseQuery so as to convert the query result into a String.

Why is the mock not working?

Having gotten that out of the way, let's see what is really fundamentally wrong with your test.

What am I doing wrong here?

To assume that the mock in your local variable is somehow effective in the other local variable in your application code. In order for a mock to be used you need to inject it into the class under test. But like so many developers, you did not design for decoupling and testability via dependency injection but you are creating your dependency - in this case the DatabaseQuery object - internally.

What else is wrong with the test?

I think your test just suffers from over-specification. Why would you want to check if a specific method in another class is called from a private method (indirectly, at that)? You do not directly test private methods but cover their code by calling public methods, and your test already does that.

If you want to cover your catch blocks, just make sure that your request causes the right exceptions. Maybe you need to mock the database connection for that and make sure it returns the expected results to the DatabaseQuery. I do not see enough of your code in order to say exactly.

Technical solution for the original problem

Now let's assume you absolutely want to check this interaction, no matter what I have said before. What you need to do depends on the situation (which you do not show in your code):

In any case you need to make the DatabaseQuery injectable. You can just add a member and another setter to your class.

Now you have a fork in the road depending on where the the interaction dbquery.executeQuery(_ as String) is made (called) from:

  1. If the method is called from outside DatabaseQuery, you can inject a normal Mock.
  2. If the method is called from inside DatabaseQuery, you need to inject a Spy because a mock would not call other internal methods like the original object because - well, it is just a mock.

Case 1: executeQuery(String) is called from outside

package de.scrum_master.query;

public class DatabaseQuery {
  private String request;

  public DatabaseQuery(String request) {
    this.request = request;
  }

  public String executeQuery(String request) {
    return request.toUpperCase();
  }

  public String getResult() {
    return executeQuery(request);
  }
}
package de.scrum_master.requests;

import de.scrum_master.query.DatabaseQuery;

public class RequestProcessor {
  private String request;
  private DatabaseQuery databaseQuery;

  public RequestProcessor(String aRequest) {
    this.request = aRequest;
    databaseQuery = new DatabaseQuery(request);
  }

  public String processRequest() {
    return executeRequest();
  }

  private String executeRequest() {
    return databaseQuery.executeQuery(request);
    //return databaseQuery.getResult();
  }

  public void setDatabaseQuery(DatabaseQuery databaseQuery) {
    this.databaseQuery = databaseQuery;
  }
}
package de.scrum_master.requests

import de.scrum_master.query.DatabaseQuery
import spock.lang.Specification

class RequestProcessorTest extends Specification {
  //Class to be tested
  RequestProcessor requestProcessor

  //Dependencies
  DatabaseQuery dbquery

  def "Given a valid request, dbquery's executeQuery method is called" () {
    given: "a valid request"
    def queryRequest = '{"info1":"value1","info2":"value2","query":"select * from users"}'

    and: "mock the DBQuery class"
    dbquery = Mock(DatabaseQuery)
    //dbquery = Spy(DatabaseQuery, constructorArgs: [queryRequest])

    and: "create a new request"
    requestProcessor = new RequestProcessor(queryRequest)
    requestProcessor.databaseQuery = dbquery

    when: "the request is processed"
    requestProcessor.processRequest()

    then: "dbquery executeQuery method is called"
    1 * dbquery.executeQuery(_ as String)
  }
}

Now the test works including the interaction check.

Case 2: executeQuery(String) is called internally from its own class

Did you see the two commented-out lines in RequestProcessor and RequestProcessorTest? Just use them and comment out the other two instead like this:

  private String executeRequest() {
    //return databaseQuery.executeQuery(request);
    return databaseQuery.getResult();
  }
    and: "mock the DBQuery class"
    //dbquery = Mock(DatabaseQuery)
    dbquery = Spy(DatabaseQuery, constructorArgs: [queryRequest])

The test still works including the interaction check.

Of course I had to fake a few things and fill in the missing puzzle tiles you did not provide, but this is basically how it works.

Upvotes: 3

Related Questions