Reputation: 575
I'm trying to use spock framework for unit testing some java class. The structure looks like this:
com.myorg.requests
(Class name: RequestProcessor
)com.myorg.query
(Class name: DatabaseQuery
)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
Reputation: 67287
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:
this.request = request
(self-assignment), but it should be this.request = aRequest;
.requestHandler.processRequest()
, but is should be requestProcessor.processRequest()
.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
.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.
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.
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:
DatabaseQuery
, you can inject a normal Mock
.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