В июле OTUS запускает новый курс «Scala-разработчик», в связи с чем мы подготовили для вас перевод полезного материала.
Новая функция ZLayer в ZIO 1.0.0-RC18+ является значительным улучшением старого паттерна модулей, что делает добавление новых сервисов намного быстрее и легче. Однако при использовании на практике я обнаружил, что может потребоваться какое-то время, чтобы освоить эту идиому.
Ниже приведен аннотированный пример финальной версии моего тестового кода, в котором я рассматриваю ряд вариантов использования. Большое спасибо Адаму Фрейзеру за помощь в оптимизации и облагораживании моей работы. Сервисы преднамеренно упрощены, так что, надеюсь, они будут достаточно понятны для быстрого чтения.
Я предполагаю, что у вас есть базовое понимание ZIO тестов, а также вы ознакомились с основной информацией касательно модулей.
Весь код запускается в zio тестах и представляет собой один файл.
Вот его верхушка:
Итак, мы добрались до нашего первого сервиса — Names (Имена)
Тут все в рамках типичного модульного паттерна.
В live используется
Игнорируя Tagged (это необходимо для работы всего Has/Layers), вы можете видеть, что здесь используется функция f: A => B — которая в данном случае является просто конструктором кейс класса для
Как вы можете видеть, для работы Names требуется Random из среды zio.
Вот тест:
Он использует
Мы предоставляем Names для теста следующим образом:
Суть Teams (Команды) заключается в тестировании зависимостей между модулями, которые мы создали.
Teams выберут команду из доступных names, сделав выбор по размеру.
Следуя паттернам использования модулей, хотя для работы pickTeam нужны Names, мы не помещаем его в ZIO[Names, Nothing, Set[String]] — вместо этого мы держим на него ссылку в
Наш первый тест прост.
Чтобы запустить его, нам нужно предоставить ему слой Teams:
Что такое «>>>»?
Это вертикальная композиция. Она указывает, что нам нужен слой Names, которому нужен слой Teams.
Тем не менее, при запуске этого, есть небольшая проблема.
Возвращаясь к определению
Таким образом, наш
Изменим наш набор тестов на:
Это исправляет проблему, что означает, что слой создается в тесте только один раз
JustTeamsTest требует только teams. Но что, если я хотел получить доступ к Teams и Names?
Чтобы это работало, нам нужно предоставить и то, и другое:
Здесь мы используем комбинатор ++ для создания слоя Names с Teams. Обратите внимание на приоритет оператора и дополнительные скобки
Вначале, я сам на это попался — в противном случае компилятор будет делать не правильно.
History (История) немного сложнее.
Конструктор
Тест проводится по той же схеме, что и раньше:
И все.
В приведенном выше коде предполагается, что вы возвращаете ZLayer[R, Nothing, T] — другими словами, конструкция службы среды имеет тип Nothing. Но если он выполняет что-то вроде чтения из файла или базы данных, то, скорее всего, это будет ZLayer[R, Throwable, T] — потому что такого рода вещи часто включают именно тот внешний фактор, который вызывает исключение. Так что представьте себе, что в конструкции Names произошла ошибка. Для ваших тестов есть способ обойти это:
затем в конце теста
В «стандартные» элементы среды входят Clock (часы) и Random. В наших Names мы уже использовали Random. Но что, если мы также хотим, чтобы один из этих элементов еще больше «понизил» наши зависимости? Для этого я создал вторую версию History — History2 — и здесь для создания экземпляра нужен Clock.
Это не очень полезный пример, но важной частью является то, что строка
заставляет нас предоставлять часы в нужном месте.
Теперь
Вместо этого вам нужно предоставить
Полный исходный код (за исключением throwable) приведен ниже:
Если у вас есть более сложные вопросы, обращайтесь в Discord #zio-users или посетите сайт и документацию zio.
Узнать о курсе подробнее.
Новая функция ZLayer в ZIO 1.0.0-RC18+ является значительным улучшением старого паттерна модулей, что делает добавление новых сервисов намного быстрее и легче. Однако при использовании на практике я обнаружил, что может потребоваться какое-то время, чтобы освоить эту идиому.
Ниже приведен аннотированный пример финальной версии моего тестового кода, в котором я рассматриваю ряд вариантов использования. Большое спасибо Адаму Фрейзеру за помощь в оптимизации и облагораживании моей работы. Сервисы преднамеренно упрощены, так что, надеюсь, они будут достаточно понятны для быстрого чтения.
Я предполагаю, что у вас есть базовое понимание 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")
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)
}
Тут все в рамках типичного модульного паттерна.
- Объявите Names как псевдоним типа для Has
- В объекте, определите Service как трейт
- Создайте реализацию (конечно, вы можете создать несколько),
- Создайте ZLayer внутри объекта для данной реализации. Конвенция ZIO имеет тенденцию вызывать их в реальном времени.
- Добавляется объект пакета, который обеспечивает удобное для доступа сокращение.
В live используется
ZLayer.fromService
, который определяется как:def fromService[A: Tagged, B: Tagged](f: A => B): ZLayer[Has[A], Nothing, Has[B]
Игнорируя Tagged (это необходимо для работы всего Has/Layers), вы можете видеть, что здесь используется функция f: A => B — которая в данном случае является просто конструктором кейс класса для
NamesImpl
.Как вы можете видеть, для работы Names требуется Random из среды zio.
Вот тест:
def namesTest = testM("names test") {
for {
name <- names.randomName
} yield {
assert(firstNames.contains(name))(equalTo(true))
}
}
Он использует
ZIO.accessM
для извлечения Names из среды. _.get
извлекает сервис.Мы предоставляем Names для теста следующим образом:
suite("needs Names")(
namesTest
).provideCustomLayer(Names.live),
provideCustomLayer
добавляет слой Names в существующую среду.Teams
Суть Teams (Команды) заключается в тестировании зависимостей между модулями, которые мы создали.
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)
}
Teams выберут команду из доступных names, сделав выбор по размеру.
Следуя паттернам использования модулей, хотя для работы 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),
Что такое «>>>»?
Это вертикальная композиция. Она указывает, что нам нужен слой Names, которому нужен слой Teams.
Тем не менее, при запуске этого, есть небольшая проблема.
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
создается дважды. Чем это чревато, если наш сервис содержит какой-нибудь уникальный системный ресурс приложения? На самом деле, оказывается, что проблема вовсе не в механизме Layers — слои запоминаются и не создаются несколько раз в графе зависимостей. На самом деле это артефакт тестовой среды.Изменим наш набор тестов на:
suite("needs just Team")(
justTeamsTest
).provideCustomLayerShared(Names.live >>> Teams.live),
Это исправляет проблему, что означает, что слой создается в тесте только один раз
JustTeamsTest требует только teams. Но что, если я хотел получить доступ к Teams и Names?
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)),
Здесь мы используем комбинатор ++ для создания слоя Names с Teams. Обратите внимание на приоритет оператора и дополнительные скобки
(Names.live >>> Teams.live)
Вначале, я сам на это попался — в противном случае компилятор будет делать не правильно.
History
History (История) немного сложнее.
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))
И все.
Throwable ошибки
В приведенном выше коде предполагается, что вы возвращаете 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 кейсов
В «стандартные» элементы среды входят Clock (часы) и Random. В наших Names мы уже использовали 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.
Узнать о курсе подробнее.