具有隐式参数的Scala中的伪造系统时钟模式

伪系统时钟是一种设计模式,用于解决严重依赖系统时间的程序的可测试性问题。 如果业务逻辑流取决于当前系统时间,那么测试各种流就变得很麻烦甚至是不可能的。 此类问题场景的示例包括:

  1. 某些业务流程仅在周末运行(或被忽略)
  2. 自某些其他事件发生后一个小时后才触发某些逻辑
  3. 当两个事件恰好同时发生(通常为1 ms精度)时,应该发生一些情况

上面的每种情况都会带来一系列独特的挑战。 从字面上看,我们的单元测试只能在特定的一天(1)运行或睡眠一个小时才能观察到某些行为。 在某些情况下,方案(3)甚至可能无法测试,因为系统时钟随时都可能滴答1毫秒,因此使测试不可靠。

伪系统时钟通过在简单接口上抽象系统时间来解决这些问题。 本质上你从不打电话
new Date()new GregorianCalendar()System.currentTimeMillis()但始终依赖于此:

import org.joda.time.{DateTime, Instant}

trait Clock {

    def now(): Instant

    def dateNow(): DateTime

}

如您所见,我依赖于Joda Time库。 由于我们已经在Scala土地上,因此不妨考虑一下
标量时间nscala时间包装器。 此外,抽象名称Clock不是巧合。 它简短而具有描述性,但更重要的是,它模仿Java 8中的java.time.Clock类-恰好解决了在JDK级别上讨论的同一问题! 但是,由于Java 8仍然不在这里,让我们保留我们甜美而小巧的抽象。

通常使用的标准实现只是将时间委托给系统时间:

import org.joda.time.{Instant, DateTime}

object SystemClock extends Clock {

    def now() = Instant.now()

    def dateNow() = DateTime.now()

}

为了进行单元测试,我们将开发其他实现,但首先让我们关注使用场景。 在典型的Spring / JavaEE应用程序中,伪造的系统时钟可以变成容器可以轻松注入的依赖项。 这使得对系统时间的依赖变得明确且易于管理,尤其是在测试中:

@Controller
class FooController @Autowired() (fooService: FooService, clock: Clock) {

    def postFoo(name: String) =
        fooService store new Foo(name, clock)

}

在这里,我使用Spring构造函数注入,要求容器提供一些Clock实现。 当然,在这种情况下, SystemClock被标记为@Service 。 在单元测试中,我可以通过假的实现,而在集成测试中,我可以在上下文中放置另一个@Primary bean,以遮盖SystemClock

这很好用,但是对于某些类型的对象(即实体/ DTO bean和实用程序( static )类)却很痛苦。 这些通常不是由Spring管理的,因此它不能向它们注入Clock Bean。 这迫使我们通过
Clock从最后一个“管理”层手动:

class Foo(fooName: String, clock: Clock) {

    val name = fooName
    val time = clock.dateNow()

}

类似地:

object TimeUtil {

    def firstFridayOfNextMonth(clock: Clock) = //...

}

从设计角度来看,这还不错。 Foo构造函数和firstFridayOfNextMonth()方法都依赖于系统时间,因此我们将其明确化。 另一方面,必须将Clock依赖项拖到某些层上,有时要拖到很多层,以便可以在某处以一种方法使用它。 同样,这本身还不错。 如果您的高级方法具有Clock参数,则从一开始就知道它依赖于当前时间。 但是似乎仍然有很多样板和开销,却几乎没有收益。 幸运的是Scala可以在以下方面为我们提供帮助:

让我们重构一下解决方案,使Clock是一个隐式参数:

@Controller
class FooController(fooService: FooService) {

    def postFoo(name: String)(implicit clock: Clock) =
        fooService store new Foo(name)

}

@Service
class FooService(fooRepository: FooRepository) {

    def store(foo: Foo)(implicit clock: Clock) =
        fooRepository storeInFuture foo

}

@Repository
class FooRepository {

    def storeInFuture(foo: Foo)(implicit clock: Clock) = {
        val friday = TimeUtil.firstFridayOfNextMonth()
        //...
    }

}

object TimeUtil {

    def firstFridayOfNextMonth()(implicit clock: Clock) = //...

}

注意,我们如何忽略第二个clock参数来调用fooRepository storeInFuture foo 。 然而,仅此还不够。 我们仍然必须提供一些Clock实例作为第二个参数,否则会出现编译错误:

could not find implicit value for parameter clock: com.blogspot.nurkiewicz.foo.Clock
    controller.postFoo("Abc")
                      ^

not enough arguments for method postFoo: (implicit clock: com.blogspot.nurkiewicz.foo.Clock)Unit.
Unspecified value parameter clock.
    controller.postFoo("Abc")
                      ^

编译器试图找到Clock参数的隐式值,但失败了。 但是我们真的很接近,最简单的解决方案是使用package对象

package com.blogspot.nurkiewicz.foo

package object foo {

    implicit val clock = SystemClock

}

前面定义SystemClock 。 这是发生的情况:每次我调用带有implicit clock: Clock的函数implicit clock: Clock com.blogspot.nurkiewicz.foo包中的implicit clock: Clock参数时,编译器将发现foo.clock隐式变量并将其透明地传递。 换句话说,以下代码段是等效的,但第二个代码段提供了显式Clock ,因此忽略了隐式代码:

TimeUtil.firstFridayOfNextMonth()
TimeUtil.firstFridayOfNextMonth()(SystemClock)

也等效(编译器将第一种形式转换为第二种形式):

fooService.store(foo)
fooService.store(foo)(SystemClock)

有趣的是,在字节码级别上,隐式参数与普通参数没有什么不同,因此,如果要从Java调用此类方法,则传递Clock实例是强制性和显式的。

implicit clock参数似乎工作得很好。 它隐藏了普遍存在的依赖关系,同时仍然提供了覆盖它的可能性。 例如:

测验

抽象系统时间的全部目的是通过完全控制时间流来进行单元测试。 让我们从一个简单的假系统时钟实现开始,该实现始终返回相同的指定时间:

class FakeClock(fixed: DateTime) extends Clock {
    def now() = fixed.toInstant

    def dateNow() = fixed
}

当然,您可以随意放置任何逻辑:通过任意值提前时间,加快时间等。您明白了。 现在请记住, implicit参数的原因是在正常生产代码中隐藏Clock ,同时仍然能够提供替代实现。 有两种方法:在测试中显式地传递FakeClock

val fakeClock = new FakeClock(
   new DateTime(2013, 7, 15, 0, 0, DateTimeZone.UTC))

controller.postFoo("Abc")(fakeClock)

或使其隐式但更特定于编译器解析机制:

implicit val fakeClock = new FakeClock(
   new DateTime(2013, 7, 15, 0, 0, DateTimeZone.UTC))

controller.postFoo("Abc")

后一种方法更易于维护,因为您不必始终记住将fakeClock传递给fakeClock方法。 当然, fakeClock可以更全局地定义为字段,甚至可以在测试包对象内部定义。 无论我们选择哪种技术来提供fakeClock ,都会在对服务,存储库和实用程序的所有调用中使用它。 当我们为该参数赋予显式值时,隐式参数解析将被忽略。

问题与总结

以上针对严重依赖时间的测试系统的解决方案本身并非没有问题。 首先隐式
Clock参数必须在所有层中传播直至客户端代码。 注意,仅在存储库/实用程序层中需要Clock ,而我们必须将它向上拖动到控制器层。 没什么大不了的,因为编译器会为我们填充它,但是我们的大多数方法迟早都会包含这个额外的参数。

同样,在代码之上运行的Java和框架也不知道在编译时会发生Scala隐式解析。 因此,例如我们的Spring MVC控制器将无法工作,因为Spring不知道SystemClock隐式变量。 尽管可以使用WebArgumentResolver

伪系统时钟模式通常仅在一致使用时才起作用。 如果您直接使用实时而不是Clock抽象时,甚至只有一个地方,那么在寻找测试失败原因时会很幸运。 这同样适用于库和SQL查询。 因此,如果您要设计一个依赖当前时间的库,请考虑提供可插入的Clock抽象,以便客户端代码可以提供自定义实现,例如FakeClock 。 另一方面,在SQL中,不要依赖像NOW()这样的函数,而总是显式地从代码中提供日期(因此从自定义Clock提供日期)。


翻译自: https://www.javacodegeeks.com/2013/07/fake-system-clock-pattern-in-scala-with-implicit-parameters.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值