O novo recurso ZLayer no ZIO 1.0.0-RC18 + é uma melhoria significativa no antigo padrão de módulo, tornando a adição de novos serviços muito mais rápida e fácil. No entanto, na prática, descobri que pode demorar um pouco para dominar esse idioma.
Abaixo está um exemplo anotado da versão final do meu código de teste, na qual observo vários casos de uso. Muito obrigado a Adam Fraser por me ajudar a otimizar e refinar meu trabalho. Os serviços são intencionalmente simplificados, portanto, esperamos que sejam claros o suficiente para serem lidos rapidamente.
Suponho que você tenha um entendimento básico dos testes do ZIO e que esteja familiarizado com informações básicas sobre os módulos.
Todo o código é executado nos testes do zio e é um único arquivo.
Aqui está a dica:
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")
Nomes
Então, chegamos ao nosso primeiro serviço - Names (Names)
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)
}
Tudo aqui está dentro da estrutura de um padrão modular típico.
- Declarar nomes como um alias de tipo para Has
- No objeto, defina Serviço como uma característica
- Crie uma implementação (é claro que você pode criar várias),
- Crie um ZLayer dentro do objeto para a implementação fornecida. A convenção da ZIO costuma chamá-los em tempo real.
- É adicionado um objeto de pacote que fornece um atalho de fácil acesso.
Ao vivo, é usado,
ZLayer.fromService
definido como:
def fromService[A: Tagged, B: Tagged](f: A => B): ZLayer[Has[A], Nothing, Has[B]
Ignorando Tagged (isso é necessário para que todos os Has / Layers funcionem), você pode ver que aqui a função f: A => B é usada - que neste caso é apenas um construtor da classe de caso
NamesImpl
.
Como você pode ver, o Names requer o Random do ambiente zio para funcionar.
Aqui está um teste:
def namesTest = testM("names test") {
for {
name <- names.randomName
} yield {
assert(firstNames.contains(name))(equalTo(true))
}
}
Ele usa
ZIO.accessM
para extrair nomes do ambiente. _.get
recupera o serviço.
Fornecemos nomes para o teste da seguinte forma:
suite("needs Names")(
namesTest
).provideCustomLayer(Names.live),
provideCustomLayer
adiciona a camada de nomes ao ambiente existente.
Equipas
A essência das equipes (equipes) é testar as dependências entre os módulos que criamos.
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)
}
As equipes selecionam uma equipe entre os nomes disponíveis por tamanho .
Seguindo os padrões de uso do módulo, embora pickTeam precise de Names para funcionar , não o colocamos no ZIO [Names, Nothing, Set [String]] - em vez disso, mantemos uma referência a ele
TeamsImpl
.
Nosso primeiro teste é simples.
def justTeamsTest = testM("small team test") {
for {
team <- teams.pickTeam(1)
} yield {
assert(team.size)(equalTo(1))
}
}
Para executá-lo, precisamos fornecer uma camada de equipes:
suite("needs just Team")(
justTeamsTest
).provideCustomLayer(Names.live >>> Teams.live),
O que é ">>>"?
Esta é uma composição vertical. Indica que precisamos da camada Nomes , que precisa da camada Equipes .
No entanto, ao executar isso, há um pequeno 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
Voltando à definição
NamesImpl
case class NamesImpl(random: Random.Service) extends Names.Service {
println(s"created namesImpl")
def randomName =
random.nextInt(firstNames.size).map(firstNames(_))
}
Então o nosso
NamesImpl
é criado duas vezes. Qual é o risco se nosso serviço contiver algum recurso exclusivo do sistema de aplicativos? De fato, verifica-se que o problema não existe no mecanismo Camadas - as camadas são lembradas e não são criadas várias vezes no gráfico de dependência. Este é realmente um artefato do ambiente de teste.
Vamos mudar nossa suíte de testes para:
suite("needs just Team")(
justTeamsTest
).provideCustomLayerShared(Names.live >>> Teams.live),
Isso corrige um problema, o que significa que a camada é criada apenas uma vez no
teste.O JustTeamsTest requer apenas equipes . Mas e se eu quisesse acessar equipes e nomes ?
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 isso funcione, precisamos fornecer ambos:
suite("needs Names and Teams")(
inMyTeam
).provideCustomLayer(Names.live ++ (Names.live >>> Teams.live)),
Aqui estamos usando o combinador ++ para criar a camada de nomes com as equipes . Preste atenção à precedência do operador e parênteses extras
(Names.live >>> Teams.live)
No começo, eu me apaixonei por isso - caso contrário, o compilador não fará o que é certo.
História
A história é um pouco mais 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))
}
}
O construtor
HistoryImpl
requer muitos nomes . Mas a única maneira de obtê-lo é retirando-o das equipes . E isso requer o ZIO - portanto, usamos ZLayer.fromServiceM
para nos dar o que precisamos.
O teste é realizado da mesma maneira 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))
E isso é tudo.
Erros jogáveis
O código acima pressupõe que você está retornando o ZLayer [R, Nothing, T] - em outras palavras, a construção de serviço do ambiente é do tipo Nothing. Mas se fizer algo como ler um arquivo ou banco de dados, provavelmente será o ZLayer [R, Throwable, T] - porque esse tipo de coisa geralmente envolve o fator externo que está causando a exceção. Então, imagine que haja um erro na construção de nomes. Existe uma maneira de seus testes contornarem isso:
val live: ZLayer[Random, Throwable, Names] = ???
então no final do teste
.provideCustomLayer(Names.live).mapError(TestFailure.test)
mapError
transforma o objeto throwable
em uma falha de teste - é isso que você deseja - pode ser que o arquivo de teste não exista ou algo parecido.
Mais casos de ZEnv
Os elementos "padrão" do ambiente incluem Clock e Random. Já usamos Random em nossos nomes. Mas e se também queremos que um desses elementos "abaixe" ainda mais nossas dependências? Para fazer isso, criei uma segunda versão do History - History2 - e aqui o Clock é necessário para criar uma instância.
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 não é um exemplo muito útil, mas a parte importante é que a linha
someTime <- ZIO.accessM[Clock](_.get.nanoTime)
nos obriga a fornecer o relógio no lugar certo.
Agora
.provideCustomLayer
pode adicionar nossa camada à pilha de camadas e ela aparece magicamente Random em Names. Mas isso não acontecerá nas horas necessárias abaixo no History2. Portanto, o código a seguir NÃO é compilado:
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)),
Em vez disso, você precisa fornecer o
History2.live
relógio explicitamente, da seguinte maneira:
suite("needs History2 and Teams")(
wonLastYear2
).provideCustomLayerShared((Names.live >>> Teams.live) ++ (((Names.live >>> Teams.live) ++ Clock.any) >>> History2.live))
Clock.any
É uma função que obtém qualquer relógio disponível de cima. Nesse caso, será um relógio de teste, porque não tentamos usar Clock.live
.
Fonte
O código fonte completo (excluindo o lançamento) é mostrado abaixo:
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 perguntas mais avançadas, entre em contato com Discord # zio-users ou visite o site e a documentação do zio.
Saiba mais sobre o curso.