Tuesday, March 15, 2011

Day 33 - Test::Nginx, pipelined_requests, raw_request and other testing tricks

Hello and welcome back to this terrific blog... ;) First and foremost for thos who care: snow was not good but weather was awesome. Now, back to the treacherous slopes of Nginx and more specifically one of its minions: Test::Nginx.

Previously on nginx discovery, I told you I was about to move from my python-based testing to agentzh's Test::Nginx. I was half-way through and the other half was nto the easiest one, so I figured I would share with you the pitfalls I ran into (just to avoid you reading through the perl code like I had to.

URL encoding. Yes, I am a lazy guy and I refuse to commit to memory the hexa code of : (that is if I remember that : should be url encoded, which I tend to forget as there is only one in urls, just after the http). So, when I write a test, I like to keep my arguments simple (e.g. N:12345), not crippled with percent signs (e.g. N%3A12345). Which is not that simple when your test framework is purely declarative. Now, that's where the dynamic aspects of a language like perl comes handy: you can use a perl expression in you data (instead of a regular constant). To be correct, this is actually possible because it is supported by both the language and the testing framework. Here is what it looks like:
=== TEST 4: POST
The main case (when everything submitted is GOOD and the
DB should be updated).
--- config
    location /rrd/taratata {
        rrd /var/rrd/taratata.rrd;
    }
--- more_headers
Content-type: application/x-www-form-urlencoded
--- request eval
use URI::Escape;
"POST /rrd/taratata
value=".uri_escape("N:12345")
--- response_body_like: go round.*Robin
--- error_code: 200
The magic lies in request eval which basically says that the content of request should be applied (or filtered by as the FILTERS section of Test::Base would say) the eval function before being handed over to the tests runner. This is actually a handy feature that I had to dig out of Test::Base. You can apply any kind of perl function on this. Gives you the power of perl without breaking the "declarative" paradigm.

One test, multiple requests. As I mentioned before, one of my python tests was specifically built to show that once a RRD request was in error, all the following requests would be too. It went like:
  1. POST with a correct value: response is OK
  2. POST with a bad value: server barks out with a message
  3. POST with the same value as in step 1: response is OK
The only way I found to do so is to use the pipelined_requests. Here is the relevant snippet:
--- more_headers
Content-type: application/x-www-form-urlencoded
--- pipelined_requests eval
use URI::Escape;
["POST /rrd/taratata
value=".uri_escape("N:12345"), "POST /rrd/taratata
value=".uri_escape("N:whatever"), "POST /rrd/taratata
value=".uri_escape("N:12345")]
--- response_body_like: Robin.*Problem.*Robin
--- error_code: 200
This is not strictly equivalent to what I was doing for a couple of reasons:
  1. The requests are all performed on the same TCP connection (from what I can see, pipelined_requests was intended to test HTTP/1.1 cases).
  2. Checking the results is not as easy as it was in python (or as one could hope), so you end up testing only the last error_code and using a pattern Robin.*Problem.*Robin to make sure the responses were OK/KO/OK.
  3. The client sends all the requests in one gulp whereas with python I was waiting for request one to complete before sending request 2 etc. This is not a problem in our case because there is only one nginx process handling all the requests and they are handled sequentially. But this is not as natural as what I used to do with python.

Controling the buffers. I talked about the buffer problems you have to face/understand when dealing with POST requests and I even made a post about this: Post body buffers. There is a way to finely control what is sent by the test and when. But it takes you to manually craft the request yourself. Something most people don't like to do because it requires a good understanding of the HTTP protocol. On the other side, if you are trying to test nginx buffering special cases, there is a good chance HTTP doesn't scare you:
=== TEST 10: POST show buffers
Request specially crafted to produce 2 buffers in the body received
handler (one with the beginning of the entity read with the headers
and one with the rest of the body).
--- config
    location /rrd/taratata {
        rrd /var/rrd/taratata.rrd;
    }
--- raw_request eval
use URI::Escape;
my $val="value=".uri_escape("N:12345:678").("678"x300);
["POST /rrd/taratata HTTP/1.1\r
Host: localhost\r
Connection: Close\r
Content-Type: application/x-www-form-urlencoded\r
Content-Length:".length($val)."\r\n\r\n",
substr($val, 0, 6),
substr($val, 6, 15),
substr($val, 21)]
--- raw_request_middle_delay
1
--- response_body_like: Problem .*updating
--- error_code: 500
The two things worth noting here are the raw_request and raw_request_middle_delay. The first one provides the "chunks" that constitute the request where as the second one indicates how much the test should wait between two chunks.

Now, I managed to move all my testing to Test::Nginx. And there are still things I think should be improved. But one of the things I did not like in my previous post (namely the inability to run only one test) is solved by Test::Base (using --- ONLY to have it run only the specified test).

I'll try to chat with agentzh to see if there is any chance we could improve things.

No comments:

Post a Comment