【问题标题】:Groovy Spock - mocked method not returning desired valueGroovy Spock - 模拟方法未返回所需值
【发布时间】:2021-09-27 08:34:29
【问题描述】:

到目前为止,我有一个简单的 Java 类,它使用单一方法从数据库中获取数据然后进行一些更新,它看起来像这样:

@Slf4j
@Service
public class PaymentServiceImpl implements PaymentService {

    private final PaymentsMapper paymentsMapper;
    private final MyProperties myProperties;

    public PaymentServiceImpl(PaymentsMapper paymentsMapper,
                                 MyProperties myProperties) {
        this.paymentsMapper = paymentsMapper;
        this.myProperties = myProperties;
    }

    @Override
    @Transactional
    public void doSomething() {

        List<String> ids = paymentsMapper.getPaymentIds(
                myProperties.getPayments().getOperator(),
                myProperties.getPayments().getPeriod().getDuration().getSeconds());

        long updated = 0;
        for (String id : ids ) {
            updated += paymentsMapper.updatedPaymentsWithId(id);
        }
    }
}

为了记录,MyProperties 类是一个从application.properties 获取属性的@ConfigurationProperties 类,它看起来像这样:

@Data
@Configuration("myProperties")
@ConfigurationProperties(prefix = "my")
@PropertySource("classpath:application.properties")
public class MyProperties {

    private Payments payments;

    @Getter
    @Setter
    public static class Payments {
        private String operator;
        private Period period;
        @Getter @Setter
        public static class Period{
            private Duration duration;
        }
    }
}

现在我正在尝试为这种方法编写一个简单的测试,我想出了这个:

class PaymentServiceImplTest extends Specification {

    @Shared
    PaymentsMapper paymentsMapper = Mock(PaymentsMapper)
    @Shared
    MyProperties properties = new MyProperties()
    @Shared
    PaymentServiceImpl paymentService = new PaymentServiceImpl(paymentsMapper, properties)

    def setupSpec() {
        properties.setPayments(new MyProperties.Payments())
        properties.getPayments().setOperator('OP1')
        properties.getPayments().setPeriod(new MyProperties.Payments.Period())
        properties.getPayments().getPeriod().setDuration(Duration.ofSeconds(3600))
    }


    def 'update pending acceptation payment ids'() {
        given:
        paymentsMapper.getPaymentIds(_ as String, _ as long) >> Arrays.asList('1', '2', '3')

        when:
        paymentService.doSomething()

        then:
        3 * paymentsMapper.updatedPaymentsWithId(_ as String)
    }
}

但尝试运行测试我得到一个空指针异常:

java.lang.NullPointerException
    at com.example.PaymentServiceImpl.doSomething(PaymentServiceImpl.java:33)
    at com.example.service.PaymentServiceImplTest.update pending acceptation payment ids(PaymentServiceImplTest.groovy:33)

谁能告诉我这是为什么?为什么我会在那里获得 NPE?

我对 Spock 的 pom.xml 依赖如下:

<dependency>
    <groupId>org.spockframework</groupId>
    <artifactId>spock-core</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.spockframework</groupId>
    <artifactId>spock-spring</artifactId>
    <scope>test</scope>
</dependency>

【问题讨论】:

  • myProperties.getPayments() 是否返回非空值? .getPeriod().getDuration().getSeconds() 中的每个调用都有相同的问题。
  • 它们都返回setupSpec() 中定义的非空值。问题在于paymentsMapper.getPaymentsIds(),而不是定义的列表返回null。我不知道为什么。

标签: java spring groovy spock


【解决方案1】:

这里有几个问题:

  1. 您使用@Shared 变量。你应该只在绝对必要时这样做,例如如果您需要实例化昂贵的对象,例如数据库连接。否则,特性方法 A 的上下文可能会溢出到 B 中,因为共享对象已被修改。然后特征突然变得对执行顺序敏感,这是不应该的。

  2. 您的模拟也是共享的,但您试图从功能方法中指定存根结果和交互。如果您在多种功能方法中这样做,这不会像您预期的那样工作。在这种情况下,您应该为每个功能创建一个新实例,这也意味着共享变量不再有意义。唯一可能有意义的情况是,如果使用完全相同的模拟实例而不对所有功能方法进行任何更改。但随后像3 * mock.doSomething() 这样的交互将继续计算功能。另外,mock 总是很便宜,那么为什么首先要共享一个 mock?

  3. 交互paymentsMapper.getPaymentIds(_ as String, _ as long) 在您的情况下将不匹配,因此null 默认返回值。原因是 Groovy 中第二个参数的运行时类型是Long。因此,您需要将参数列表更改为 (_ as String, _ as Long) 或更简单的 (_ as String, _) (_, _)(*_),具体取决于您的匹配需要的具体程度。

因此您可以执行以下任一操作:

  • 不要将@Shared 用于您的字段并将setupSpec 重命名为setup。很简单,规范运行不会因此而明显变慢。

  • 如果您坚持使用共享变量,请确保在 setupSpec 方法中仅设置一次两个模拟交互或在模拟定义中内联,即类似

    @Shared
    PaymentsMapper paymentsMapper = Mock(PaymentsMapper) {
      getPaymentIds(_ as String, _ as Long) >> Arrays.asList('1', '2', '3')
      3 * updatedPaymentsWithId(_ as String)
    }
    
    // ...
    
    def 'update pending acceptation payment ids'() {
      expect:
      paymentService.doSomething()
    }
    

    但是模拟交互在特征方法之外,这可能会让读者想知道特征方法实际上做了什么。因此,从技术上讲,这是可行的,但易于阅读的测试看起来与 IMO 不同。

  • 您也可能有在每个功能方法中实例化和配置共享模拟的想法。但是,对@Shared PaymentServiceImpl paymentService 的一次性分配将使用另一个实例或null。哦哦!你看到共享模拟的问题了吗?我想,这不值得你过早地优化,因为这就是我所相信的。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2019-08-29
    • 2018-10-09
    • 2023-03-26
    • 2018-03-31
    • 1970-01-01
    • 2016-06-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多