I'm working in a Spring Boot app, I have a Groovy class:
@Component
class MyClass {
DSLContext jooq
MyClass(DSLContext jooq) {
this.jooq = jooq
}
void execute(String value) {
Table tbl // assume assigned a Table
Field<?> fld // assume assigned a Field
List<Record> records = jooq.select(DSL.asterisk()).from(tbl).where(fld.eq(value)).fetch()
InsertValueStepN<Record> insertStep = jooq.insertInto(tbl)?.columns()
// steps to complete insertStep elided
insertStep?.execute()
}
}
And I have a Spock unit test class:
class MyClassTest extends Specification {
DSLContext jooq = Mock()
MyClass tested = new MyClass(jooq)
def "execute doesn't throw exceptions when no records found"() {
given:
jooq.select(_ as Field[]) >> []
expect:
tested.execute(null)
}
}
My trouble is in mocking jOOQ and the result of jooq.select()
. I need Spock to get passed calling jooq.select()
. As shown, I get an NPE when trying to invoke from()
on a null object.
I've also tried jooq.select(DSL.asterisk()) >> []
, but it throws a GroovyCastException
because []
as an ArrayList
isn't a SelectSelectStep
... but SelectSelectStep
is an interface whose only implementation is SelectImpl
and newing-up a SelectImpl
is proving to be a challenge (and doesn't seem like the right answer anyway).
For the time being, I've decided to use the safe-navigation operator (?.
), which allows my current mock to pass, but again, this doesn't seem right. I'd like to actually mock the results of the jOOQ calls. What am I missing?
Not because I think that it is particularly nice, but for a simple unit test without Testcontainers it would look like this with the original code, only adjusted to make the Table
and Field
values injectable to get it compiling and running:
package de.scrum_master.stackoverflow.q75745239
import org.jooq.*
import org.jooq.impl.DSL
import org.jooq.impl.Eq
import spock.lang.Specification
class MyClassTest extends Specification {
DSLContext jooq = Mock(DSLContext) {
select(_) >> Mock(SelectSelectStep) {
from(_) >> Mock(SelectJoinStep) {
where(_) >> Mock(SelectConditionStep) {
fetch() >> Mock(Result)
}
}
}
insertInto(_) >> Mock(InsertSetStep)
}
MyClass tested = new MyClass(jooq,
// These two mocks only exist to make the sample code work
Mock(Table),
Mock(Field) {
eq(_) >> Mock(Eq)
}
)
def "execute doesn't throw exceptions when no records found"() {
when:
tested.execute("dummy")
then:
noExceptionThrown()
}
}
class MyClass {
DSLContext jooq
Table tbl // assume assigned a Table
Field<?> fld // assume assigned a Field
MyClass(DSLContext jooq, Table tbl, Field<?> fld) {
this.jooq = jooq
this.tbl = tbl
this.fld = fld
}
void execute(String value) {
List<Record> records = jooq.select(DSL.asterisk()).from(tbl).where(fld.eq(value)).fetch()
InsertValuesStepN<Record> insertStep = jooq.insertInto(tbl)?.columns()
// steps to complete insertStep elided
insertStep?.execute()
}
}
Now if we want to make code with chained DSL calls like JOOQ's more easily testable, we can simply extract those call chains into helper methods with clean names and then use a Spy
to stub them in unit tests (not in integration tests or closer-to-integration tests using Testcontainers or an in-memory DB):
package de.scrum_master.stackoverflow.q75745239
import org.jooq.*
import org.jooq.impl.DSL
import spock.lang.Specification
class MyClassTest extends Specification {
MyClass tested = Spy(new MyClass(Mock(DSLContext), Mock(Table), Mock(Field))) {
fetchRecords(_) >> Mock(Result)
createInsertStep() >> null
}
def "execute doesn't throw exceptions when no records found"() {
when:
tested.execute("dummy")
then:
noExceptionThrown()
}
}
class MyClass {
DSLContext jooq
Table tbl // assume assigned a Table
Field<?> fld // assume assigned a Field
MyClass(DSLContext jooq, Table tbl, Field<?> fld) {
this.jooq = jooq
this.tbl = tbl
this.fld = fld
}
void execute(String value) {
List<Record> records = fetchRecords(value)
InsertValuesStepN<Record> insertStep = createInsertStep()
// steps to complete insertStep elided
insertStep?.execute()
}
protected Result<Record> fetchRecords(String value) {
return jooq.select(DSL.asterisk()).from(tbl).where(fld.eq(value)).fetch()
}
protected InsertValuesStepN<Record> createInsertStep() {
return jooq.insertInto(tbl)?.columns()
}
}
See? No mock hell, the unit test is clean and simple.
IMO, nothing replaces a unit test and nothing beats it with regard to speed. If it is difficult to write a unit test, refactor. TDD and BDD are design tools, not just means to cover code with tests. It is pretty easy to extract complex call chains into small methods.