ZIO 1.0.0-RC18 +中的新ZLayer功能是对旧模块模式的重大改进,使添加新服务变得更快,更容易。但是,在实践中,我发现掌握这一习语可能需要一段时间。
下面是我的测试代码最终版本的带注释的示例,其中我查看了一些用例。非常感谢Adam Fraser帮助我优化和完善了我的工作。这些服务是有意简化的,因此希望它们足够清晰以便快速阅读。
我假设您对ZIO测试有基本的了解,并且熟悉有关模块的基本信息。
所有代码都在zio测试中运行,并且是一个文件。
提示:
import zio._
import zio.test._
import zio.random.Random
import Assertion._
object LayerTests extends DefaultRunnableSpec {
type Names = Has[Names.Service]
type Teams = Has[Teams.Service]
type History = Has[History.Service]
val firstNames = Vector( "Ed", "Jane", "Joe", "Linda", "Sue", "Tim", "Tom")
名字
因此,我们开始了我们的第一项服务-名称(名称)
type Names = Has[Names.Service]
object Names {
trait Service {
def randomName: UIO[String]
}
case class NamesImpl(random: Random.Service) extends Names.Service {
println(s"created namesImpl")
def randomName =
random.nextInt(firstNames.size).map(firstNames(_))
}
val live: ZLayer[Random, Nothing, Names] =
ZLayer.fromService(NamesImpl)
}
package object names {
def randomName = ZIO.accessM[Names](_.get.randomName)
}
这里的所有内容都在典型的模块化模式的框架内。
- 将名称声明为Has的类型别名
- 在对象中,将“服务”定义为特征
- 创建一个实现(当然您可以创建多个实现),
- 在给定实现的对象内部创建ZLayer。ZIO约定倾向于实时调用它们。
- 添加了一个包对象,它提供了易于访问的快捷方式。
在现场使用
ZLayer.fromService
它的定义为:
def fromService[A: Tagged, B: Tagged](f: A => B): ZLayer[Has[A], Nothing, Has[B]
忽略Tagged(这对于所有Has / Lays层都是必需的),您可以看到这里使用了函数f:A => B-在这种情况下,它只是case类的构造函数
NamesImpl
。
如您所见,“名称”需要zio环境中的“随机”才能起作用。
这是一个测试:
def namesTest = testM("names test") {
for {
name <- names.randomName
} yield {
assert(firstNames.contains(name))(equalTo(true))
}
}
它用于从环境中
ZIO.accessM
提取名称。_.get
检索服务。
我们为测试提供以下名称:
suite("needs Names")(
namesTest
).provideCustomLayer(Names.live),
provideCustomLayer
将“ 名称”层添加到现有环境。
队伍
团队(团队) 的本质是测试我们创建的模块之间的依赖关系。
object Teams {
trait Service {
def pickTeam(size: Int): UIO[Set[String]]
}
case class TeamsImpl(names: Names.Service) extends Service {
def pickTeam(size: Int) =
ZIO.collectAll(0.until(size).map { _ => names.randomName}).map(_.toSet ) // , , < !
}
val live: ZLayer[Names, Nothing, Teams] =
ZLayer.fromService(TeamsImpl)
}
团队将从可用名称中按大小选择一个团队。
按照模块使用模式,尽管pickTeam需要使用Names才能工作,但我们没有将其放在ZIO [Names,Nothing,Set [String]]中 -而是在其中保留对其的引用
TeamsImpl
。
我们的第一个测试很简单。
def justTeamsTest = testM("small team test") {
for {
team <- teams.pickTeam(1)
} yield {
assert(team.size)(equalTo(1))
}
}
要运行它,我们需要给它一个Teams层:
suite("needs just Team")(
justTeamsTest
).provideCustomLayer(Names.live >>> Teams.live),
什么是“ >>>”?
这是一个垂直构图。这表明我们需要名称层,而团队层也需要。
但是,运行此程序时,存在一个小问题。
created namesImpl
created namesImpl
[32m+[0m individually
[32m+[0m needs just Team
[32m+[0m small team test
[36mRan 1 test in 225 ms: 1 succeeded, 0 ignored, 0 failed[0m
返回定义
NamesImpl
case class NamesImpl(random: Random.Service) extends Names.Service {
println(s"created namesImpl")
def randomName =
random.nextInt(firstNames.size).map(firstNames(_))
}
所以我们的
NamesImpl
被创建了两次。如果我们的服务包含一些独特的应用程序系统资源,会有什么风险?实际上,事实证明,问题根本不在“层”机制中-记住了层,并且在依赖图中没有多次创建层。这实际上是测试环境的产物。
让我们将测试套件更改为:
suite("needs just Team")(
justTeamsTest
).provideCustomLayerShared(Names.live >>> Teams.live),
这解决了一个问题,这意味着该层在测试中仅创建一次,而
JustTeamsTest只需要teams。但是,如果我想访问团队和姓名怎么办?
def inMyTeam = testM("combines names and teams") {
for {
name <- names.randomName
team <- teams.pickTeam(5)
_ = if (team.contains(name)) println("one of mine")
else println("not mine")
} yield assertCompletes
}
为此,我们需要同时提供以下两项:
suite("needs Names and Teams")(
inMyTeam
).provideCustomLayer(Names.live ++ (Names.live >>> Teams.live)),
在这里,我们使用++组合器通过Teams创建Names层。注意运算符的优先级和多余的括号
(Names.live >>> Teams.live)
一开始,我本人很喜欢-否则编译器将无法正确执行。
历史
历史有点复杂。
object History {
trait Service {
def wonLastYear(team: Set[String]): Boolean
}
case class HistoryImpl(lastYearsWinners: Set[String]) extends Service {
def wonLastYear(team: Set[String]) = lastYearsWinners == team
}
val live: ZLayer[Teams, Nothing, History] = ZLayer.fromServiceM { teams =>
teams.pickTeam(5).map(nt => HistoryImpl(nt))
}
}
构造函数
HistoryImpl
需要许多Names。但是获得它的唯一方法是从Teams中撤出它。它需要ZIO-因此我们使用ZLayer.fromServiceM
它来满足我们的需求。
该测试以与之前相同的方式进行:
def wonLastYear = testM("won last year") {
for {
team <- teams.pickTeams(5)
ly <- history.wonLastYear(team)
} yield assertCompletes
}
suite("needs History and Teams")(
wonLastYear
).provideCustomLayerShared((Names.live >>> Teams.live) ++ (Names.live >>> Teams.live >>> History.live))
就这样。
抛出错误
上面的代码假定您正在返回ZLayer [R,Nothing,T]-换句话说,环境服务构造的类型为Nothing。但是,如果它执行的操作类似于从文件或数据库中读取数据,则很有可能是ZLayer [R,Throwable,T]-因为此类操作通常涉及导致异常的外部因素。因此,假设Names构造中存在错误。有一种方法可以解决此问题:
val live: ZLayer[Random, Throwable, Names] = ???
然后在测试结束时
.provideCustomLayer(Names.live).mapError(TestFailure.test)
mapError
将对象throwable
变成测试失败-这就是您想要的-可能表示测试文件不存在或类似的东西。
更多ZEnv案例
环境的“标准”元素包括“时钟”和“随机”。我们已经在名称中使用了Random。但是,如果我们还希望这些元素之一进一步“降低”我们的依赖关系呢?为此,我创建了History的第二个版本-History2-在这里需要Clock来创建实例。
object History2 {
trait Service {
def wonLastYear(team: Set[String]): Boolean
}
case class History2Impl(lastYearsWinners: Set[String], lastYear: Long) extends Service {
def wonLastYear(team: Set[String]) = lastYearsWinners == team
}
val live: ZLayer[Clock with Teams, Nothing, History2] = ZLayer.fromEffect {
for {
someTime <- ZIO.accessM[Clock](_.get.nanoTime)
team <- teams.pickTeam(5)
} yield History2Impl(team, someTime)
}
}
这不是一个非常有用的示例,但重要的部分是该行
someTime <- ZIO.accessM[Clock](_.get.nanoTime)
迫使我们在正确的位置提供时钟。
现在
.provideCustomLayer
可以将我们的图层添加到图层堆栈中,并神奇地将Random弹出到Names中。但这不会在History2中要求的以下时间内发生。因此,以下代码无法编译:
def wonLastYear2 = testM("won last year") {
for {
team <- teams.pickTeam(5)
_ <- history2.wonLastYear(team)
} yield assertCompletes
}
// ...
suite("needs History2 and Teams")(
wonLastYear2
).provideCustomLayerShared((Names.live >>> Teams.live) ++ (Names.live >>> Teams.live >>> History2.live)),
相反,您需要
History2.live
显式提供时钟,方法如下:
suite("needs History2 and Teams")(
wonLastYear2
).provideCustomLayerShared((Names.live >>> Teams.live) ++ (((Names.live >>> Teams.live) ++ Clock.any) >>> History2.live))
Clock.any
是从上方获取任何可用时钟的函数。在这种情况下,它将是一个测试时钟,因为我们没有尝试使用Clock.live
。
资源
完整的源代码(不包括throwable)如下所示:
import zio._
import zio.test._
import zio.random.Random
import Assertion._
import zio._
import zio.test._
import zio.random.Random
import zio.clock.Clock
import Assertion._
object LayerTests extends DefaultRunnableSpec {
type Names = Has[Names.Service]
type Teams = Has[Teams.Service]
type History = Has[History.Service]
type History2 = Has[History2.Service]
val firstNames = Vector( "Ed", "Jane", "Joe", "Linda", "Sue", "Tim", "Tom")
object Names {
trait Service {
def randomName: UIO[String]
}
case class NamesImpl(random: Random.Service) extends Names.Service {
println(s"created namesImpl")
def randomName =
random.nextInt(firstNames.size).map(firstNames(_))
}
val live: ZLayer[Random, Nothing, Names] =
ZLayer.fromService(NamesImpl)
}
object Teams {
trait Service {
def pickTeam(size: Int): UIO[Set[String]]
}
case class TeamsImpl(names: Names.Service) extends Service {
def pickTeam(size: Int) =
ZIO.collectAll(0.until(size).map { _ => names.randomName}).map(_.toSet ) // , , < !
}
val live: ZLayer[Names, Nothing, Teams] =
ZLayer.fromService(TeamsImpl)
}
object History {
trait Service {
def wonLastYear(team: Set[String]): Boolean
}
case class HistoryImpl(lastYearsWinners: Set[String]) extends Service {
def wonLastYear(team: Set[String]) = lastYearsWinners == team
}
val live: ZLayer[Teams, Nothing, History] = ZLayer.fromServiceM { teams =>
teams.pickTeam(5).map(nt => HistoryImpl(nt))
}
}
object History2 {
trait Service {
def wonLastYear(team: Set[String]): Boolean
}
case class History2Impl(lastYearsWinners: Set[String], lastYear: Long) extends Service {
def wonLastYear(team: Set[String]) = lastYearsWinners == team
}
val live: ZLayer[Clock with Teams, Nothing, History2] = ZLayer.fromEffect {
for {
someTime <- ZIO.accessM[Clock](_.get.nanoTime)
team <- teams.pickTeam(5)
} yield History2Impl(team, someTime)
}
}
def namesTest = testM("names test") {
for {
name <- names.randomName
} yield {
assert(firstNames.contains(name))(equalTo(true))
}
}
def justTeamsTest = testM("small team test") {
for {
team <- teams.pickTeam(1)
} yield {
assert(team.size)(equalTo(1))
}
}
def inMyTeam = testM("combines names and teams") {
for {
name <- names.randomName
team <- teams.pickTeam(5)
_ = if (team.contains(name)) println("one of mine")
else println("not mine")
} yield assertCompletes
}
def wonLastYear = testM("won last year") {
for {
team <- teams.pickTeam(5)
_ <- history.wonLastYear(team)
} yield assertCompletes
}
def wonLastYear2 = testM("won last year") {
for {
team <- teams.pickTeam(5)
_ <- history2.wonLastYear(team)
} yield assertCompletes
}
val individually = suite("individually")(
suite("needs Names")(
namesTest
).provideCustomLayer(Names.live),
suite("needs just Team")(
justTeamsTest
).provideCustomLayer(Names.live >>> Teams.live),
suite("needs Names and Teams")(
inMyTeam
).provideCustomLayer(Names.live ++ (Names.live >>> Teams.live)),
suite("needs History and Teams")(
wonLastYear
).provideCustomLayerShared((Names.live >>> Teams.live) ++ (Names.live >>> Teams.live >>> History.live)),
suite("needs History2 and Teams")(
wonLastYear2
).provideCustomLayerShared((Names.live >>> Teams.live) ++ (((Names.live >>> Teams.live) ++ Clock.any) >>> History2.live))
)
val altogether = suite("all together")(
suite("needs Names")(
namesTest
),
suite("needs just Team")(
justTeamsTest
),
suite("needs Names and Teams")(
inMyTeam
),
suite("needs History and Teams")(
wonLastYear
),
).provideCustomLayerShared(Names.live ++ (Names.live >>> Teams.live) ++ (Names.live >>> Teams.live >>> History.live))
override def spec = (
individually
)
}
import LayerTests._
package object names {
def randomName = ZIO.accessM[Names](_.get.randomName)
}
package object teams {
def pickTeam(nPicks: Int) = ZIO.accessM[Teams](_.get.pickTeam(nPicks))
}
package object history {
def wonLastYear(team: Set[String]) = ZIO.access[History](_.get.wonLastYear(team))
}
package object history2 {
def wonLastYear(team: Set[String]) = ZIO.access[History2](_.get.wonLastYear(team))
}
有关更多高级问题,请联系Discord#zio-users或访问zio 网站和文档。
了解有关该课程的更多信息。