You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spock/Groovy测试同时验证调用次数与异常失败,单独验证正常?

Spock测试同时验证方法调用次数与异常时失败的问题

问题场景

在Spock/Groovy测试中,当同一个测试用例同时验证方法调用次数和抛出的异常时,测试失败;但单独验证任意一项时,测试均可正常通过。

可正常通过的测试用例1(仅验证异常)

def "should throw AppointmentNotFoundException when appointment do not exist" () {
  given:
  long nonExistingAppointmentId = 69L

  when:
  appointmentRepository.findById(nonExistingAppointmentId) >> { throw new AppointmentNotFoundException("Appointment with id " + nonExistingAppointmentId + " not found") }
  findAppointmentQueryHandler.handle(nonExistingAppointmentId)

  then:
  thrown(AppointmentNotFoundException)
}

可正常通过的测试用例2(仅验证方法调用次数)

def "should throw AppointmentNotFoundException when appointment do not exist" () {
  given:
  long nonExistingAppointmentId = 69L

  when:
  appointmentRepository.findById(nonExistingAppointmentId) >> { throw new AppointmentNotFoundException("Appointment with id " + nonExistingAppointmentId + " not found") }
  findAppointmentQueryHandler.handle(nonExistingAppointmentId)

  then:
  1 * appointmentRepository.findById(69L)
}

合并后失败的测试用例

def "should throw AppointmentNotFoundException when appointment do not exist" () {
  given:
  long nonExistingAppointmentId = 69L

  when:
  appointmentRepository.findById(nonExistingAppointmentId) >> { throw new AppointmentNotFoundException("Appointment with id " + nonExistingAppointmentId + " not found") }
  findAppointmentQueryHandler.handle(nonExistingAppointmentId)

  then:
  1 * appointmentRepository.findById(69L)
  thrown(AppointmentNotFoundException)
}

错误信息

Expected exception of type 'com.bbc.anotherhospital.exceptions.AppointmentNotFoundException', but no exception was thrown
    at org.spockframework.lang.SpecInternals.checkExceptionThrown(SpecInternals.java:84)
    at org.spockframework.lang.SpecInternals.thrownImpl(SpecInternals.java:71)
    at com.bbc.anotherhospital.appointment.handlers.FindAppointmentQueryHandlerSpec.should throw AppointmentNotFoundException when appointment do not exist

依赖配置

<dependency>
  <groupId>org.apache.groovy</groupId>
  <artifactId>groovy-all</artifactId>
  <version>4.0.15</version>
  <type>pom</type>
</dependency>
<dependency>
  <groupId>org.spockframework</groupId>
  <artifactId>spock-core</artifactId>
  <version>2.4-M1-groovy-4.0</version>
  <scope>test</scope>
</dependency>

问题原因及解决方案

原因

Spock中,交互断言(如1 * 方法调用)的优先级高于存根(>>)设置。当你在then块中添加1 * appointmentRepository.findById(69L)时,该断言会覆盖你之前在when块中设置的存根行为(抛出异常),此时findById方法会返回对应类型的默认值(如null),而非抛出异常,导致thrown断言失败。

解决方案

有两种可行的修正方式:

方式1:将存根行为与交互断言合并

把存根的抛出异常逻辑直接整合到交互断言中,确保调用次数和行为同时生效:

def "should throw AppointmentNotFoundException when appointment do not exist" () {
  given:
  long nonExistingAppointmentId = 69L

  when:
  findAppointmentQueryHandler.handle(nonExistingAppointmentId)

  then:
  1 * appointmentRepository.findById(nonExistingAppointmentId) >> { 
    throw new AppointmentNotFoundException("Appointment with id " + nonExistingAppointmentId + " not found") 
  }
  thrown(AppointmentNotFoundException)
}

方式2:将存根设置移至given块

将存根逻辑放在测试准备阶段的given块中,then块仅验证调用次数和异常:

def "should throw AppointmentNotFoundException when appointment do not exist" () {
  given:
  long nonExistingAppointmentId = 69L
  appointmentRepository.findById(nonExistingAppointmentId) >> { 
    throw new AppointmentNotFoundException("Appointment with id " + nonExistingAppointmentId + " not found") 
  }

  when:
  findAppointmentQueryHandler.handle(nonExistingAppointmentId)

  then:
  1 * appointmentRepository.findById(nonExistingAppointmentId)
  thrown(AppointmentNotFoundException)
}

内容的提问来源于stack exchange,提问作者michalbarnat

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.06 07:07:48