Tuesday, July 9, 2013

Waiting for Multiple Futures


I have had myself an adventure testing HTTP in Dart. I consider my current approach doing it wrong™—starting an actual HTTP server against which my HttpRequest tests can run. I would much prefer to stub HTTP requests the way that Sinon.js does. Still, it has proven interesting to explore what is possible and has provided useful tests.

Before moving on to newer ground, there is one more REST-like request that I would like to test: collection retrieval. Thanks to my work over the last few nights, I have tests for retrieving records (GET /widget/:id), creating new records (POST /widgets), updating existing records (PUT /widgets/:id), and deleting records (DELETE /widgets/:id). Tonight, I would like to create multiple records and verify that my Hipster MVC library can successfully retrieve the list.

I have really gotten to love unittest's async support in Dart. The feature that I find myself using repeatedly is returning a Dart Future from a test group's set-up block. Futures, also known as promises in other languages, are a way to encapsulate an action that will finish at some point in the future. In test set-up blocks, a future signals that the set up is dependent on some action that will not complete for some time—an action like an HTTP request to create records:
    group('(w/ multiple pre-existing records)', (){
      setUp((){
        model = new FakeModel()
          ..url = 'http://localhost:31337/widgets'
          ..attributes = {'test': 1};

        return HipsterSync.call('create', model).
      });
    });
In this setup block, I return the result of HipsterSync.call(), which is a Future. HipsterSync is responsible for storing MVC data like that in my fake model. By default, it will store data in a REST-like data store pointed to by the model's url property. Since this is done over HTTP, there is async built into the operation, which is why HipsterSync.call() returns a Future. I then exploit this so that my setup will block subsequent tests from running until the model is successfully created in my test server.

I already know that this setup works (I copied it from working tests), but it is not quite what I want to do. It only creates a single record in the REST-like data store. As the group description indicates, I want to create multiple records. But how do I return the combination of two Futures from my set up block?

The answer to that is Future.wait(), which waits until all futures in an iterable complete before itself completing:
    group('(w/ multiple pre-existing records)', (){
      setUp((){
        var model1 = new FakeModel()
          ..url = 'http://localhost:31337/widgets'
          ..attributes = {'test': 1};

        var model2 = new FakeModel()
          ..url = 'http://localhost:31337/widgets'
          ..attributes = {'test': 2};

        return Future.wait([
          HipsterSync.call('create', model1),
          HipsterSync.call('create', model2)
        ]);
      });
    });
That setup will poll until both models are created over REST. With that, I can write a collection GET test:
    group('(w/ multiple pre-existing records)', (){
      setUp((){ /* ... */ });
      test("can retrieve a collection of records", (){
        var collection = new FakeModel()
          ..url = 'http://localhost:31337/widgets';

        HipsterSync.call('read', collection).
          then(
            expectAsync1((response) {
              expect(response.length, 2);
            })
          );
      });
    });
Just because this is an actual test instead of set up does not change the asynchronous nature of HipsterSync.call(). Since it is part of the test, I need to wrap the expectation in unittest's expectAsync1(), which is a test's way to poll until the asynchronous wait is over.

When I run that test, however, I find that I do not have 2 items in my collection. Instead, I have 88!
FAIL: Hipster Sync (w/ multiple pre-existing records) can retrieve a collection of records
  Expected: <2>
    Actual: <88>
This is because I have not been clearing my REST backend. I update my the delete handler in test_server.dart to clear the DB when DELETEing “ALL”:
deleteWidget(id, req) {
  if (id == 'ALL') { db.clear(); }
  else { /* ... */ }

  HttpResponse res = req.response;
  res.statusCode = HttpStatus.NO_CONTENT;
  res.close();
}
To use that, I create a teardown to DELETE all records:
    group('(w/ multiple pre-existing records)', (){
      setUp((){ /* ... */ });
      tearDown(() {
        var model = new FakeModel()
          ..url = 'http://localhost:31337/widgets/ALL';
        return HipsterSync.call('delete', model);
      });
      test("can retrieve a collection of records", (){ /* ... */ });
    });
With that, I have my REST-like, fully functional tests for Hipster MVC passing:
CONSOLE MESSAGE: PASS: Hipster Sync can parse regular JSON
CONSOLE MESSAGE: PASS: Hipster Sync can parse empty responses
CONSOLE MESSAGE: PASS: Hipster Sync HTTP get it can parse responses
CONSOLE MESSAGE: PASS: Hipster Sync HTTP post it can POST new records
CONSOLE MESSAGE: PASS: Hipster Sync (w/ a pre-existing record) HTTP PUT: can update existing records
CONSOLE MESSAGE: PASS: Hipster Sync (w/ a pre-existing record) HTTP DELETE: can remove the record from the store
CONSOLE MESSAGE: PASS: Hipster Sync (w/ multiple pre-existing records) can retrieve a collection of records
CONSOLE MESSAGE: 
CONSOLE MESSAGE: All 7 tests passed.
This remain REST-like, not RESTful (I think I am short 164 tests or so for that). Also, it is full stack by every measure as it requires an actual server. Still, it may be “good enough” for my purposes. I have strong tests over the core of the data persistence layer in Hipster MVC. I could very easily write tests for the remainder of my library by swapping out the persistence layer for something more testable and still be secure in the knowledge that the core works.

And yet, I still want very badly to sub HttpRequests. I remain unconvinced that it is possible in Dart, but I will give it a shot. Tomorrow.

Day #807

No comments:

Post a Comment