-
Notifications
You must be signed in to change notification settings - Fork 120
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
Add Req.Test.expect/3 #325
Conversation
Could you share a real world-ish example of using this API? It's amazing it's so little code thanks to NimbleOwnership. I'm not sure about expanding the scope, I'd really prefer to keep it focused on just stubs. However, maybe the way it started, this is the only logical next step. I.e. because it's possible to set stubs per test it's only natural you want to set some expectations. Maybe these are not stubs something that has predefined behaviour (i.e. completely static) but these were mocks all along. Sorry thinking out loud here and getting a bit philosophical. Anyhow, an example would be great! |
My tests are now riddled with stubs like this: Example codeReq.Test.stub(Klaviyo.API, fn conn ->
cond do
conn.method == "PATCH" and conn.request_path == "/api/profiles/#{user.klaviyo_id}" ->
send(test_pid, :updated)
conn
|> Plug.Conn.put_status(200)
|> Req.Test.json(%{})
conn.method == "POST" and conn.request_path =~ "/api/lists/" ->
assert ["api", "lists", list_id, "relationships", "profiles"] =
String.split(conn.request_path, "/", trim: true)
send(test_pid, {:added_to_list, list_id})
conn
|> Plug.Conn.put_status(204)
|> Req.Test.json(%{})
true ->
flunk("No stub defined for request: #{conn.method} #{conn.request_path}")
end
end) Essentially, I have to do the heavy lifting of figuring out which request I’m stubbing, how many requests have been made, in which order, and so on. Expectations let me define an order of requests so I can rewrite the example above with something like this: Req.Test.expect(Klaviyo.API, fn conn ->
assert conn.mathod == "PATCH"
assert conn.request_path = ...
conn
|> Plug.Conn.put_status(200)
|> Req.Test.json(%{})
end)
Req.Test.expect(...) Now it takes care of the ordering, of making sure these don't get called too many times (for too few times we need
Yep 🙃 |
adfbf88
to
3516e03
Compare
Thanks for the example, that's super useful. I think for your particular use case something custom would be way more appealing. Something like the following perhaps? Klaviyo.Test.expect("PATCH", "/api/profiles/#{user.klaviyo_id}", fn conn, _params ->
Req.Test.json(conn, %{})
end)
Klaviyo.Test.expect("POST", "/api/lists/:id/relationships/profiles", fn conn, %{"id" => id} ->
Req.Test.json(%{conn | status: 204}, %{})
end) And I don't think it would be super difficult to implement. Ideally you'd be able to build this custom solution on top of Req.Test but I gotta a feeling you'd not be able to reuse any of Such All that being said, I'm not sure on expanding the scope just yet so I'm thinking about holding off on this. Important question is if we can ship WDYT? |
I guess part of my hesitations is I tend not to write too many expectations which, yeah, make my tests catch less bugs but I don't think I've been burned too badly. (It really helps that I always write perfect code.) Maybe I should read a book on testing or something! |
Mmm sure but I think if anything having only stubs makes it easier to write bugs in the code, no?
The API you suggested would be quite a bit of custom code that I'd end up writing for all HTTP APIs I used Req with, really. Every call to the proposed
Yeah this I absolutely don't think we're painting ourselves into a corner. If you want to ship |
OK, let's forget about
I guess my idea was to use NimbleOwnership process started by Req and write your custom expect/2 function just like this in PR which wouldn't be a lot of code but I don't think it's gonna work after all as the ownership will hold different state. So you'd need to reimplement everything, here's a quick very lightly tested draft: https://gist.github.com/wojtekmach/bb4a77037386c170d103d8beb455de37. Fair enough it's a bit more stuff than I expected.
That's very fair. I'll think about it. Btw to take a big step back, the whole deal with Mox is of course we use explicit contracts. Req.Test a bit mimics Mox. But With Req.Test we can stub (and with this PR also expect) willy nilly anything. Maybe that's gonna be misused? |
That's a 100 lines of pretty complex code if you ask me. Sure I can do that, but I’m not sure most folks are familiar with nimble_ownership, which itself is a pretty niche lib targeted at a pretty specific use case.
I don't think you can stub anything, I think you can only stub/expect requests. Req is a HTTP client that only makes HTTP requests (with a bunch of stuff to embellish the requests, sure, but at the end of the day) and we're stubbing/expecting only that, so I think it fits like a glove? 🙃
|
OK I partially take it back, we can't stub willy nilly anything, the stubs are explicitly named. The point still stands that we can put anything into a stub. The primary use case has always been plug stubs. But another totally viable use case is Bypass. I don't talk about it in docs but this is possible today: # config/test.exs
config :myapp, req_options: [base_url: fn -> Req.Test.stub(:foo) end]
# test/myapp_test.exs
Req.Test.stub(:foo, "http://localhost:#{bypass.port}") But yeah, nevermind, I freaked out a little bit but because stubs are explicitly named I think we're good.
Yeah it all makes sense. I think given all this I'm leaning towards adding My question is, does this look ok conceptually? Req.Test.verify(:foo, 1)
Req.Test.stub(:foo) I.e. we define an expectation so it's a mock but then refer to it as stub? Should we rename:
(Let's not do any changes just yet.) I guess it's all a bit blurry to me. The way I always understood mocks was we expect function fun to be called with arg1..argN and return result1. Here there are no functions involved per se, I mean, again, the primary use case is stubbing plug functions but these functions are treated as data if that makes sense. But maybe it doesn't matter in practice. |
Not really.
I think you hit the nail on the head: it doesn't matter in practice.
That's a subset of what mocks can be. Terminology is always hard to agree on, but the concept of a "mock" in this case is more like a value that you can use in place of the real thing. In Mox those are functions and behaviors (as the value), but here it can be a plug or a value that you return (Bypass URL) or whatever. Even with that, you still might want to assert that a value is fetched |
One more note: I don't think the "stub can be any value" vs "stub is behavior in the context of mocks, so we need |
Ok to recap, today we have: Req.Test.stub(stub_name)
Req.Test.stub(stub_name, value)
Req.Test.allow(stub_name, owner_pid, pid_to_allow) I'd be OK with renaming to Req.Test.stub(mock_name, value)
Req.Test.expect(mock_name, count \\ 1, value)
Req.Test.allow(mock_name, owner_pid, pid_to_allow)
Req.Test.get(mock_name)
# and eventually also:
Req.Test.verify!()
Req.Test.verify!(mock_name)
Req.Test.verify_on_exit!() Strong Mox vibes can make it really easy for folks to pick up. It's gonna suck when someone in the same test is gonna import both Req.Test and Mox though, which is totally gonna happen, but I guess it is what it is. Speaking of Mox, worth pointing out that it's trivial to replicate Req.Test with it: Mix.install([:req, :plug, :mox])
Mox.defmock(MyApp.Mock, for: Plug)
Mox.stub(MyApp.Mock, :call, fn conn, [] ->
Plug.Conn.send_resp(conn, 200, "hi")
end)
"hi" = Req.get!(plug: MyApp.Mock).body (which is how Req.Test started btw) but it's important to me to have something nice built-in and I guess it grows on me that having expectations is non negotiable. I'll think some more about it. |
I would call it
If |
Yeah I've been thinking more and more about this... I like this. I'd do this for now.
It is. We can rename these to
Love |
Maybe we don't suggest users to import. |
A separate So the benefit of not directly depending on NimbleOwnership is on principle we should bring as least deps as possible, less package downloads for folks, less possible version conflicts. That being said, NimbleOwnership is pretty nimble, 300 LOC without docs & specs... I might just vendor it in and be on the hook for manually updating it. Would that be ridiculous? In any case, I'll think about Yeah, not suggesting to import Req.Test sounds good to me. Regarding |
You could vendor in but the |
No as in group of requests, but good point. |
Thank you! I'm still not sure about naming, stub/mock/group/scope/etc, so will leave it as is for now. |
@wojtekmach I think this makes sense in the optics of tests and stubs. I have found myself doing a lot of
send(test_pid, ...)
to ensure that everything gets called and in the right order. This helps a lot with that, since it introduces ordering. We'd need the usualverify
suspects, but I'll only spend time on those if you think this change is good 😄