diff --git a/.github/workflows/pr-test.yaml b/.github/workflows/pr-test.yaml index 3d30284..b769e1f 100644 --- a/.github/workflows/pr-test.yaml +++ b/.github/workflows/pr-test.yaml @@ -18,5 +18,5 @@ jobs: java-version: "17" cache: "sbt" - name: Test Opinions - run: sbt opinions/test + run: sbt test diff --git a/README.md b/README.md index ab40581..8380f13 100644 --- a/README.md +++ b/README.md @@ -7,24 +7,28 @@ Scala 3 (and only Scala 3). ## Installation -Check the badge above, or the latest GitHub release for the latest version. +Check the badge above, or the latest GitHub release for the latest version +to replace `x.y.z`. ### sbt ```scala -libraryDependencies += "com.alterationx10" %% "opinionated-zio" % "0.0.1" +libraryDependencies += "com.alterationx10" %% "opinionated-zio" % "x.y.z" +libraryDependencies += "com.alterationx10" %% "opinionated-zio-test" % "x.y.z" % Test ``` ### scala cli ```scala -//> using lib com.alterationx10::opinionated-zio:v0.0.1 +//> using dep com.alterationx10::opinionated-zio:x.y.z +//> using test.dep com.alterationx10::opinionated-zio-test:x.y.z ``` ### mill ```scala -ivy"com.alterationx10::opinionated-zio:v0.0.1" +ivy"com.alterationx10::opinionated-zio:x.y.z" +ivy"com.alterationx10::opinionated-zio-test:x.y.z" ``` ## Example Usages @@ -90,4 +94,15 @@ val superLayer: ZLayer[Int & Config, Nothing, Service] = AutoLayer.as[Service, ServiceImpl] -``` \ No newline at end of file +``` + +## Example Test Library Usages + +Everything is bundled into one package for the test library as well, and to use +it, you only need to + +```scala +import test.opinons.* +``` + +More docs to come, but please check the corresponding tests for examples. diff --git a/build.sbt b/build.sbt index 7744a47..1c4d6a2 100644 --- a/build.sbt +++ b/build.sbt @@ -25,7 +25,7 @@ lazy val root = (project in file(".")) .settings( publish / skip := true ) - .aggregate(opinions) + .aggregate(opinions, testOpinions) lazy val opinions = (project in file("opinions")) .settings( @@ -33,3 +33,11 @@ lazy val opinions = (project in file("opinions")) libraryDependencies ++= Dependencies.opinions, testFrameworks += new TestFramework("zio.test.sbt.ZTestFramework") ) + +lazy val testOpinions = (project in file("test-opinions")) + .settings( + name := "opinionated-zio-test", + libraryDependencies ++= Dependencies.testOpinions, + testFrameworks += new TestFramework("zio.test.sbt.ZTestFramework") + ) + .dependsOn(opinions) diff --git a/opinions/src/main/scala/opinions/ZIO.scala b/opinions/src/main/scala/opinions/ZIO.scala index 94ce914..9a943a7 100644 --- a/opinions/src/main/scala/opinions/ZIO.scala +++ b/opinions/src/main/scala/opinions/ZIO.scala @@ -22,6 +22,10 @@ extension [Z: Tag](z: Z) */ def uio: UIO[Z] = ZIO.succeed(z) + /** Wraps an instance z: Z in ZIO.fail + */ + def fail: IO[Z, Nothing] = ZIO.fail(z) + extension [R: Tag, E: Tag, A: Tag](zio: ZIO[R, E, A]) /** Wraps a zio: ZIO[R, E, A] as ZLayer(zio) */ diff --git a/project/Dependencies.scala b/project/Dependencies.scala index 5b11d78..88f946f 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -20,4 +20,11 @@ object Dependencies { "dev.zio" %% "zio-config-typesafe" % Versions.zioConfig ) + val testOpinions: Seq[ModuleID] = Seq( + "dev.zio" %% "zio-test" % Versions.zio, + "dev.zio" %% "zio-test-sbt" % Versions.zio, + "dev.zio" %% "zio-test-magnolia" % Versions.zio, + "dev.zio" %% "zio-mock" % Versions.zioMock + ) + } diff --git a/test-opinions/src/main/scala/test/opinions/Assertions.scala b/test-opinions/src/main/scala/test/opinions/Assertions.scala new file mode 100644 index 0000000..85be831 --- /dev/null +++ b/test-opinions/src/main/scala/test/opinions/Assertions.scala @@ -0,0 +1,15 @@ +package test.opinions + +import zio.* +import zio.test.* +import opinions.* + +extension [A](a: A) + + /** Wrap a: A in Assertion.equalTo(a) + */ + def eqTo: Assertion[A] = Assertion.equalTo(a) + + /** Wrap a: A in Assertion.not(Assertion.equalTo(a)) + */ + def neqTo: Assertion[A] = Assertion.not(a.eqTo) diff --git a/test-opinions/src/main/scala/test/opinions/Expectations.scala b/test-opinions/src/main/scala/test/opinions/Expectations.scala new file mode 100644 index 0000000..5c97d0c --- /dev/null +++ b/test-opinions/src/main/scala/test/opinions/Expectations.scala @@ -0,0 +1,14 @@ +package test.opinions + +import zio.* +import zio.mock.* + +extension [A: Tag](a: A) + + /** Wrap a: A in Expectation.value(a) + */ + def expected: Result[Any, Nothing, A] = Expectation.value(a) + + /** Wrap a: A in Expectation.failure(a) + */ + def expectedF: Result[Any, A, Nothing] = Expectation.failure(a) diff --git a/test-opinions/src/main/scala/test/opinions/Gens.scala b/test-opinions/src/main/scala/test/opinions/Gens.scala new file mode 100644 index 0000000..06a48a6 --- /dev/null +++ b/test-opinions/src/main/scala/test/opinions/Gens.scala @@ -0,0 +1,24 @@ +package test.opinions + +import zio.test.* +import zio.* + +object GenRandom: + + /** Generate a random List[A] + * @param count + * The number of elements to generate + * @param gen + * The implicit Gen[Any, A] to use. + * @tparam A + */ + def apply[A](count: Int)(using gen: Gen[Any, A]): UIO[List[A]] = + gen.runCollectN(count) + + /** Generate a random A + * @param gen + * The implicit Gen[Any, A] to use. + * @tparam A + */ + def apply[A](using gen: Gen[Any, A]): UIO[A] = + GenRandom[A](1).map(_.head) diff --git a/test-opinions/src/main/scala/test/opinions/Mocks.scala b/test-opinions/src/main/scala/test/opinions/Mocks.scala new file mode 100644 index 0000000..ecae342 --- /dev/null +++ b/test-opinions/src/main/scala/test/opinions/Mocks.scala @@ -0,0 +1,37 @@ +package test.opinions + +import zio.* +import zio.mock.* +import zio.test.Assertion + +extension [S, E: Tag, I, O: Tag](serviceMethod: Mock[S]#Effect[I, E, O]) + + /** For a given ZIO Mock Capability (serviceMethod), apply the given values as + * Expectation.value/Assertion.equalTo + * @param o + * Expected output value + * @param i + * Asserted input value + */ + def expectWhen(o: O, i: I): Expectation[S] = + serviceMethod.apply(i.eqTo, o.expected) + + /** For a given ZIO Mock Capability (serviceMethod), apply the given values as + * Expectation.failure/Assertion.equalTo + * @param e + * Expected output failure value + * @param i + * Asserted input value + */ + def expectWhenF(e: E, i: I): Expectation[S] = + serviceMethod.apply(i.eqTo, e.expectedF) + + /** For a given ZIO Mock Capability (serviceMethod), apply the given + * Expectation Result/Assertion + * @param o + * The expected output Result/Expectation + * @param i + * The expected input assertion + */ + def expectWhen(o: Result[Any, E, O], i: Assertion[I]): Expectation[S] = + serviceMethod.apply(i, o) diff --git a/test-opinions/src/test/scala/test/opinions/AssertionsSpec.scala b/test-opinions/src/test/scala/test/opinions/AssertionsSpec.scala new file mode 100644 index 0000000..c1b72a5 --- /dev/null +++ b/test-opinions/src/test/scala/test/opinions/AssertionsSpec.scala @@ -0,0 +1,16 @@ +package test.opinions + +import zio.* +import zio.test.* +import opinions.* + +object AssertionsSpec extends ZIOSpecDefault: + override def spec: Spec[TestEnvironment with Scope, Any] = + suite("AssertionsSpec")( + test("eqTo") { + assertZIO(42.uio)(42.eqTo) + }, + test("neqTo") { + assertZIO(42.uio)(69.neqTo) + } + ) diff --git a/test-opinions/src/test/scala/test/opinions/ExpectationsSpec.scala b/test-opinions/src/test/scala/test/opinions/ExpectationsSpec.scala new file mode 100644 index 0000000..5ec114d --- /dev/null +++ b/test-opinions/src/test/scala/test/opinions/ExpectationsSpec.scala @@ -0,0 +1,17 @@ +package test.opinions + +import zio.* +import zio.test.* +import zio.mock.* + +object ExpectationsSpec extends ZIOSpecDefault: + + override def spec: Spec[TestEnvironment with Scope, Any] = + suite("ExpectationsSpec")( + test("expected") { + assertZIO(42.expected.io(()))(42.eqTo) + }, + test("expectedF") { + assertZIO(42.expectedF.io(()).flip)(42.eqTo) + } + ) diff --git a/test-opinions/src/test/scala/test/opinions/GensSpec.scala b/test-opinions/src/test/scala/test/opinions/GensSpec.scala new file mode 100644 index 0000000..d93027b --- /dev/null +++ b/test-opinions/src/test/scala/test/opinions/GensSpec.scala @@ -0,0 +1,32 @@ +package test.opinions + +import zio.* +import zio.test.* +import opinions.* +import zio.test.magnolia.DeriveGen + +import java.time.Instant +import java.util.UUID + +object GensSpec extends ZIOSpecDefault: + + case class SomeModel(a: Int, b: String, c: Instant, d: UUID) + given Gen[Any, SomeModel] = DeriveGen[SomeModel] + + override def spec: Spec[TestEnvironment with Scope, Any] = + suite("GensSpec")( + test("Generate a random List of case class instances") { + for { + someNumber <- Random.nextIntBetween(10, 50) + models <- GenRandom[SomeModel](someNumber) + } yield assertTrue( + models.length == someNumber, + models.distinct.length == someNumber + ) + }, + test("Generate a random instance of a case class") { + for { + model <- GenRandom[SomeModel] + } yield assertCompletes + } + ) diff --git a/test-opinions/src/test/scala/test/opinions/MocksSpec.scala b/test-opinions/src/test/scala/test/opinions/MocksSpec.scala new file mode 100644 index 0000000..fe9770c --- /dev/null +++ b/test-opinions/src/test/scala/test/opinions/MocksSpec.scala @@ -0,0 +1,71 @@ +package test.opinions + +import zio.* +import zio.test.* +import zio.mock.* +import opinions.* + +object MocksSpec extends ZIOSpecDefault: + + trait SomeService: + def get(id: Int): Task[String] + + object SomeMockService extends Mock[SomeService]: + object Get extends Effect[Int, Throwable, String] + + val compose: URLayer[Proxy, SomeService] = + ZLayer { + for { + proxy <- ZIO.service[Proxy] + } yield new SomeService: + override def get(id: RuntimeFlags): Task[String] = proxy(Get, id) + } + override def spec: Spec[TestEnvironment with Scope, Any] = + suite("MocksSpec + ")( + test("expectWhen of values") { + for { + result <- ZIO + .serviceWithZIO[SomeService](_.get(42)) + .provide(SomeMockService.Get.expectWhen("forty two", 42)) + _ <- ZIO + .serviceWithZIO[SomeService](_.get(42)) + .provide( + // Comparison without expectWhen extension method + SomeMockService + .Get(Assertion.equalTo(42), Expectation.value("forty two")) + ) + } yield assertTrue(result == "forty two") + }, + test("expectWhenF of values") { + for { + error <- + ZIO + .serviceWithZIO[SomeService](_.get(42)) + .flip + .provide( + SomeMockService.Get.expectWhenF(new Exception("boom"), 42) + ) + } yield assertTrue(error.getMessage == "boom") + }, + test("expectWhen of assertions") { + for { + result <- + ZIO + .serviceWithZIO[SomeService](_.get(42)) + .provide( + SomeMockService.Get.expectWhen("forty two".expected, 42.eqTo) + ) + error <- + ZIO + .serviceWithZIO[SomeService](_.get(42)) + .flip + .provide( + SomeMockService.Get + .expectWhen(new Exception("boom").expectedF, 42.eqTo) + ) + } yield assertTrue( + result == "forty two", + error.getMessage == "boom" + ) + } + )