Groovy Spock - 模拟方法未返回所需值

Groovy Spock - mocked method not returning desired value

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

@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 class 是一个从 application.properties 获取属性的 @ConfigurationProperties class,它看起来像这样:

@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>

你这里有几个问题:

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

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

  3. 交互 paymentsMapper.getPaymentIds(_ as String, _ as long) 在您的情况下不匹配,因此 null 默认值 return。原因是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()
    }
    

    但是模拟交互在特征方法之外,这可能会让读者想知道特征方法到底做了什么。所以,从技术上讲这是可行的,但在我看来,一个易于阅读的测试看起来不同。

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