-
Notifications
You must be signed in to change notification settings - Fork 645
Begin working on a revamped test harness #1697
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
Conversation
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.
In general I'm supportive of adapting to an API similar to reqwest
. I'd like to point out some things I observed when building out parts of the v2 harness that I think are worth considering in the final design.
Auth
The current API for auth has felt really funky to me. Having an object
called User constructing HTTP requests felt backwards, and it also
meant we had to decide up front whether we were going to send some
requests signed in or not, and also how we were authenticating. It
also didn't have a good way to send requests as more than one user.
One of the huge issues I had in following some scenarios in the v1 harness was that in many tests a single request was created and then mutated throughout the test run. Sometimes it was necessary to trace through the logic to determine which combination of HTTP method, url, and user were in play for a particular request.
While I like the idea of building partial requests and then being able to add authentication along the way, I'd like to make sure that it isn't possible (in the common workflow) to create a request with multiple cookies/tokens or even both a cookie and a token. Maybe authentication can normally be provided in the final step, such as .send_with(cookie)
and .send_with(token)
, with .send()
producing an anonymous request.
It also didn't have a good way to send requests as more than one user.
This didn't work well in the v1 design, but this should be addressed by the v2 implementation. Calling app.db_new_user("user2")
will add a user to the database and return a MockCookieUser
value that represents that user and their session cookie. Calling user2.db_new_token("bar")
creates a token for that user and gives a value that authenticates with the new token. These can be initialized at the beginning of the test and used to issue multiple requests.
Response handling
request.send()
returns aResult
, where a 4xx or 5xx response is
considered an errorresponse.json<T>()
will parse the body into the given type
While I support having a better split between handling response codes and deserializing the response, there is one area where I think it is helpful to keep these somewhat coupled. Until we can switch to 5xx for all api errors on cargo endpoints, it is very helpful to require a status code when deserializing to the Bad
JSON response value with the current helper. In the v2 test harness forces the test to assert the expected response code for that API endpoint.
Error handling
This is a bit tricky, and I'll probably be tweaking as I move tests over,
since results bubbled all the way up with?
will completely lose the
backtrace. Right now, I'm figuring that things we really just don't
expect to fail ever get bubbled up with?
, while the things we're
actually testing will remain as assertions.
In my experience I would stick to calling unwrap
everywhere. I never find the source reference in the top-level error to be helpful without a backtrace. When using unwrap
the error points to src/libcore/result.rs:997:5
and when using ?
you get /cargo/registry/src/git.colasdn.top-1ecc6299db9ec823/libtest-0.0.1/lib.rs:335:5
. As you mention, the downside of ?
is that when you do request that backtrace, much of the associated call stack context is lost. If an error condition is unexpected in the tests, I think it usually makes sense to panic as quickly as possible.
More conversions
I've started by just moving tests/read_only_mode over, since it's a
relatively small file, and also one where the old API was more painful
for me to use. I'll be porting other files over in the next few days.
There are about a dozen tests remaining against the v1 harness (calling app()
or simple_app()
directly). It may be helpful to convert those next so that everything is either v2 or v3 going forward. I'd also like to land #1686 soon, which includes some finishing touches on the v2 harness in terms of orchestrating the index, workers, and playback proxy.
|
||
app.db(|conn| { | ||
CrateBuilder::new("foo_download_read_only", user.as_model().id) | ||
let user = app.create_user("new_user")?; |
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.
I'm not sure how this works here, because crate_user
appears to get a separate database connection from the pool, but as far as I can tell this is working. I can even add app.get("/api/v1/users/new_user").send().unwrap()
within the db
closure (after the user is created), and everything seems to work. I don't yet understand exactly what is going on here.
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.
We only have the single connection in tests, it doesn't matter how many times we ask for it we'll always get back the same connection
.delete("/api/v1/crates/foo_yank_read_only/1.0.0/yank") | ||
.with_token(&token) | ||
.send() | ||
.allow_errors()?; |
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.
Does this mean every time we test for an error, we have to call allow_errors
?
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.
Right now, yeah. I'll likely end up with an expect_status
function that returns the response if the status matched or panics otherwise. Erroring on 4xxx/5xx felt like the right default though, so this makes sense as a primitive to get out of that default behavior
Indeed, I'm wondering how the proposed changes in this PR would look with multiple requests done by the same user. I rather like how the current tests make you think about who is doing the requesting up front, and that it's
@sgrif could you perhaps convert a test that sends requests as more than one user as part of this PR to explore what would be different?
Do you propose having all tests that publish a crate be in one file, or do you propose duplicating the publish code in multiple files? There are multiple files that publish crates now, and I've also often considered splitting up the krate tests into multiple files because there's a lot of tests in that file and it's hard to navigate.
👍 👍 👍 👍 👍 I do think there's room for improvement with the current test utils; I have an unfinished branch somewhere trying to make the construction of app/user/etc not need so many underscores to ignore unneeded parts. |
I've specifically made it so
I'd expect the tests around publish behavior to live in one file, yes. This is a false dichotomy though, we can certainly put shared helpers in their own files. I'm just arguing we shouldn't have them live on |
This is yet another attempt to improve our test harness. This is intended to be a replacement for `TestApp` which currently lives in `tests/util.rs` This new structure tries to address several pain points I've had using the existing harness: - Abstracting into smaller pieces Most of the functions in the current harness give no opportunity to modify the request, require a type to parse the response into, and assume the request is a 2xx or 3xx response. This has made testing our edge cases difficult, and they're less well tested as a result. I've attempted to make it easier to do: "just like this other thing but with an additional header set", or "I don't care about the response body" What was previously `anon.get::<T>("...")` will now be 3 separate pieces: - `app.get("...")` returns a request object that can be modified if needed - `request.send()` returns a `Result`, where a 4xx or 5xx response is considered an error - `response.json<T>()` will parse the body into the given type The API is roughly modeled after `reqwest`, which I've enjoyed using in our app. - More explicit handling of auth The current API for auth has felt really funky to me. Having an object called `User` constructing HTTP requests felt backwards, and it also meant we had to decide up front whether we were going to send some requests signed in or not, and also how we were authenticating. It also didn't have a good way to send requests as more than one user. The new API moves auth to be a part of request construction. So `user.get("...")` becomes `app.get("...").as_user(&user)` Finally, I've structured this new API around returning `Result` everywhere, so that we can keep assertions closer to the tests where they matter (giving line numbers in the test, not the util file). This is a bit tricky, and I'll probably be tweaking as I move tests over, since results bubbled all the way up with `?` will completely lose the backtrace. Right now, I'm figuring that things we really just don't expect to fail ever get bubbled up with `?`, while the things we're actually testing will remain as assertions. The tests I've moved over don't have a ton to abstract, but I'd also like to keep helpers like `publish` off of the `App` struct, and instead keep them in the test file where they're relevant. I've started by just moving `tests/read_only_mode` over, since it's a relatively small file, and also one where the old API was more painful for me to use. I'll be porting other files over in the next few days.
72214c7
to
86a9270
Compare
I've added a test ( |
Just to address a few remaining comments that I don't think have been addressed in the code or could use further explanation:
Following the builder pattern, it's possible to override the previous value by calling the function multiple times, but I've made sure that it never sends both a token and a cookie.
As of Rust 1.34 (which is releasing this week), errors are nicely displayed regardless of response code. As part of migrating tests over to this harness, I think we should also go through the process of converting the endpoints being tested to non-2xx response codes on error (and also probably ensuring its error conditions are sufficiently tested). I'd really like to have
I'm fine with using
Just to make sure I've communicated this well, all methods around building a request, including With the changes I've made and these notes, are we comfortable moving forward with this direction? There will certainly be more iteration on this as more tests get converted (as was the case with the v2 harness), but hopefully we're on the same page about the general direction this is headed. |
☔ The latest upstream changes (presumably #1686) made this pull request unmergeable. Please resolve the merge conflicts. |
Fix yank test This test wasn't set up correctly, so the error message that was being checked wasn't quite right. Also, [based on this comment](#1697 (comment)) it seems like the names of these tests weren't as descriptive as they could be-- the previous test covers the success case of an owner being able to yank, and the next text checks that nonowners aren't allowed to yank.
Fix yank test This test wasn't set up correctly, so the error message that was being checked wasn't quite right. Also, [based on this comment](#1697 (comment)) it seems like the names of these tests weren't as descriptive as they could be-- the previous test covers the success case of an owner being able to yank, and the next text checks that nonowners aren't allowed to yank.
@jtgeibel / @carols10cents - if the rebasing and resolving conflicts are the only remaining actions for this PR, I'm happy to help and reduce our PR backlog before 2020 :) |
I'm going to close this for now. We can always reopen this PR if we have the bandwidth to convert over a larger portion of the tests. |
This is yet another attempt to improve our test harness. This is
intended to be a replacement for
TestApp
which currently lives intests/util.rs
This new structure tries to address several pain points I've had using
the existing harness:
Abstracting into smaller pieces
Most of the functions in the current harness give no opportunity to
modify the request, require a type to parse the response into, and
assume the request is a 2xx or 3xx response. This has made testing our
edge cases difficult, and they're less well tested as a result. I've
attempted to make it easier to do: "just like this other thing but
with an additional header set", or "I don't care about the response
body"
What was previously
anon.get::<T>("...")
will now be 3 separatepieces:
app.get("...")
returns a request object that can be modified ifneeded
request.send()
returns aResult
, where a 4xx or 5xx response isconsidered an error
response.json<T>()
will parse the body into the given typeThe API is roughly modeled after
reqwest
, which I've enjoyed usingin our app.
More explicit handling of auth
The current API for auth has felt really funky to me. Having an object
called
User
constructing HTTP requests felt backwards, and it alsomeant we had to decide up front whether we were going to send some
requests signed in or not, and also how we were authenticating. It
also didn't have a good way to send requests as more than one user.
The new API moves auth to be a part of request construction. So
user.get("...")
becomesapp.get("...").as_user(&user)
Finally, I've structured this new API around returning
Result
everywhere, so that we can keep assertions closer to the tests where
they matter (giving line numbers in the test, not the util file). This
is a bit tricky, and I'll probably be tweaking as I move tests over,
since results bubbled all the way up with
?
will completely lose thebacktrace. Right now, I'm figuring that things we really just don't
expect to fail ever get bubbled up with
?
, while the things we'reactually testing will remain as assertions.
The tests I've moved over don't have a ton to abstract, but I'd also
like to keep helpers like
publish
off of theApp
struct, and insteadkeep them in the test file where they're relevant.
I've started by just moving
tests/read_only_mode
over, since it's arelatively small file, and also one where the old API was more painful
for me to use. I'll be porting other files over in the next few days.