Новая функция 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.
ссылка на оригинал статьи https://habr.com/ru/company/otus/blog/509438/
Добавить комментарий