Thursday, July 18, 2013

HttpClient for Testing HTTP Services in Dart


I have gotten reasonably adept at using HttpRequest in Dart unit tests to make actual connections to test servers. But that is the HttpRequest class from the dart:html library, not in dart:io.

The HttpRequest class in dart:io is very different from the class of the same name in dart:html—the primary difference being that it is impossible to directly instantiate an HttpRequest object in dart:io. The server-side HttpRequest, which lives in the dart:io library, is only for encapsulating server requests. As such, only an HttpServer can create a dart:io HttpRequest.

Since the dart:io HttpRequest encapsulates the request that a server sees, it cannot be used to establish a new connection to a server like its dart:html counterpart (which replaces the venerable XMLHttpRequest in Dart). So the question is, how do I write a unit test in dart:io (i.e. doesn't use a web browser context)?

I believe that the answer is with the HttpClient class.

What I hope to do is start the server that I am trying to test in a setup block. Once that is ready, then I would like to test the /stub resource with HttpRequest.

To get the setup working, I need a Future to tell me when the server is ready. For that, I need my library's main() method to return the result of the server's bind() method, which is a future that completes when the server is ready:
library plumbur_kruk;
// imports and variable declaration...
main() {
  return HttpServer.bind('127.0.0.1', 31337)..then((app) {
    // do server stuff here...
  });
}
I use the method cascade operator (the “..”) to invoke then() on the future returned from the bind() call. Instead of returning the result of the then() call, the method cascade returns the object whose method is being invoked. In other words, I return the same Future on which this then() is being invoked.

Back in my test, I create a test group that runs this main() method:
  group("Running Server", (){
    var server;
    setUp((){
      return PlumburKruk.main()
        ..then((s){ server = s; });
    });
    // tests go here...
  });
I use the same method cascade technique here that I used in the actual server. Once the main() function's server is running, then I assign a local variable server to the server passed from the bind() method's future. More importantly, I return the same future from the setUp() block. In Dart unit tests, returning a future from setUp() blocks the tests from running until the future completes. In this case, my tests will block until the server is ready, which is exactly what I want.

Once the server is up and running, I can write my test. I create a POST request from the HttpClient. To create the POST's body content, I have to use the future returned from the client to write() the body:
  group("Running Server", (){
    // setup...
    test("POST /stub responds successfully", (){
      new HttpClient().
        postUrl(Uri.parse("http://localhost:31337/stub")).
        then((request) {
          request.write('{"foo": 42}');
          return request.close();
        });
    });
  });
I am still not testing anything here, but I have sent the request to the server and am ready to set the expectation that will test things.

And here, I find something new—that a then() call on a future returns another future. It makes sense, but I had never really given it much thought. I have always performed some asynchronous action, then performed a single action when it was done.

In this case, I need to wait for the HttpClient() to open a connection at the specified URL, then post some data, and once that is complete, then I do what I want with the response:
  group("Running Server", (){
    var server;
    setUp((){
      return PlumburKruk.main()
        ..then((s){ server = s; });
    });

    tearDown(() => server.close());

    test("POST /stub responds successfully", (){
      new HttpClient().
        postUrl(Uri.parse("http://localhost:31337/stub")).
        then((request) {
          request.write('{"foo": 42}');
          return request.close();
        }).
        then(expectAsync1(
           (response) {
             expect(response.statusCode, 204);
           }
        ));
    });
  });
I use the normal expectAsync1() in the final then() to block the test until the expected asynchronous call with one argument (the response in this case) fires.

Just like that, I have another passing test against my test server:
➜  plumpbur-kruk git:(master) ✗ dart test/server_test.dart
unittest-suite-wait-for-done
PASS: Core Server non-existent resource results in a 404
PASS: Running Server POST /stub responds successfully

All 2 tests passed.
unittest-suite-success
Yay!

I like HttpClient, though I do wish that I could specify the POST body in the constructor or in the post() method. I still dislike the two different HttpRequest classes as it is easy to find myself looking at the wrong documentation. Nevertheless, I think that I am getting the hang of all this.


Day #816

No comments:

Post a Comment