La nueva característica ZLayer en ZIO 1.0.0-RC18 + es una mejora significativa en el patrón del módulo anterior, lo que hace que agregar nuevos servicios sea mucho más rápido y fácil. Sin embargo, en la práctica, he descubierto que puede llevar un tiempo dominar este idioma.
A continuación se muestra un ejemplo anotado de la versión final de mi código de prueba en el que analizo varios casos de uso. Muchas gracias a Adam Fraser por ayudarme a optimizar y refinar mi trabajo. Los servicios se simplifican intencionalmente, por lo que esperamos que sean lo suficientemente claros como para leerlos rápidamente.
Supongo que tiene una comprensión básica de las pruebas ZIO y que está familiarizado con la información básica sobre los módulos.
Todo el código se ejecuta en pruebas zio y es un solo archivo.
Aquí está el consejo:
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")
Nombres
Entonces, llegamos a nuestro primer servicio - Nombres (Nombres)
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)
}
Todo aquí está dentro del marco de un patrón modular típico.
- Declarar nombres como un alias de tipo para Has
- En el objeto, defina Servicio como un rasgo
- Cree una implementación (por supuesto, puede crear múltiples),
- Cree un ZLayer dentro del objeto para la implementación dada. La convención ZIO tiende a llamarlos en tiempo real.
- Se agrega un objeto de paquete que proporciona un acceso directo fácil de acceder.
En vivo se usa lo
ZLayer.fromService
que se define como:
def fromService[A: Tagged, B: Tagged](f: A => B): ZLayer[Has[A], Nothing, Has[B]
Ignorando Etiquetado (esto es necesario para que todos los Has / Layers funcionen), puede ver que aquí se usa la función f: A => B , que en este caso es solo un constructor de la clase de caso
NamesImpl
.
Como puede ver, Names requiere Random del entorno zio para funcionar.
Aquí hay una prueba:
def namesTest = testM("names test") {
for {
name <- names.randomName
} yield {
assert(firstNames.contains(name))(equalTo(true))
}
}
Se utiliza
ZIO.accessM
para extraer nombres del entorno. _.get
recupera el servicio
Proporcionamos nombres para la prueba de la siguiente manera:
suite("needs Names")(
namesTest
).provideCustomLayer(Names.live),
provideCustomLayer
agrega la capa de nombres al entorno existente.
Equipos
La esencia de los equipos (equipos) es probar las dependencias entre módulos, que hemos creado.
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)
}
Los equipos seleccionarán un equipo de los nombres disponibles por tamaño .
Siguiendo los patrones de uso del módulo, aunque pickTeam necesita nombres para funcionar , no lo colocamos en ZIO [Names, Nothing, Set [String]] ; en cambio, guardamos una referencia a él
TeamsImpl
.
Nuestra primera prueba es simple.
def justTeamsTest = testM("small team test") {
for {
team <- teams.pickTeam(1)
} yield {
assert(team.size)(equalTo(1))
}
}
Para ejecutarlo, debemos darle una capa de equipos:
suite("needs just Team")(
justTeamsTest
).provideCustomLayer(Names.live >>> Teams.live),
¿Qué es ">>>"?
Esta es una composición vertical. Indica que necesitamos la capa de nombres , que necesita la capa de equipos .
Sin embargo, al ejecutar esto, hay un pequeño problema.
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
Volviendo a la definición
NamesImpl
case class NamesImpl(random: Random.Service) extends Names.Service {
println(s"created namesImpl")
def randomName =
random.nextInt(firstNames.size).map(firstNames(_))
}
Entonces el nuestro
NamesImpl
es creado dos veces. ¿Cuál es el riesgo si nuestro servicio contiene algún recurso de sistema de aplicación único? De hecho, resulta que el problema no está en absoluto en el mecanismo de capas: las capas se recuerdan y no se crean varias veces en el gráfico de dependencia. Esto es en realidad un artefacto del entorno de prueba.
Cambiemos nuestro conjunto de pruebas a:
suite("needs just Team")(
justTeamsTest
).provideCustomLayerShared(Names.live >>> Teams.live),
Esto soluciona un problema, lo que significa que la capa se crea solo una vez en la prueba.
JustTeamsTest solo requiere equipos . Pero, ¿y si quisiera acceder a Equipos y Nombres ?
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
}
Para que esto funcione, necesitamos proporcionar ambos:
suite("needs Names and Teams")(
inMyTeam
).provideCustomLayer(Names.live ++ (Names.live >>> Teams.live)),
Aquí estamos usando el combinador ++ para crear la capa de Nombres con Teams . Preste atención a la precedencia del operador y paréntesis adicionales
(Names.live >>> Teams.live)
Al principio, me enamoré de mí mismo; de lo contrario, el compilador no lo hará bien.
Historia
La historia es un poco más complicada.
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))
}
}
El constructor
HistoryImpl
requiere muchos nombres . Pero la única forma de obtenerlo es sacándolo de los equipos . Y requiere ZIO, por lo que lo usamos ZLayer.fromServiceM
para darnos lo que necesitamos.
La prueba se lleva a cabo de la misma manera que antes:
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))
Y eso es todo.
Errores arrojables
El código anterior supone que está devolviendo ZLayer [R, Nothing, T]; en otras palabras, la construcción del servicio de entorno es de tipo Nothing. Pero si hace algo como leer un archivo o una base de datos, lo más probable es que sea ZLayer [R, Throwable, T], porque este tipo de cosas a menudo implica el factor muy externo que está causando la excepción. Entonces imagine que hay un error en la construcción de nombres. Hay una manera para que sus pruebas eviten esto:
val live: ZLayer[Random, Throwable, Names] = ???
luego al final de la prueba
.provideCustomLayer(Names.live).mapError(TestFailure.test)
mapError
convierte el objeto throwable
en una falla de prueba, eso es lo que desea, podría decir que el archivo de prueba no existe o algo así.
Más casos de ZEnv
Los elementos "estándar" del entorno incluyen Reloj y Aleatorio. Ya hemos usado Aleatorio en nuestros nombres. Pero, ¿qué sucede si también queremos que uno de estos elementos "reduzca" aún más nuestras dependencias? Para hacer esto, creé una segunda versión de History - History2 - y aquí se necesita Clock para crear una instancia.
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)
}
}
Este no es un ejemplo muy útil, pero la parte importante es que la línea
someTime <- ZIO.accessM[Clock](_.get.nanoTime)
nos obliga a proporcionar el reloj en el lugar correcto.
Ahora
.provideCustomLayer
puede agregar nuestra capa a la pila de capas y mágicamente aparece Aleatorio en Nombres. Pero esto no sucederá durante las horas requeridas a continuación en History2. Por lo tanto, el siguiente código NO se compila:
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)),
En cambio, debe proporcionar el
History2.live
reloj explícitamente, que se realiza de la siguiente manera:
suite("needs History2 and Teams")(
wonLastYear2
).provideCustomLayerShared((Names.live >>> Teams.live) ++ (((Names.live >>> Teams.live) ++ Clock.any) >>> History2.live))
Clock.any
Es una función que obtiene cualquier reloj disponible desde arriba. En este caso, será un reloj de prueba, porque no intentamos usarlo Clock.live
.
Fuente
A continuación se muestra el código fuente completo (excluyendo el que se puede lanzar):
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))
}
Para preguntas más avanzadas, comuníquese con Discord # zio-users o visite el sitio web y la documentación de zio.
Aprende más sobre el curso.