-
Couldn't load subscription status.
- Fork 1.4k
Mocking framework #1830
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Mocking framework #1830
Conversation
Usage examplesimport zio.{UIO, ZIO}
import zio.test.mock.{Method, Mock, Mockable}
// module
trait Cache {
val cache: Cache.Service[Any]
}
object Cache {
// service
trait Service[R] {
def get(id: Int): ZIO[R, Nothing, String]
def set(id: Int, value: String): ZIO[R, Nothing, Unit]
val clear: ZIO[R, Nothing, Unit]
}
// capability tags
object Service {
case object get extends Method[Int, String]
case object set extends Method[(Int, String), Unit]
case object clear extends Method[Nothing, Unit]
}
// mock implementation
implicit val mockable: Mockable[Cache] = (mock: Mock) =>
new Cache {
val cache = new Service[Any] {
def get(id: Int): UIO[String] = mock(Service.get)(id)
def set(id: Int, value: String): UIO[Unit] = mock(Service.set)(id, value)
val clear: UIO[Unit] = mock(Service.clear)
}
}
// helper object for easy access to service capabilities
object > extends Service[Cache] {
def get(id: Int) = ZIO.accessM(_.cache.get(id))
def set(id: Int, value: String) = ZIO.accessM(_.cache.set(id, value))
val clear = ZIO.accessM(_.cache.clear)
}
}The With macros the example above can be shortened to: import zio.ZIO
import zio.macros.access.Accessable
import zio.macros.mock.Mockable
@Accessable
@Mockable
trait Cache {
val cache: Cache.Service[Any]
}
object Cache {
trait Service[R] {
def get(id: Int): ZIO[R, Nothing, String]
def set(id: Int, value: String): ZIO[R, Nothing, Unit]
val clear: ZIO[R, Nothing, Unit]
}
}
Having all that machinery in place, this is how it would be used in tests: import zio.{UIO, ZIO, ZManaged}
import zio.test.{assertM, suite, testM, DefaultRunnableSpec}
import zio.test.mock.MockSpec
import zio.test.Assertion.{anything, equalTo}
object CacheSpec extends DefaultRunnableSpec(
suite("Cache")(
testM("mock get 4 times") {
import Cache.mockable
val managedEnv = (
MockSpec.expectM(Cache.Service.get)(equalTo(1))(_ => UIO.succeed("foo")) *>
MockSpec.expectM_(Cache.Service.get)(equalTo(2))(UIO.succeed("bar")) *>
MockSpec.expect(Cache.Service.get)(equalTo(3))(_ => "baz") *>
MockSpec.expect_(Cache.Service.get)(equalTo(4))("acme")
)
val app =
for {
str1 <- Cache.>.get(1)
str2 <- Cache.>.get(2)
str3 <- Cache.>.get(3)
str4 <- Cache.>.get(4)
} yield s"$str1 $str2 $str3 $str4"
managedEnv.use { env =>
val result = app.provide(env).flatten
assertM(result, equalTo("foo bar baz acme"))
}
}
, testM("mock get > set > get") {
import Cache.mockable
val managedEnv = (
MockSpec.expect_(Cache.Service.get)(equalTo(1))("foo") *>
MockSpec.expectIn(Cache.Service.set)(equalTo(2 -> "bar")) *>
MockSpec.expect_(Cache.Service.get)(equalTo(3))("baz")
)
val app =
for {
str1 <- Cache.>.get(1)
_ <- Cache.>.set(2, "bar")
str2 <- Cache.>.get(3)
} yield s"$str1 $str2"
managedEnv.use { env =>
val result = app.provide(env).flatten
assertM(result, equalTo("foo baz"))
}
}
, testM("mock clear > get") {
import Cache.mockable
val managedEnv = (
MockSpec.expectOut_(Cache.Service.clear)(()) *>
MockSpec.expect_(Cache.Service.get)(equalTo(1))("")
)
val app = Cache.>.clear *> Cache.>.get(1)
managedEnv.use { env =>
val result = app.provide(env).flatten
assertM(output, equalTo(""))
}
}
)
) |
|
I think it is just we have a namespace conflict because all the existing |
|
Anyway, will review in more detail later but very excited about this! |
446840d to
5de4d71
Compare
|
Added combinators and updated example in PR description. |
|
Damn this is nice! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks really great! For the next round can we try to get a test based on your example? I always find that is helpful to make sure that the user facing API still works as you evolve it. I tried to run the example locally and there were some methods like ExpectM that were in the example but not the code.
I think we need to work a little but on the naming between this and the existing mock objects so we don't have namespace conflicts and it is clear what each of them are and when you should use them.
Overall though this looks really slick! Excited to get it in!
|
Looking good! I like how it reuses assertions 👍 |
c6f2c8d to
16fb403
Compare
|
Work in progress. Do not merge yet - I plan to add a few things today and tomorrow. |
1c20687 to
ac7b86f
Compare
|
Ready for another round of review. Still on TODO list:
|
|
Okay, will take a look tonight. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking good. Just a couple of minor comments.
We should figure out what we want to do on the tests. To date we have had the philosophy that we didn't want to use ZIO Test as the testing framework for ZIO Test's own internal test suite. I still think there is value in that though I know you have a different point of view that it is more akin to a compiler bootstrapping itself after it reaches a certain maturity. Regardless we should align one way or the other because it doesn't make much sense to have some tests written in ZIO Test and others written in the bootstrap testing framework.
As stated above, I think it's OK to test I'd like to hear from others @iravid @ghostdogpr @jdegoes @neko-kai WDYT? |
cf29cbc to
eb03592
Compare
|
@sideeffffect That's really nice! Any idea why is seems to show the same failure twice? |
|
Personally, I have no real opinion on whether ZIO Test should be tested with ZIO Test itself. I think on the one hand, it makes it slightly more difficult for maintainers, because if one things break, many things break, and it can be hard to figure out what the root cause is; but on the other hand, it reduces code duplication and is a way of "dogfooding". Indeed, it is a sign of compiler maturity when a language can bootstrap itself, and this process generally increases contributions because the same people using the "language" can now contribute in the same "language". My opinion is mainly that we should NOT mix two styles, that is, go all-in on one way or the other. |
|
🤷♂️ sorry, @adamgfraser no idea, I've just enabled this feature recently and I am no expert in CircleCI EDIT: only thing I know is that it's based on reports from |
|
I have the same issue with bitbucket pipelines at work (results reported twice) so I guess the issue is in |
|
@ioleo One more substantive comment. Would it make sense to add sequence/traverse methods to |
|
@jdegoes Those are good points. I had liked the idea of having an "independent check" on ZIO Test but you are right that it would make it easier for contributors and would let us replace more ad hoc implementations of various testing functionality with the ones in ZIO Test. I also looked at some other testing frameworks and Scalacheck, Specs2, and Hedgehog all test against themselves (though Hedgehog has a funny note about this very issue). Most of all I agree that we should only have one way of writing tests for ZIO Test. So if we are going to switch we should make a relatively concerted effort to move everything over. Maybe this could be a good issue for the next hackathon? |
|
@ghostdogpr @adamgfraser @jdegoes Added documentation entries:
I'm not sure if I need to do something to have their links displayed in the left menu of "How to" section? Regarding the discussion:
|
|
@ghostdogpr regarding the release notes, here is a quick overview what has changed:
If you need more detailed info I'll be on gitter tomorrow (aka in ~8h from now). |
|
@ioleo you need to edit https://github.com/zio/zio/blob/master/website/sidebars.json for the sidebar. I created a draft for v1.0.0-RC14 release notes with your information so that whoever does the release will have it. |
|
You can run |
|
@ioleo This looks great! Awesome job adding a ton of documentation. I think there is one comment from my previous review that is still outstanding and I left some minor comments on the documentation you added. Completely agree with you about adding the |
|
Rebased & resolved conflicts. Will get to comments now. |
|
I think this can be merged now and we can deal with why was mdoc failing in a seperate PR. |
* Add mocking framework * Ignore mdoc compile errors
This PR introduces the framework for zero-dependency mocking of services. Kudos to @jdegoes for the design and helping me out with some type inference issues ❤️.
I think it's best explained by showing how it's supposed to be used:
(examples moved below)
There is still room for improvement:
MockSpec.expectMockExceptioninzio.Testand display nicely the failed expectationsMockSpec, eg. one that ignores theAor one without predicate like hereMockSpec.expect(Cache.Service.Clear)(anything)(_ => UIO.unit)sincecleardoes not take any input values (input type isNothing), but we're forced to add theanythingpredicate and ignore the value when producing the return value_ => UIO.unitzio.testor deal otherwise with TestMail testsswitch tono longer neccessaryZIO.dieOptionto drop tracing info & refactor DefaultTestRenderer to match onCause.Diedirectly (without wrappingCause.Traced) when Add Meta Subtype to Cause #1706 is mergedCC @adamgfraser @ghostdogpr @jdegoes