Fitur ZLayer baru di ZIO 1.0.0-RC18 + merupakan peningkatan signifikan pada pola modul lama, membuat menambahkan layanan baru lebih cepat dan lebih mudah. Namun, dalam praktiknya, saya merasa perlu waktu untuk menguasai idiom ini.
Di bawah ini adalah contoh versi terakhir dari kode pengujian saya yang dianotasi di mana saya melihat sejumlah kasus penggunaan. Terima kasih banyak kepada Adam Fraser karena membantu saya mengoptimalkan dan memperbaiki pekerjaan saya. Layanan ini sengaja disederhanakan, jadi semoga layanan tersebut cukup jelas untuk dibaca dengan cepat.
Saya berasumsi bahwa Anda memiliki pemahaman dasar tentang tes ZIO dan bahwa Anda terbiasa dengan informasi dasar mengenai modul.
Semua kode berjalan dalam tes zio dan merupakan satu file.
Inilah tipnya:
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")
Nama
Jadi, kami sampai ke layanan pertama kami - Nama (Nama)
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)
}
Semuanya di sini adalah dalam kerangka pola modular yang khas.
- Nyatakan Nama sebagai alias tipe untuk Has
- Di objek, tentukan Layanan sebagai sifat
- Buat implementasi (tentu saja Anda dapat membuat beberapa),
- Buat ZLayer di dalam objek untuk implementasi yang diberikan. Konvensi ZIO cenderung memanggil mereka secara langsung.
- Obyek paket ditambahkan yang menyediakan cara pintas yang mudah diakses.
Dalam live itu digunakan
ZLayer.fromService
yang didefinisikan sebagai:
def fromService[A: Tagged, B: Tagged](f: A => B): ZLayer[Has[A], Nothing, Has[B]
Mengabaikan Tagged (ini diperlukan agar semua Has / Layers berfungsi), Anda dapat melihat bahwa di sini fungsi f: A => B digunakan - yang dalam hal ini hanya merupakan konstruktor dari kelas kasus untuk
NamesImpl
.
Seperti yang Anda lihat, Nama membutuhkan Random dari lingkungan zio agar berfungsi.
Inilah tesnya:
def namesTest = testM("names test") {
for {
name <- names.randomName
} yield {
assert(firstNames.contains(name))(equalTo(true))
}
}
Ini digunakan
ZIO.accessM
untuk mengekstrak Nama dari lingkungan. _.get
mengambil layanan.
Kami memberikan Nama untuk pengujian sebagai berikut:
suite("needs Names")(
namesTest
).provideCustomLayer(Names.live),
provideCustomLayer
menambahkan lapisan Nama ke lingkungan yang ada.
Tim
Inti dari Tim (Tim) adalah untuk menguji dependensi antar modul, yang telah kami buat.
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)
}
Tim akan memilih tim dari nama yang tersedia berdasarkan ukuran .
Mengikuti pola penggunaan modul, meskipun pickTeam membutuhkan Nama untuk berfungsi , kami tidak memasukkannya dalam ZIO [Nama, Tidak Ada, Set [String]] - sebagai gantinya, kami menyimpan referensi untuknya
TeamsImpl
.
Tes pertama kami sederhana.
def justTeamsTest = testM("small team test") {
for {
team <- teams.pickTeam(1)
} yield {
assert(team.size)(equalTo(1))
}
}
Untuk menjalankannya, kita perlu memberinya layer Tim:
suite("needs just Team")(
justTeamsTest
).provideCustomLayer(Names.live >>> Teams.live),
Apa itu ">>>"?
Ini adalah komposisi vertikal. Ini menunjukkan bahwa kita memerlukan lapisan Nama , yang membutuhkan lapisan Tim .
Namun, saat menjalankan ini, ada masalah kecil.
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
Kembali ke definisi
NamesImpl
case class NamesImpl(random: Random.Service) extends Names.Service {
println(s"created namesImpl")
def randomName =
random.nextInt(firstNames.size).map(firstNames(_))
}
Jadi kita
NamesImpl
diciptakan dua kali. Apa risikonya jika layanan kami mengandung beberapa sumber daya sistem aplikasi yang unik? Bahkan, ternyata masalahnya tidak ada sama sekali dalam mekanisme Layers - lapisan diingat dan tidak dibuat beberapa kali dalam grafik dependensi. Ini sebenarnya merupakan artefak dari lingkungan pengujian.
Mari kita ubah suite pengujian kami menjadi:
suite("needs just Team")(
justTeamsTest
).provideCustomLayerShared(Names.live >>> Teams.live),
Ini memperbaiki masalah, yang berarti bahwa layer dibuat hanya sekali dalam pengujian.
JustTeamsTest hanya membutuhkan tim . Tetapi bagaimana jika saya ingin mengakses Tim dan Nama ?
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
}
Agar ini berfungsi, kami harus menyediakan keduanya:
suite("needs Names and Teams")(
inMyTeam
).provideCustomLayer(Names.live ++ (Names.live >>> Teams.live)),
Di sini kita menggunakan combinator ++ untuk membuat lapisan Nama dengan Tim . Perhatikan prioritas operator dan kurung tambahan
(Names.live >>> Teams.live)
Pada awalnya, saya jatuh cinta sendiri - jika tidak kompiler tidak akan melakukannya dengan benar.
Sejarah
Sejarah sedikit lebih rumit.
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))
}
}
Konstruktor
HistoryImpl
membutuhkan banyak Nama . Tetapi satu-satunya cara untuk mendapatkannya adalah dengan menariknya dari Tim . Dan itu membutuhkan ZIO - jadi kami menggunakannya ZLayer.fromServiceM
untuk memberikan apa yang kami butuhkan.
Tes dilakukan dengan cara yang sama seperti sebelumnya:
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))
Dan itu saja.
Kesalahan yang bisa dilempar
Kode di atas mengasumsikan bahwa Anda mengembalikan ZLayer [R, Nothing, T] - dengan kata lain, konstruk layanan lingkungan adalah tipe Nothing. Tetapi jika ia melakukan sesuatu seperti membaca dari file atau database, maka kemungkinan besar akan menjadi ZLayer [R, Throwable, T] - karena hal semacam ini sering melibatkan faktor yang sangat eksternal yang menyebabkan pengecualian. Jadi bayangkan ada kesalahan dalam konstruksi Nama. Ada cara bagi tes Anda untuk mengatasi ini:
val live: ZLayer[Random, Throwable, Names] = ???
lalu di akhir tes
.provideCustomLayer(Names.live).mapError(TestFailure.test)
mapError
mengubah objek throwable
menjadi kegagalan pengujian - itu yang Anda inginkan - mungkin mengatakan file pengujian tidak ada atau sesuatu seperti itu.
Lebih banyak kasus ZEnv
Unsur "standar" lingkungan termasuk Jam dan Acak. Kami telah menggunakan Acak di Nama kami. Tetapi bagaimana jika kita juga ingin salah satu dari unsur-unsur ini untuk lebih "menurunkan" ketergantungan kita? Untuk melakukan ini, saya membuat versi kedua History - History2 - dan di sini Clock diperlukan untuk membuat instance.
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)
}
}
Ini bukan contoh yang sangat berguna, tetapi bagian yang penting adalah garis itu
someTime <- ZIO.accessM[Clock](_.get.nanoTime)
memaksa kita untuk menyediakan jam di tempat yang tepat.
Sekarang
.provideCustomLayer
dapat menambahkan lapisan kita ke tumpukan lapisan dan secara ajaib muncul Acak ke Nama. Tapi ini tidak akan terjadi selama jam yang diperlukan di bawah di History2. Karenanya, kode berikut TIDAK mengkompilasi:
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)),
Sebagai gantinya, Anda perlu memberikan
History2.live
jam secara eksplisit, yang dilakukan sebagai berikut:
suite("needs History2 and Teams")(
wonLastYear2
).provideCustomLayerShared((Names.live >>> Teams.live) ++ (((Names.live >>> Teams.live) ++ Clock.any) >>> History2.live))
Clock.any
Merupakan fungsi yang mendapatkan jam apa pun yang tersedia dari atas. Dalam hal ini, ini akan menjadi jam tes, karena kami tidak mencoba menggunakannya Clock.live
.
Sumber
Kode sumber lengkap (tidak termasuk yang bisa dibuang) ditunjukkan di bawah ini:
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))
}
Untuk pertanyaan lebih lanjut, silakan hubungi Discord # zio-pengguna atau kunjungi situs web dan dokumentasi zio.
Pelajari lebih lanjut tentang kursus.