如何在Spock中模拟jOOQ结果

huangapple go评论63阅读模式
英文:

How to mock a jOOQ result in Spock

问题

我正在使用Spring Boot应用程序,我有一个Groovy类:

@Component
class MyClass {

    DSLContext jooq

    MyClass(DSLContext jooq) {
        this.jooq = jooq
    }

    void execute(String value) {
        Table tbl     // 假设分配了一个Table
        Field<?> fld  // 假设分配了一个Field

        List<Record> records = jooq.select(DSL.asterisk()).from(tbl).where(fld.eq(value)).fetch()

        InsertValueStepN<Record> insertStep = jooq.insertInto(tbl)?.columns()
        // 完成insertStep的步骤已省略
        insertStep?.execute()
    }
}

而且我有一个Spock单元测试类:

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)
    }
}

我的问题出在模拟jOOQ和jooq.select()的结果上。我希望Spock在调用jooq.select()时能够成功执行。如上所示,当试图在空对象上调用from()时,我得到了一个NPE。

我还尝试过jooq.select(DSL.asterisk()) >> [],但它会抛出GroovyCastException,因为[]作为ArrayList不是SelectSelectStep... 但SelectSelectStep是一个只有一个实现SelectImpl的接口,而创建一个SelectImpl正在变得具有挑战性(而且似乎不是正确的答案)。

目前,我决定使用安全导航操作符(?.),这允许我的当前模拟通过,但再次强调,这似乎不正确。我希望能够真正模拟jOOQ调用的结果。我漏掉了什么?

英文:

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&lt;?&gt; fld  // assume assigned a Field

        List&lt;Record&gt; records = jooq.select(DSL.asterisk()).from(tbl).where(fld.eq(value)).fetch()

        InsertValueStepN&lt;Record&gt; 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 &quot;execute doesn&#39;t throw exceptions when no records found&quot;() {
        given:
            jooq.select(_ as Field[]) &gt;&gt; []
        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()) &gt;&gt; [], 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?

答案1

得分: 3

模拟 jOOQ API

jOOQ API 非常庞大,你几乎无法完全模拟它。你遇到的简单障碍已经告诉你,你即将面临一段漫长而痛苦的道路。我建议你不要模拟 jOOQ API。

你遇到的问题表明,你不能只从 jooq.select() 返回 []。你必须继续模拟所有返回的 DSL 类型,直到达到像 fetch() 这样的操作(然后,你还必须模拟所有的变体)。

模拟 JDBC

jOOQ 提供了 MockConnection,允许通过单个 lambda 表达式或使用文件来模拟 JDBC(而不是 jOOQ API)。然而,正如你在这些手册页面上所注意到的那样,有一个免责声明:

> 使用 jOOQ API 模拟 JDBC 连接的一般想法是提供快速的解决方法、注入点等,使用非常简单的 JDBC 抽象。不建议使用此模拟 API 来模拟整个数据库(包括复杂的状态转换、事务、锁定等)。一旦你有这个要求,请考虑使用实际的数据库产品进行集成测试,而不是在 MockDataProvider 内实现测试数据库。

模拟数据库很少是一个好主意,因为你将实现自己的数据库,而不是测试与实际数据库的交互。

更好的选择:集成测试

最好使用集成测试,例如通过 testcontainers。本文说明了testcontainers 通常比使用测试数据库产品(例如 H2)更好的原因,(这仍然比模拟更好)。这也可以用于生成 jOOQ 代码

英文:

Mocking the jOOQ API

The jOOQ API is vast and you will hardly be able to mock it completely. The simple roadblock you ran into should tell you that you're about to go down a long road of pain and suffering. I recommend you don't mock the jOOQ API.

The problem you ran into is shows that you can't just return an [] from jooq.select(). You have to continue mocking all the returned DSL types until you reach an operation like fetch() (and then, you have to mock also all the variants!).

Mocking JDBC

jOOQ offers a MockConnection, which allows for mocking JDBC (not the jOOQ API!) via a single lambda expression, or using a file. However, as you will notice on those manual pages, there's a disclaimer:

> The general idea of mocking a JDBC connection with this jOOQ API is to provide quick workarounds, injection points, etc. using a very simple JDBC abstraction. It is NOT RECOMMENDED to emulate an entire database (including complex state transitions, transactions, locking, etc.) using this mock API. Once you have this requirement, please consider using an actual database product instead for integration testing, rather than implementing your test database inside of a MockDataProvider.

Mocking a database is rarely a good idea, as you will be implementing your own database, rather than testing interactions with your actual database.

Better: integration testing

Better use integration tests, e.g. via testcontainers. This article illustrates why testcontainers is generally better than using a test database product, such as H2, (which is again still better than mocking). This can also be useful to generate jOOQ code.

答案2

得分: 1

Variant A: 原始代码

不是因为我认为它特别好,而是为了进行一个简单的单元测试,没有使用Testcontainers,原始代码如下,只需调整以使TableField值可注入以使其编译和运行:

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(_) &gt;&gt; Mock(SelectSelectStep) {
      from(_) &gt;&gt; Mock(SelectJoinStep) {
        where(_) &gt;&gt; Mock(SelectConditionStep) {
          fetch() &gt;&gt; Mock(Result)
        }
      }
    }
    insertInto(_) &gt;&gt; Mock(InsertSetStep)
  }
  MyClass tested = new MyClass(jooq,
    // 这两个模拟仅存在以使示例代码工作
    Mock(Table),
    Mock(Field) {
      eq(_) &gt;&gt; Mock(Eq)
    }
  )

  def &quot;execute doesn&#39;t throw exceptions when no records found&quot;() {
    when:
    tested.execute(&quot;dummy&quot;)

    then:
    noExceptionThrown()
  }
}
class MyClass {
  DSLContext jooq
  Table tbl     // 假设已经分配了一个Table
  Field&lt;?&gt; fld  // 假设已经分配了一个Field

  MyClass(DSLContext jooq, Table tbl, Field&lt;?&gt; fld) {
    this.jooq = jooq
    this.tbl = tbl
    this.fld = fld
  }

  void execute(String value) {
    List&lt;Record&gt; records = jooq.select(DSL.asterisk()).from(tbl).where(fld.eq(value)).fetch()
    InsertValuesStepN&lt;Record&gt; insertStep = jooq.insertInto(tbl)?.columns()
    // 完成insertStep的步骤已省略
    insertStep?.execute()
  }
}

Variant B: 为了便于测试而轻微重构的测试代码

现在,如果我们想要更容易测试的链式DSL调用的代码,例如JOOQ的代码,我们可以简单地将这些调用链提取到具有清晰名称的辅助方法中,然后在单元测试中使用Spy来存根它们(不是在使用Testcontainers或内存数据库的集成测试或接近集成测试中):

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(_) &gt;&gt; Mock(Result)
    createInsertStep() &gt;&gt; null
  }

  def &quot;execute doesn&#39;t throw exceptions when no records found&quot;() {
    when:
    tested.execute(&quot;dummy&quot;)

    then:
    noExceptionThrown()
  }
}
class MyClass {
  DSLContext jooq
  Table tbl     // 假设已经分配了一个Table
  Field&lt;?&gt; fld  // 假设已经分配了一个Field

  MyClass(DSLContext jooq, Table tbl, Field&lt;?&gt; fld) {
    this.jooq = jooq
    this.tbl = tbl
    this.fld = fld
  }

  void execute(String value) {
    List&lt;Record&gt; records = fetchRecords(value)
    InsertValuesStepN&lt;Record&gt; insertStep = createInsertStep()
    // 完成insertStep的步骤已省略
    insertStep?.execute()
  }

  protected Result&lt;Record&gt; fetchRecords(String value) {
    return jooq.select(DSL.asterisk()).from(tbl).where(fld.eq(value)).fetch()
  }

  protected InsertValuesStepN&lt;Record&gt; createInsertStep() {
    return jooq.insertInto(tbl)?.columns()
  }
}

看到了吗?没有模拟混乱,单元测试简洁明了。

在我看来,没有什么能代替单元测试,也没有什么能超越它,特别是在速度方面。如果难以编写单元测试,请进行重构。TDD和BDD是设计工具,不仅是用来覆盖代码的测试手段。将复杂的调用链提取为小方法非常容易。

英文:

Variant A: original code

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(_) &gt;&gt; Mock(SelectSelectStep) {
      from(_) &gt;&gt; Mock(SelectJoinStep) {
        where(_) &gt;&gt; Mock(SelectConditionStep) {
          fetch() &gt;&gt; Mock(Result)
        }
      }
    }
    insertInto(_) &gt;&gt; Mock(InsertSetStep)
  }
  MyClass tested = new MyClass(jooq,
    // These two mocks only exist to make the sample code work
    Mock(Table),
    Mock(Field) {
      eq(_) &gt;&gt; Mock(Eq)
    }
  )

  def &quot;execute doesn&#39;t throw exceptions when no records found&quot;() {
    when:
    tested.execute(&quot;dummy&quot;)

    then:
    noExceptionThrown()
  }
}
class MyClass {
  DSLContext jooq
  Table tbl     // assume assigned a Table
  Field&lt;?&gt; fld  // assume assigned a Field

  MyClass(DSLContext jooq, Table tbl, Field&lt;?&gt; fld) {
    this.jooq = jooq
    this.tbl = tbl
    this.fld = fld
  }

  void execute(String value) {
    List&lt;Record&gt; records = jooq.select(DSL.asterisk()).from(tbl).where(fld.eq(value)).fetch()
    InsertValuesStepN&lt;Record&gt; insertStep = jooq.insertInto(tbl)?.columns()
    // steps to complete insertStep elided
    insertStep?.execute()
  }
}

Variant B: code under test slightly refactored for testability

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(_) &gt;&gt; Mock(Result)
    createInsertStep() &gt;&gt; null
  }

  def &quot;execute doesn&#39;t throw exceptions when no records found&quot;() {
    when:
    tested.execute(&quot;dummy&quot;)

    then:
    noExceptionThrown()
  }
}
class MyClass {
  DSLContext jooq
  Table tbl     // assume assigned a Table
  Field&lt;?&gt; fld  // assume assigned a Field

  MyClass(DSLContext jooq, Table tbl, Field&lt;?&gt; fld) {
    this.jooq = jooq
    this.tbl = tbl
    this.fld = fld
  }

  void execute(String value) {
    List&lt;Record&gt; records = fetchRecords(value)
    InsertValuesStepN&lt;Record&gt; insertStep = createInsertStep()
    // steps to complete insertStep elided
    insertStep?.execute()
  }

  protected Result&lt;Record&gt; fetchRecords(String value) {
    return jooq.select(DSL.asterisk()).from(tbl).where(fld.eq(value)).fetch()
  }

  protected InsertValuesStepN&lt;Record&gt; 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.

huangapple
  • 本文由 发表于 2023年3月15日 21:15:30
  • 转载请务必保留本文链接:https://go.coder-hub.com/75745239.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定