Tuesday, August 20, 2013

Coding Humility: Actually Trying to Use My Library


I think that my fake HTTP test server for Dart is ready to go. To be sure, I am going to try to replace some pretty awful mock HTTP request code that I have been using to test code in Dart for Hipsters.

The code in question is a main() entry point that makes an HTTP GET request of the /comics resource, parses the response as JSON, and uses a template function to populate an UL element:
main() {
  var list_el = document.query('#comics-list'),
      req = MaybeMockHttpRequest.httpRequest();
  req.open('get', '/comics');
  req.onLoad.listen((res) {
    var list = JSON.parse(req.responseText);
    list_el.innerHtml = graphic_novels_template(list);
  });
  req.send();
}
It is pretty standard stuff that, accompanied by my typically wonderful prose, is easy to follow for existing and new web programmers interested in Dart. The problem, of course, is that MaybeMockHttpRequest junk that is pretending to be an HTTP request object. Before it goes in the book, I have to jump through several hoops to replace it with new HttpRequest(). Even if I did not have to jump through said hoops, this would remain an undesirable situation as I have no way to catch changes to the HttpRequest API.

As an aside, I might write this with methods cascades:
main() {
  var list_el = document.query('#comics-list');

  new HttpRequest()
    ..open('get', '/comics')
    ..onLoad.listen((req) {
        var list = JSON.parse(req.target.responseText);
        list_el.innerHtml = graphic_novels_template(list);
      })
    ..send();
}
But this is an example from early in the book, so I stick with the more traditional way of writing this:
main() {
  var list_el = document.query('#comics-list');
  var req = new HttpRequest();
  req.open('get', '/comics');
  req.onLoad.listen((req) {
    var list = JSON.parse(req.target.responseText);
    list_el.innerHtml = graphic_novels_template(list);
  });
  req.send();
}
Gah! That's terrible. Dart, dammit, you're ruining me for other languages. Anyhow…

By default, my fake HTTP test server, code named plummbur_kruk, does not have resources at /comics. Instead it has a REST-like data store at /widget. I have options with this, but I want to get this working first, so I change the GET request to include the server and /widgets path:
main() {
  var list_el = document.query('#comics-list');
  var req = new HttpRequest();
  req.open('get', '${Kruk.SERVER_ROOT}/widgets');
  req.onLoad.listen((req) {
    var list = JSON.parse(req.target.responseText);
    list_el.innerHtml = graphic_novels_template(list);
  });
  req.send();
}
I need the SERVER_ROOT because my test is not loaded from the same server with /widgets. My test is loaded from the filesystem, so a root relative URL will not work, hence the need for the full http:// URL. Again, I think that I have options to get around this, but I will explore them once I get this working.

My test runner Bash script already starts the plummbur_kruk server:
  # Start the test server
  dart dummy_server.dart &
  server_pid=$!

  # Run a set of Dart Unit tests
  # ...

  # Stop the server
  kill $server_pid
  rm -f test.db test/test.db
So all that I need to do is write a plummbur_kruk enabled test. I import both the main.dart that contains the main() entry point and plummbur_kruk (still from a local path as setup last night):
import 'package:plummbur_kruk/kruk.dart';
import '../public/scripts/comics.dart' as Main;

group("[main]", (){
  // tests here ...
}
By way of setup for my tests, I create the comics-list UL element and populate the plummbur_kruk /widgets resource with a comic book:
    setUp((){
      el = document.body.append(new Element.html('<ul id=comics-list>'));

      var doc = '{"id":"42", "title": "Sandman", "author":"Neil Gaiman"}';
      return HttpRequest.
        request('${Kruk.SERVER_ROOT}/widgets', method: 'post', sendData: doc);
    });
Dang it. This is item #1 that plummbur_kruk still lacks: the ability to easily create /widgets. I am going to need a library interface for this so that I do not have to remember the HttpRequest call every time that I want to do this. Mental note made.

For test teardown, I remove the comics-list UL element and delete all records from the /widgets resource:
    setUp(){ /* ... */ });
    tearDown((){
      el.remove();

      return HttpRequest.
        request('${Kruk.SERVER_ROOT}/widgets/ALL', method: 'delete');
    });
Yup. That is item #2.

The test itself is pretty easy. I want to verify that, after the main() entry point is run, the comics-list contains a reference to the “Sandman” comic book that I created in /widgets:
    setUp(){ /* ... */ });
    tearDown((){ /* ... */ });
    test('populates the list', (){
      Main.main();
      expect(document.body.innerHtml, contains('Sandman'));
    });
(this is a bit of a lie because I also end up adding a slight delay before the expectation to prevent false-positive timing failures)

With that, I have my Dart for Hipsters test running against my “real fake” HTTP test server. But I still have two problems back in the original code: I do not want to include SERVER_ROOT because it unnecessarily complicates the discussion in the book and I want to point to the /comics resource.

The latter is already supported in plummbur_kruk. The Kruk.alias() static method will create an alias in the server:
    setUp((){
      el = document.body.append(new Element.html('<ul id=comics-list>'));

      var doc = '{"id":"42", "title": "Sandman", "author":"Neil Gaiman"}';
      return Future.wait([
        HttpRequest.
          request('${Kruk.SERVER_ROOT}/widgets', method: 'post', sendData: doc),
        Kruk.alias('/widgets', as: '/comics')
      ]);
    });
    tearDown((){ /* ... */ });
    test('populates the list', (){ /* ... */ });
The Future.wait() static methods waits for both futures—the /widgets population and the alias—to complete before it completes. And, since it is returned from setUp(), the subsequent test will wait until it has completed.

With that, I can rewrite the test to point against '${Kruk.SERVER_ROOT}/comics'. But that is only part of what I want to do. I also want to get rid of the need for the ${Kruk.SERVER_ROOT} in the code snippet. To accomplish that, I can play a trick in the test setup using BaseElement. The <base> element changes the URL root of all elements in a document. In this case, I use it to change the base URL for all URLs in my test:
    setUp((){
      document.head.append(new BaseElement()..href = Kruk.SERVER_ROOT);

      el = document.body.append(new Element.html('<ul id=comics-list>'));

      var doc = '{"id":"42", "title": "Sandman", "author":"Neil Gaiman"}';
      return Future.wait([
        HttpRequest.
          request('${Kruk.SERVER_ROOT}/widgets', method: 'post', sendData: doc),
        Kruk.alias('/widgets', as: '/comics')
      ]);
    });
    tearDown((){ /* ... */ });
    test('populates the list', (){ /* ... */ });
With that, my code snippet is exactly what I want:
main() {
  var list_el = document.query('#comics-list');
  var req = new HttpRequest();
  req.open('get', '/comics');
  req.onLoad.listen((req) {
    var list = JSON.parse(req.target.responseText);
    list_el.innerHtml = graphic_novels_template(list);
  });
  req.send();
}
Well, except for the method cascades, but those come later in the book. But otherwise, that is fantastic!

I have a couple of TODOs before plummbur_kruk is officially ready, but this is shaping up to be exactly what I need from a real fake test HTTP server.


Day #849

No comments:

Post a Comment