├── .github └── workflows │ └── scala.yml ├── .gitignore ├── .scalafmt.conf ├── LICENSE ├── README.md ├── build.sbt ├── project ├── build.properties └── plugins.sbt └── src ├── main └── scala │ └── egast │ └── zioeasymock │ ├── expectingmock.scala │ ├── mock.scala │ └── package.scala └── test └── scala └── egast └── zioeasymock └── zioeasymockSpec.scala /.github/workflows/scala.yml: -------------------------------------------------------------------------------- 1 | name: Zio-EasyMock CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Set up JDK 1.8 17 | uses: actions/setup-java@v1 18 | with: 19 | java-version: 1.8 20 | - name: Run tests 21 | run: sbt scalafmtCheck +test 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | *.log 3 | .idea 4 | target 5 | -------------------------------------------------------------------------------- /.scalafmt.conf: -------------------------------------------------------------------------------- 1 | version=2.5.3 2 | maxColumn = 120 3 | align = most 4 | continuationIndent.defnSite = 2 5 | assumeStandardLibraryStripMargin = true 6 | docstrings = JavaDoc 7 | lineEndings = preserve 8 | includeCurlyBraceInSelectChains = false 9 | danglingParentheses.defnSite = true 10 | danglingParentheses.callSite = true 11 | spaces { 12 | inImportCurlyBraces = true 13 | } 14 | optIn.annotationNewlines = true 15 | 16 | rewrite.rules = [SortImports, RedundantBraces] 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Zio-EasyMock CI](https://github.com/egast/zio-easymock/workflows/Zio-EasyMock%20CI/badge.svg) 2 | # zio-easymock 3 | EasyMock mocking for ZIO 4 | 5 | ## Quickstart 6 | Add the following dependency to your `build.sbt` file: 7 | ``` 8 | libraryDependencies ++= Seq( 9 | "com.github.egast" %% "zio-easymock" % "0.5.0" 10 | ) 11 | ``` 12 | 13 | ### Example usage 14 | ```scala 15 | import egast.zioeasymock._ 16 | import zio._ 17 | import zio.test.Assertion._ 18 | import zio.test._ 19 | 20 | object ExampleSpec extends DefaultRunnableSpec { 21 | override def spec = suite("example")( 22 | testM("mock one service as layer") { 23 | expecting[TestService.Service] { service1 => 24 | expect(service1.multiplyByTwo(1000))(_.andReturn(ZIO.effectTotal(2000))) 25 | }.whenExecutingAsLayer(mockLayer => 26 | assertM(TestService.multiplyByTwo(1000))(equalTo(2000)) 27 | .provideCustomLayer(mockLayer) 28 | ) 29 | }, 30 | testM("mock two services as layer") { 31 | expecting[TestService.Service, TestService2.Service] { (service1, service2) => 32 | expect(service1.multiplyByTwo(1000))(_.andReturn(ZIO.effectTotal(2000))) *> 33 | expect(service2.intToString(200))(_.andReturn(ZIO.effectTotal("200"))) 34 | }.whenExecutingAsLayer(mockLayer => 35 | assertM(TestService2.intToString(200) *> TestService.multiplyByTwo(1000))(equalTo(2000)) 36 | .provideCustomLayer(mockLayer) 37 | ) 38 | }, 39 | testM("mock one service") { 40 | expecting[TestService.Service] { service1 => 41 | expect(service1.multiplyByTwo(1000))(_.andReturn(ZIO.effectTotal(2000))) 42 | }.whenExecuting(service => 43 | assertM(service.multiplyByTwo(1000))(equalTo(2000)) 44 | ) 45 | }, 46 | testM("mock service with strict mocks") { 47 | createStrictMock[TestService.Service] 48 | .expecting { service1 => 49 | expect(service1.multiplyByTwo(200))(_.andReturn(ZIO.effectTotal(400))) *> 50 | expect(service1.multiplyByTwo(400))(_.andReturn(ZIO.effectTotal(800))) 51 | }.whenExecutingAsLayer(mockLayer => 52 | assertM(TestService.multiplyByTwo(200) >>= TestService.multiplyByTwo)(equalTo(800)) 53 | .provideCustomLayer(mockLayer) 54 | ) 55 | } 56 | ) 57 | } 58 | 59 | object TestService { 60 | type TestService = Has[Service] 61 | 62 | trait Service { 63 | def multiplyByTwo(n: Int): ZIO[Any, Throwable, Int] 64 | } 65 | 66 | def multiplyByTwo(n: Int): ZIO[TestService, Throwable, Int] = 67 | ZIO.accessM(_.get[Service].multiplyByTwo(n)) 68 | } 69 | 70 | object TestService2 { 71 | type TestService2 = Has[Service] 72 | 73 | trait Service { 74 | def intToString(n: Int): ZIO[Any, Throwable, String] 75 | } 76 | 77 | def intToString(n: Int): ZIO[TestService2, Throwable, String] = 78 | ZIO.accessM(_.get[Service].intToString(n)) 79 | } 80 | ``` 81 | -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | lazy val scala211 = "2.11.12" 2 | lazy val scala212 = "2.12.11" 3 | lazy val scala213 = "2.13.2" 4 | lazy val mainScala = scala213 5 | lazy val allScala = Seq(scala211, scala212, mainScala) 6 | 7 | inThisBuild( 8 | List( 9 | scalaVersion := mainScala, 10 | crossScalaVersions := allScala, 11 | organization := "com.github.egast", 12 | homepage := Some(url("https://github.com/egast/zio-easymock")), 13 | licenses := List( 14 | "Apache-2.0" -> url("http://www.apache.org/licenses/LICENSE-2.0") 15 | ), 16 | developers := List( 17 | Developer( 18 | "egast", 19 | "Erik Gast", 20 | "egast@users.noreply.github.com", 21 | url("https://github.com/egast") 22 | ) 23 | ), 24 | scmInfo := Some( 25 | ScmInfo( 26 | url("https://github.com/egast/zio-easymock/"), 27 | "scm:git:git@github.com:egast/zio-easymock.git" 28 | ) 29 | ) 30 | ) 31 | ) 32 | 33 | ThisBuild / publishTo := sonatypePublishToBundle.value 34 | 35 | name := "zio-easymock" 36 | version := "0.5.1-SNAPSHOT" 37 | 38 | val zioVersion = "1.0.0" 39 | val easymockVersion = "4.2" 40 | 41 | libraryDependencies ++= Seq( 42 | "dev.zio" %% "zio" % zioVersion, 43 | "dev.zio" %% "zio-test" % zioVersion, 44 | "dev.zio" %% "zio-test-sbt" % zioVersion % "test" 45 | ) 46 | libraryDependencies ++= Seq("org.easymock" % "easymock" % easymockVersion) 47 | 48 | testFrameworks := Seq(new TestFramework("zio.test.sbt.ZTestFramework")) 49 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version = 1.3.10 -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("com.jsuereth" % "sbt-pgp" % "2.0.0") 2 | addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "3.9.2") 3 | addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.4.0") 4 | -------------------------------------------------------------------------------- /src/main/scala/egast/zioeasymock/expectingmock.scala: -------------------------------------------------------------------------------- 1 | package egast.zioeasymock 2 | 3 | import egast.zioeasymock.ExpectingMock._ 4 | import org.easymock.EasyMock 5 | import zio._ 6 | import zio.test._ 7 | 8 | import scala.reflect.ClassTag 9 | 10 | case class ExpectingMock1[A <: AnyRef: Tag](mock: Mock1[A], expectation: A => Task[Any]) { 11 | def whenExecuting[R, E]( 12 | assertion: A => ZIO[R, E, TestResult] 13 | ): ZIO[R, E, TestResult] = 14 | assertWhenExecuting[Tuple1[A], R, E]( 15 | a => expectation(a._1), 16 | a => assertion(a._1) 17 | )(mock.asTuple) 18 | 19 | def whenExecutingAsLayer[R, E]( 20 | assertion: ULayer[Has[A]] => ZIO[R, E, TestResult] 21 | ): ZIO[R, E, TestResult] = 22 | whenExecuting(a => assertion(ZLayer.succeed(a))) 23 | } 24 | 25 | case class ExpectingMock2[A <: AnyRef: Tag, B <: AnyRef: Tag]( 26 | mock: Mock2[A, B], 27 | expectation: (A, B) => Task[Any] 28 | ) { 29 | def whenExecuting[R, E]( 30 | assertion: (A, B) => ZIO[R, E, TestResult] 31 | ): ZIO[R, E, TestResult] = 32 | assertWhenExecuting(expectation.tupled, assertion.tupled)(mock.asTuple) 33 | 34 | def whenExecutingAsLayer[R, E]( 35 | assertion: ULayer[Has[A] with Has[B]] => ZIO[R, E, TestResult] 36 | ): ZIO[R, E, TestResult] = 37 | whenExecuting { 38 | case (a, b) => assertion(ZLayer.succeedMany(Has.allOf[A, B](a, b))) 39 | } 40 | } 41 | 42 | case class ExpectingMock3[A <: AnyRef: Tag, B <: AnyRef: Tag, C <: AnyRef: Tag]( 43 | mock: Mock3[A, B, C], 44 | expectation: (A, B, C) => Task[Any] 45 | ) { 46 | def whenExecuting[R, E]( 47 | assertion: (A, B, C) => ZIO[R, E, TestResult] 48 | ): ZIO[R, E, TestResult] = 49 | assertWhenExecuting(expectation.tupled, assertion.tupled)(mock.asTuple) 50 | 51 | def whenExecutingAsLayer[R, E]( 52 | assertion: ULayer[Has[A] with Has[B]] => ZIO[R, E, TestResult] 53 | ): ZIO[R, E, TestResult] = 54 | whenExecuting { 55 | case (a, b, c) => 56 | assertion(ZLayer.succeedMany(Has.allOf[A, B, C](a, b, c))) 57 | } 58 | } 59 | case class ExpectingMock4[A <: AnyRef: Tag, B <: AnyRef: Tag, C <: AnyRef: Tag, D <: AnyRef: Tag]( 60 | mock: Mock4[A, B, C, D], 61 | expectation: (A, B, C, D) => Task[Any] 62 | ) { 63 | def whenExecuting[R, E]( 64 | assertion: (A, B, C, D) => ZIO[R, E, TestResult] 65 | ): ZIO[R, E, TestResult] = 66 | assertWhenExecuting(expectation.tupled, assertion.tupled)(mock.asTuple) 67 | 68 | def whenExecutingAsLayer[R, E]( 69 | assertion: ULayer[Has[A] with Has[B]] => ZIO[R, E, TestResult] 70 | ): ZIO[R, E, TestResult] = 71 | whenExecuting { 72 | case (a, b, c, d) => 73 | assertion(ZLayer.succeedMany(Has.allOf[A, B, C, D](a, b, c, d))) 74 | } 75 | } 76 | 77 | private[zioeasymock] object ExpectingMock { 78 | 79 | def assertWhenExecuting[M <: Product: ClassTag, R, E]( 80 | expectation: M => Task[Any], 81 | assertion: M => ZIO[R, E, TestResult] 82 | )(mocks: Task[M]) = 83 | mocks.foldM( 84 | t => ZIO.effectTotal(assert(t)(failMockExpectation)), 85 | allMocks => assertMocked(expectation, assertion)(allMocks) 86 | ) 87 | 88 | private def assertMocked[M <: Product: ClassTag, R, E]( 89 | expectation: M => Task[Any], 90 | assertion: M => ZIO[R, E, TestResult] 91 | )(allMocks: M): ZIO[R, E, TestResult] = 92 | (for { 93 | _ <- Task(expectation(allMocks)).flatten 94 | allMocksList = allMocks.productIterator.toList.map(_.asInstanceOf[AnyRef]) 95 | replayResult <- ZIO 96 | .foreach(allMocksList)(replayTestResult) 97 | .map(_.reduce(_ && _)) 98 | testResult <- assertion(allMocks) 99 | .catchAllCause(c => 100 | ZIO.fail( 101 | c.defects.headOption 102 | .getOrElse(new AssertionError("unknown mocking error")) 103 | ) 104 | ) 105 | 106 | verifyResult <- ZIO 107 | .foreach(allMocksList)(verifyTestResult) 108 | .map(_.reduce(_ && _)) 109 | } yield replayResult && testResult && verifyResult) 110 | .fold(t => assert(t)(failMockExpectation), v => v) 111 | 112 | private def replayZio[A <: AnyRef](mock: A): Task[A] = 113 | ZIO.effect(EasyMock.replay(mock)).as(mock) 114 | 115 | private def verifyZio[A <: AnyRef](mock: A): Task[A] = 116 | ZIO.effect(EasyMock.verify(mock)).as(mock) 117 | 118 | private def replayTestResult[M <: AnyRef](m: M): IO[Throwable, TestResult] = 119 | replayZio(m).as(assertCompletes) 120 | 121 | private def verifyTestResult[M <: AnyRef](m: M): IO[Throwable, TestResult] = 122 | verifyZio(m).as(assertCompletes) 123 | 124 | private def failMockExpectation: Assertion[Throwable] = 125 | Assertion.assertion("mocking expectations")()(_ => false) 126 | 127 | } 128 | -------------------------------------------------------------------------------- /src/main/scala/egast/zioeasymock/mock.scala: -------------------------------------------------------------------------------- 1 | package egast.zioeasymock 2 | 3 | import org.easymock.EasyMock 4 | import zio.{ Tag, Task, ZIO } 5 | 6 | import scala.reflect.ClassTag 7 | 8 | case class Mock1[A <: AnyRef: Tag](mock1: Task[A]) { 9 | 10 | def expecting(expectation: A => Task[Any]): ExpectingMock1[A] = 11 | ExpectingMock1(this, expectation) 12 | 13 | private[zioeasymock] val asTuple: Task[Tuple1[A]] = mock1.map(Tuple1(_)) 14 | } 15 | 16 | case class Mock2[A <: AnyRef: Tag, B <: AnyRef: Tag](mock1: Task[A], mock2: Task[B]) { 17 | def expecting(expectation: (A, B) => Task[Any]): ExpectingMock2[A, B] = 18 | ExpectingMock2(this, expectation) 19 | 20 | private[zioeasymock] val asTuple: Task[(A, B)] = mock1.zip(mock2) 21 | } 22 | 23 | case class Mock3[A <: AnyRef: Tag, B <: AnyRef: Tag, C <: AnyRef: Tag](mock1: Task[A], mock2: Task[B], mock3: Task[C]) { 24 | def expecting(expectation: (A, B, C) => Task[Any]): ExpectingMock3[A, B, C] = 25 | ExpectingMock3(this, expectation) 26 | 27 | private[zioeasymock] val asTuple: Task[(A, B, C)] = for { 28 | m1 <- mock1 29 | m2 <- mock2 30 | m3 <- mock3 31 | } yield (m1, m2, m3) 32 | } 33 | 34 | case class Mock4[A <: AnyRef: Tag, B <: AnyRef: Tag, C <: AnyRef: Tag, D <: AnyRef: Tag]( 35 | mock1: Task[A], 36 | mock2: Task[B], 37 | mock3: Task[C], 38 | mock4: Task[D] 39 | ) { 40 | def expecting(expectation: (A, B, C, D) => Task[Any]): ExpectingMock4[A, B, C, D] = 41 | ExpectingMock4(this, expectation) 42 | 43 | private[zioeasymock] val asTuple: Task[(A, B, C, D)] = for { 44 | m1 <- mock1 45 | m2 <- mock2 46 | m3 <- mock3 47 | m4 <- mock4 48 | } yield (m1, m2, m3, m4) 49 | } 50 | 51 | object Mock { 52 | 53 | def mockZio[A <: AnyRef](implicit cls: ClassTag[A]): Task[A] = 54 | ZIO.effect(EasyMock.mock[A](cls.runtimeClass)) 55 | 56 | def mockStrictZio[A <: AnyRef](implicit cls: ClassTag[A]): Task[A] = 57 | ZIO.effect(EasyMock.strictMock[A](cls.runtimeClass)) 58 | } 59 | -------------------------------------------------------------------------------- /src/main/scala/egast/zioeasymock/package.scala: -------------------------------------------------------------------------------- 1 | package egast 2 | 3 | import egast.zioeasymock.Mock._ 4 | import org.easymock.{ EasyMock, IExpectationSetters } 5 | import zio.{ Tag, Task } 6 | 7 | import scala.reflect.ClassTag 8 | 9 | package object zioeasymock { 10 | 11 | def createMock[A <: AnyRef: ClassTag: Tag]: Mock1[A] = 12 | zioeasymock.Mock1(mockZio[A]) 13 | 14 | def createMock[A <: AnyRef: ClassTag: Tag, B <: AnyRef: ClassTag: Tag]: Mock2[A, B] = Mock2(mockZio[A], mockZio[B]) 15 | 16 | def createMock[A <: AnyRef: ClassTag: Tag, B <: AnyRef: ClassTag: Tag, C <: AnyRef: ClassTag: Tag]: Mock3[A, B, C] = 17 | Mock3(mockZio[A], mockZio[B], mockZio[C]) 18 | 19 | def createMock[ 20 | A <: AnyRef: ClassTag: Tag, 21 | B <: AnyRef: ClassTag: Tag, 22 | C <: AnyRef: ClassTag: Tag, 23 | D <: AnyRef: ClassTag: Tag 24 | ]: Mock4[A, B, C, D] = 25 | Mock4(mockZio[A], mockZio[B], mockZio[C], mockZio[D]) 26 | 27 | def createStrictMock[A <: AnyRef: ClassTag: Tag]: Mock1[A] = 28 | zioeasymock.Mock1(mockStrictZio[A]) 29 | 30 | def createStrictMock[A <: AnyRef: ClassTag: Tag, B <: AnyRef: ClassTag: Tag]: Mock2[A, B] = 31 | Mock2(mockStrictZio[A], mockStrictZio[B]) 32 | 33 | def createStrictMock[A <: AnyRef: ClassTag: Tag, B <: AnyRef: ClassTag: Tag, C <: AnyRef: ClassTag: Tag] 34 | : Mock3[A, B, C] = 35 | Mock3(mockStrictZio[A], mockStrictZio[B], mockStrictZio[C]) 36 | 37 | def createStrictMock[ 38 | A <: AnyRef: ClassTag: Tag, 39 | B <: AnyRef: ClassTag: Tag, 40 | C <: AnyRef: ClassTag: Tag, 41 | D <: AnyRef: ClassTag: Tag 42 | ]: Mock4[A, B, C, D] = 43 | Mock4(mockStrictZio[A], mockStrictZio[B], mockStrictZio[C], mockStrictZio[D]) 44 | 45 | def expecting[A <: AnyRef: ClassTag: Tag]( 46 | expectation: A => Task[Any] 47 | ): ExpectingMock1[A] = 48 | createMock[A].expecting(expectation) 49 | 50 | def expecting[A <: AnyRef: ClassTag: Tag, B <: AnyRef: ClassTag: Tag]( 51 | expectation: (A, B) => Task[Any] 52 | ): ExpectingMock2[A, B] = createMock[A, B].expecting(expectation) 53 | 54 | def expecting[A <: AnyRef: ClassTag: Tag, B <: AnyRef: ClassTag: Tag, C <: AnyRef: ClassTag: Tag]( 55 | expectation: (A, B, C) => Task[Any] 56 | ): ExpectingMock3[A, B, C] = createMock[A, B, C].expecting(expectation) 57 | 58 | def expect[A, B](A: => A)( 59 | expectationSetters: IExpectationSetters[A] => IExpectationSetters[B] 60 | ): Task[IExpectationSetters[B]] = 61 | Task(EasyMock.expect(A)).flatMap(v => Task(expectationSetters(v))) 62 | 63 | } 64 | -------------------------------------------------------------------------------- /src/test/scala/egast/zioeasymock/zioeasymockSpec.scala: -------------------------------------------------------------------------------- 1 | package egast.zioeasymock 2 | 3 | import org.easymock.EasyMock 4 | import zio.test.Assertion._ 5 | import zio.test.TestAspect._ 6 | import zio.test._ 7 | import zio.{ Has, Task, ZIO } 8 | 9 | object zioeasymockSpec extends DefaultRunnableSpec { 10 | override def spec = 11 | suite("zioeasymock")( 12 | testM("test mocking") { 13 | expecting[TestService.Service, TestService2.Service] { (service1, service2) => 14 | expect(service1.doSomething(1000))(_.andReturn(ZIO.effectTotal("100"))) *> 15 | expect(service2.doSomething2(200))(_.andReturn(ZIO.effectTotal("200"))) 16 | }.whenExecutingAsLayer(mockLayer => 17 | assertM(TestService2.doSomething2(200) *> TestService.doSomething(1000))(equalTo("100")) 18 | .provideCustomLayer(mockLayer) 19 | ) 20 | }, 21 | testM("test strict mocking") { 22 | createStrictMock[TestService.Service].expecting { service1 => 23 | expect(service1.doSomething(1000))(_.andReturn(ZIO.effectTotal("1000"))) *> 24 | expect(service1.doSomething(2000))(_.andReturn(ZIO.effectTotal("2000"))) *> 25 | expect(service1.doSomething(3000))(_.andReturn(ZIO.effectTotal("3000"))) 26 | }.whenExecutingAsLayer(mockLayer => 27 | assertM( 28 | TestService.doSomething(1000) *> TestService.doSomething(2000) *> 29 | TestService.doSomething(3000) 30 | )( 31 | equalTo("3000") 32 | ).provideCustomLayer(mockLayer) 33 | ) 34 | }, 35 | testM("test strict mocking unexpected call") { 36 | createStrictMock[TestService.Service].expecting { service1 => 37 | expect(service1.doSomething(2000))(_.andReturn(ZIO.effectTotal("2000"))) *> 38 | expect(service1.doSomething(1000))(_.andReturn(ZIO.effectTotal("1000"))) *> 39 | expect(service1.doSomething(3000))(_.andReturn(ZIO.effectTotal("3000"))) 40 | }.whenExecutingAsLayer(mockLayer => 41 | assertM( 42 | TestService.doSomething(1000) *> TestService.doSomething(2000) *> 43 | TestService.doSomething(3000) 44 | )( 45 | equalTo("3000") 46 | ).provideCustomLayer(mockLayer) 47 | ) 48 | } @@ failing, 49 | testM("test mocking with mock returning a failed effect") { 50 | val failure = new RuntimeException("failure") 51 | expecting[TestService.Service, TestService2.Service] { (_, service2) => 52 | expect(service2.doSomething2(200))(_.andReturn(ZIO.fail(failure))) 53 | }.whenExecutingAsLayer(mockLayer => 54 | assertM((TestService2.doSomething2(200) *> TestService.doSomething(1000)).run)(fails(equalTo(failure))) 55 | .provideCustomLayer(mockLayer) 56 | ) 57 | }, 58 | testM("test mocking with capture") { 59 | for { 60 | capture <- Task.effect(EasyMock.newCapture[Int]()) 61 | result <- expecting[TestService.Service, TestService2.Service] { (service1, service2) => 62 | expect(service1.doSomething(EasyMock.capture(capture)))( 63 | _.andReturn(ZIO.effect(capture.getValue.toString)) 64 | ) *> 65 | expect(service2.doSomething2(200))(_.andReturn(ZIO.effectTotal("200"))) 66 | 67 | }.whenExecuting((service1, service2) => 68 | for { 69 | _ <- service2.doSomething2(200) 70 | result <- service1.doSomething(1000) 71 | } yield assert(result)(equalTo("1000")) && 72 | assert(capture.getValue)(equalTo(1000)) 73 | ) 74 | } yield result 75 | }, 76 | testM("test mocking unexpected call failure") { 77 | expecting[TestService.Service, TestService2.Service] { (service1, service2) => 78 | expect(service1.doSomething(100))(_.andReturn(ZIO.effectTotal("100"))) *> 79 | expect(service2.doSomething2(200))(_.andReturn(ZIO.effectTotal("200"))) 80 | }.whenExecutingAsLayer(mockLayer => 81 | assertM(TestService2.doSomething2(200) *> TestService.doSomething(1000))(equalTo("100")) 82 | .provideCustomLayer(mockLayer) 83 | ) 84 | } @@ failing, 85 | testM("test mocking create mock failure") { 86 | expecting[String] { string => 87 | expect(string.charAt(100))(_.andReturn('e')) 88 | }.whenExecuting(string => assertM(ZIO.effect(string.charAt(100)))(equalTo('e'))) 89 | } @@ failing, 90 | testM("test mocking verify failure") { 91 | expecting[TestService.Service, TestService2.Service] { (service1, service2) => 92 | expect(service1.doSomething(1000))(_.andReturn(ZIO.effectTotal("100"))) *> 93 | expect(service1.doSomething(1000))(_.andReturn(ZIO.effectTotal("100"))) *> 94 | expect(service2.doSomething2(200))(_.andReturn(ZIO.effectTotal("200"))) 95 | 96 | }.whenExecutingAsLayer(mockLayer => 97 | assertM(TestService2.doSomething2(200) *> TestService.doSomething(1000))(equalTo("100")) 98 | .provideCustomLayer(mockLayer) 99 | ) 100 | } @@ failing, 101 | testM("test mocking with check") { 102 | checkM(Gen.int(0, 200)) { 103 | n => 104 | expecting[TestService.Service, TestService2.Service] { (service1, service2) => 105 | expect(service1.doSomething(n))(_.andReturn(ZIO.effectTotal((n).toString))) *> 106 | expect(service2.doSomething2(n * 2))(_.andReturn(ZIO.effectTotal((n * 2).toString))) 107 | }.whenExecutingAsLayer(mockLayer => 108 | assertM(TestService2.doSomething2(n * 2) *> TestService.doSomething(n))(equalTo(n.toString)) 109 | .provideCustomLayer(mockLayer) 110 | ) 111 | } 112 | }, 113 | testM("test mocking with check should fail") { 114 | checkM(Gen.int(0, 200)) { 115 | n => 116 | expecting[TestService.Service, TestService2.Service] { (service1, service2) => 117 | expect(service1.doSomething(n))(_.andReturn(ZIO.effectTotal((n % 100).toString))) *> 118 | expect(service2.doSomething2(n * 2))(_.andReturn(ZIO.effectTotal((n * 2).toString))) 119 | }.whenExecutingAsLayer(mockLayer => 120 | assertM(TestService2.doSomething2(n * 2) *> TestService.doSomething(n))(equalTo(n.toString)) 121 | .provideCustomLayer(mockLayer) 122 | ) 123 | } 124 | } @@ failing, 125 | testM("test nested mocking ") { 126 | expecting[TestService.Service] { service1 => 127 | expect(service1.doSomething(1000))(_.andReturn(ZIO.effectTotal("100"))) *> 128 | expect(service1.doSomething(2000))(_.andReturn(ZIO.effectTotal("2000"))) 129 | }.whenExecutingAsLayer(mockLayer => 130 | expecting[TestService2.Service] { service2 => 131 | expect(service2.doSomething2(200))(_.andReturn(ZIO.effectTotal("200"))) 132 | }.whenExecutingAsLayer(mock2Layer => 133 | assertM( 134 | TestService2.doSomething2(200) *> TestService.doSomething(1000) *> 135 | TestService.doSomething(2000) 136 | )( 137 | equalTo("2000") 138 | ).provideCustomLayer(mockLayer ++ mock2Layer) 139 | ) 140 | ) 141 | }, 142 | testM("test mocking") { 143 | checkM(Gen.listOf(Gen.anyInt)) { numbers => 144 | expecting[TestService.Service] { service1 => 145 | ZIO.foreach(numbers)(n => expect(service1.doSomething(n))(_.andReturn(ZIO.effectTotal(n.toString)))) 146 | }.whenExecutingAsLayer(mockLayer => 147 | assertM( 148 | ZIO.foreach(numbers)(n => TestService.doSomething(n)) 149 | )(equalTo(numbers.map(_.toString))) 150 | .provideCustomLayer(mockLayer) 151 | ) 152 | } 153 | } 154 | ) 155 | 156 | } 157 | 158 | private object TestService { 159 | type TestService = Has[Service] 160 | 161 | trait Service { 162 | def doSomething(key: Int): ZIO[Any, Throwable, String] 163 | } 164 | 165 | def doSomething(key: Int): ZIO[TestService, Throwable, String] = 166 | ZIO.accessM(_.get[Service].doSomething(key)) 167 | } 168 | 169 | private object TestService2 { 170 | type TestService2 = Has[Service] 171 | 172 | trait Service { 173 | def doSomething2(key: Int): ZIO[Any, Throwable, String] 174 | } 175 | 176 | def doSomething2(key: Int): ZIO[TestService2, Throwable, String] = 177 | ZIO.accessM(_.get[Service].doSomething2(key)) 178 | } 179 | --------------------------------------------------------------------------------