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 'Mojolicious::Guides::Testing' tutorial. Resolves #1111 #1118
Conversation
I started with this as a section in the cookbook as originally described, but found that it got big really fast. I think having a cookbook section may still be valuable. |
lib/Mojolicious/Guides/Testing.pod
Outdated
because assertions that belong to the same request are syntactically bound in | ||
the same method chain. | ||
|
||
Occsionally it makes sense to break up a test to perform more complex |
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.
Occasionally is misspelled 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.
Emacs, you have failed in your mission.
# Pull out the id from the response | ||
my $newbee = $t->tx->res->json('/id'); | ||
|
||
# Make a new request with data from the previous response |
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 is great to share that you can do this. My only comment is that I've been told (my understanding may be way wrong) that tests shouldn't be built this way. I don't fully understand why. @jberger gave me the advice. If possible, I would love to have better explanations to how tests should be made (links to materials to read, etc). I know it's not exactly Mojolicious-specific, perhaps a reference to basic Testing advice would fit in the opening doc that points us to Learning Perl in 2.5 hours.
I don't understand good testing techniques, I just read the Test::Mojo docs and did what they allowed, they allowed me to do what's suggested here on L109, but @jberger suggested this wasn't a good way to design tests. Again, I may be misunderstanding. Any help in this regard would be wonderful, it would help users of Mojolicious to write really high quality apps that Mojolicious can be proud to be behind. It's not good for Mojolicious to have bad apps written (just look at the stigma associated with Perl from early on!)
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.
If there's a better pattern for picking out data from one response and using it in a subsequent request, I'd love to see it and incorporate it 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.
My point was, as I understand it, that one shouldn't pick out data from a response and use it in a subsequent request. I don't really understand why. Perhaps it was why I was picking data from a response and using it; surely there is a good reason to do so. Perhaps @jberger can help clear this up. Either way, I think it would be great to explain a recommendation in this regard. My reason for saying I think it would be good to explain this is that as you've done you explain that you can do this, and this leads me to go on to create tests that pick data from a response and use it in a subsequent request, but then I'm advised to not do that, I'm confused because the documentation says that I can... Hope that makes sense.
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.
It does make sense. I agree that if these are, say, unit tests, it's probably not a good idea to start slinging state around like that because you're likely conflating model and view/controller concerns, but for an integration test it makes perfect sense, especially if your application is stateful in some way. I'd be reticent to label stateful tests as inherently wrong.
->json_is('/name' => 'Karl'); | ||
|
||
The L<Test::Mojo> object is I<stateful>. As long as we haven't started a new | ||
transaction by invoking one of the C<*_ok> methods, the request and response |
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 is highly informative! I did not know this!! Thank you!!
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 might take it further. Having access to the $t->tx
means that you can test things that we don't have a convenience method for.
cmp_ok $t->tx->res->dom->at('epoch'), '>', 0, 'the current epoch value must certainly be before 1970!';
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 think I made this clear at several other points later in the document… I didn't want to put too much "WOW" too early. Thoughts?
the L<Test::Mojo> constructor, L<Test::Mojo> will instantiate the class and | ||
start it, and cause it to listen on a random (unused) port number. Testing a | ||
Mojolicious application using L<Test::Mojo> will never conflict with running | ||
applications, including the application you're testing. |
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.
Great info!!
lib/Mojolicious/Guides/Testing.pod
Outdated
# Normal route to controller | ||
$r->get('/')->to('example#welcome'); | ||
|
||
# NEW: this route only exists when 'enable_test' is set in the configuration |
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.
Love this!! I've always wanted to have solid advice regarding this.
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.
While this is true, I don't like to encourage having code that only supports tests in your application/production-code. In this case, before serving the first request you could add your /testing
route
$t->app->routes->get('/testing')-> ...
Personally I think a better example is to specify some testing resource, like say a sqlite database or some external url to be used by the application. In your test config you can point it at some dummy or even mock source.
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 is basically a "feature flag", though I didn't call it that. Feature flags allow you to soft-target an audience without doing a full release. Once it's proved out, you refactor the conditional and turn it into a release. Perhaps if I changed the word 'test' and 'testing' in the URL and flag to something like 'feature' would help clarify that? I didn't want to conflate release management with testing, only illustrate how nice it is that Test::Mojo lets you pass in config on the fly.
Perhaps I'll add another example with a database since both you and Stefan asked for that.
lib/Mojolicious/Guides/Testing.pod
Outdated
->status_is(200) | ||
->content_is('You are logged in'); | ||
|
||
# Sends the cookie from the previous transaction |
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.
Question: earlier you stated that as long as we haven't started a new transaction by invoking the *_ok methods then the request and response are available, so why are they available here on L523 when on L518 a new transaction should have started? What am I misunderstanding from your guide regarding this topic?
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.
What you're seeing in this example is Test::Mojo using its Mojo::UserAgent object's cookie handling to keep state between requests.
lib/Mojolicious/Guides/Testing.pod
Outdated
level higher (which tells L<Test::Builder> how far up the call stack to look | ||
when something fails). Finally we use L<Test::More>'s C<is> method to compare | ||
the location header with the expected header value. We wrap that in the | ||
C<success> method, which propagates the object for method chaining. |
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.
Really happy you explained this!
lib/Mojolicious/Guides/Testing.pod
Outdated
|
||
{"temperature":"23°C"} | ||
|
||
To test kind of web application, this we could do something like this for |
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.
To test this kind of web application, this we could do something like this for
lib/Mojolicious/Guides/Testing.pod
Outdated
requests to our deployed web application, and the test assertion (C<post_ok>) | ||
preserves compatibility with L<Test::Mojo>. | ||
|
||
=head1 MORE |
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.
One other thing that would be great to cover is how to handle testing of applications that utilize a database. For a bad example, let's say by hitting / the database counter is incremented by 1. So we get a 200 response but did it actually update the database counter? IIUC that should be unit tested into a Model::DBCounter test which is outside of the scope of Mojolicious, but I personally think some pointers or advice to some degree would be great for helping to ensure that authors of Mojolicious-based apps do as good of a job as possible.
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 have given some MVC training using Mojolicious in the past that might be a good start for that sort of tutorial. I agree that it's out of scope for this, but would be valuable information nonetheless.
channel C<#mojo> on C<irc.perl.org> | ||
(L<chat now!|https://chat.mibbit.com/?channel=%23mojo&server=irc.perl.org>). | ||
|
||
=cut |
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.
WOW!!! Seriously, this guide is SOOO fantastic! You did a great job, thank you so much! I hope it gets approved and brought into core!!! :D
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.
Thanks Stefan—I appreciate the feedback!
lib/Mojolicious/Guides/Testing.pod
Outdated
@@ -646,7 +646,7 @@ Or: | |||
|
|||
{"temperature":"23°C"} | |||
|
|||
To test kind of web application, this we could do something like this for | |||
To test this kind of web application, this we could do something like this for |
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.
Also remove "this" from right after the , ... yes?
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.
Yes, good catch. I must have been dozing when I wrote that.
This is a lot to review, yay! 😃 |
lib/Mojolicious/Guides/Testing.pod
Outdated
|
||
$t->websocket_ok('/hello') | ||
->message_ok | ||
->json_message_like('/howdy' => qr/pard/i); |
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 should replace the cookbook recipe.
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.
Do you mean I should replace this snippet with the cookbook recipe (which is a superior example imo) or vice versa? I didn't spend much time on WebSockets and would like a better section here. Can you clarify?
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.
The cookbook recipe should be removed and this one improved.
lib/Mojolicious/Guides/Testing.pod
Outdated
|
||
A test file for a simple web application might look like: | ||
|
||
use Test::Mojo; |
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.
You probably want use Mojo::Base -strict
here or at least some strict/warnings enabled.
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.
Agreed.
lib/Mojolicious/Guides/Testing.pod
Outdated
|
||
$t->post_ok('/doorbell', | ||
form => { action => "ring once" }); | ||
cmp_ok($t->tx->res->code, "<", 400, "status is good"); |
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 I love comparing the status code as <400. Perhaps some other example can be dreamt up to fill this space?
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.
The problem is that Test::Mojo's assertions cover nearly everything needed under normal testing conditions, so it's hard to come up with meaningful out-of-bounds examples. I'm open to suggestions to illustrate what's possible while keeping it real.
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.
Test $t->tx->res->message
instead.
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.
👍🏻
lib/Mojolicious/Guides/Testing.pod
Outdated
# Still first transaction | ||
$t->content_type_is('application/json'); | ||
|
||
# Second transaction; Test::Mojo tx object is reset |
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.
That comment sounds really bad to me, Test::Mojo tx object
is not a thing.
lib/Mojolicious/Guides/Testing.pod
Outdated
is running and make requests to it. Once the tests have completed, the | ||
L<Mojolicious> application will be torn down. | ||
|
||
my $t = Test::Mojo->new('Frogs'); ## runs on localhost:32114 |
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.
...; ## runs on localhost:32114
is not really a comment style we use in Mojolicious.
introspected from L<Test::Mojo> through the application object. This enables | ||
us to get deep test coverage of L<Mojolicious>-based applications. | ||
|
||
=head1 ASSERTIONS |
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.
The new sections have no sentence describing them though.
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 having a hard time coming up with reasons for me missing all these details that don't make me look stupid! :)
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.
You're doing great, i'm having a hard time finding new things to nitpick about. 😉
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.
On the upside, I can see why Mojolicious is so uniformly fantastic…
Regarding package Test::Mojo::Role::Location;
use Role::Tiny;
sub location_is {
my ($t, $value, $desc) = @_;
$desc ||= "Location: $value";
local $Test::Builder::Level = $Test::Builder::Level + 1;
return $t->success(is($t->tx->res->headers->location, $value, $desc));
}
1; But their application will be slightly different. my $t = Test::Mojo->with_roles('Test::Mojo::Role::Location')->new('MyApp');
$t->get_ok('/')
->status_is(302)
->location_is('http://mojolicious.org')
->or(sub { diag 'Must have been Joel!' }); Mentioning it in case you want to prepare a section before we add the feature in the next few days. 😃 |
#1120 will get applied later today or tomorrow, so |
I'll try to get something together for it.
|
lib/Mojolicious/Guides/Testing.pod
Outdated
But adding the C<Authorization> header for every request is tedious and likely | ||
to result in copy/paste errors given enough tests. We'd like to extend | ||
C<post_ok>'s behavior to build the header for us. With Mojolicious 7.41 (or | ||
later) and L<Role::Tiny> installed, we can compose this behavior with roles. |
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 don't mention Mojolicious versions in the docs.
lib/Mojolicious/Guides/Testing.pod
Outdated
my $url = shift; | ||
my $creds = shift; # username:password | ||
my $headers = (ref $_[0] eq 'HASH' ? shift : {}); | ||
$headers->{Authorization} = "Basic " . b64_encode $creds, ''; |
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.
local $Test::Builder::Level = $Test::Builder::Level + 1;
is missing here.
lib/Mojolicious/Guides/Testing.pod
Outdated
sub post_with_auth_ok { | ||
my $t = shift; | ||
my $url = shift; | ||
my $creds = shift; # username:password |
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.
Unusual comment again.
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.
And i don't see a reason not to write my ($t, $url, $creds) = (shift, shift, shift);
.
lib/Mojolicious/Guides/Testing.pod
Outdated
|
||
First, we create a new package that will implement the role we want: | ||
|
||
package Test::Mojo::Role::BasicAuth; |
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.
Think there should be an empty line here.
lib/Mojolicious/Guides/Testing.pod
Outdated
my $t = shift; | ||
my $url = shift; | ||
my $creds = shift; # username:password | ||
my $headers = (ref $_[0] eq 'HASH' ? shift : {}); |
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 is a little clunky for a doc example, maybe assume the method always gets a hash with headers?
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.
Well, actually it's not a very good example. Since we already support basic authentication through the URL.
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.
Might as well do $url = Mojo::URL->new($url)->userinfo('foo:bar')->to_unsafe_string;
.
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've removed this example.
lib/Mojolicious/Guides/Testing.pod
Outdated
|
||
my $t = Test::Mojo->with_roles('Test::Mojo::Role::BasicAuth')->new('MyApp'); | ||
|
||
$t->post_with_auth_ok('/thermostat/heat', 'me:secretly', |
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.
Side note, $t->post_ok('http://me:secretly@/thermostat/heat', ...)
should just work, sooo this actually requires more typing.
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.
Yeah, I agree. When I realized that you don't get attributes with Role::Tiny, it makes this no better than the built-in, since you have to supply the credentials with every test. I'll pare this down in the next commit.
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.
Technically, you could just use Mojo::Base
to generate an attribute for you.
package Foo::Role;
use Mojo::Base -base;
use Role::Tiny;
has whatever => 'Foo!';
sub do_stuff { say shift->whatever }
This just works. If that should be right in the first example... i'm not so sure about. 😃
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 do like your current example though, it explains a lot of important topics very well.
lib/Mojolicious/Guides/Testing.pod
Outdated
We assign a default description value (C<$desc>), set the L<Test::Builder> | ||
C<Level> global variable one level higher (which tells L<Test::Builder> how | ||
far up the call stack to look when something fails), then we use | ||
L<Test::More>'s C<is> method to compare the location header with the expected |
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.
is
is a function, not a method.
It's funny you bring this up because that's exactly what I was going to try when I had a little more time :)
I think the existing `location_is` example is good enough to get people started.
Any additional things you (or anyone else) would like to see in the tutorial?
|
Ok, maybe I'll put it back in as a second "advanced" example or something, that also blends attributes from Mojo::Base to illustrate how those two things can play together.
|
Good catch.
|
I think the guide is good now. Will give the community a little more time for proofreading and then merge tomorrow. |
Looks like we might not be able to merge after all. The newly added role composition section is a blocker for me. The code examples are just too ugly. I'll step back again until the dust has settled. |
This reverts commit 2d4595e.
reverted
|
Allright, lets consider the guide frozen now, so we can wrap it up. Just typo and formatting fixes from now on. |
100% ok by me.
|
And merged. 🎉 🎂 🎈 http://mojolicious.org/perldoc/Mojolicious/Guides/Testing |
Summary
This is a testing tutorial meant to be included in the Mojolicious::Guides
Motivation
This PR is in response to #1111
References
#1111
Notes
It's a little weak in some areas (WebSockets, HTML/XML parsing come to mind) and I don't cover file uploads at all. I'm willing to continue work on it as needed.