Thursday, August 1, 2013

Testing Errors in Scheduled Test


After last night, I remain unsure of how best to run Dart tests written with the Scheduled Test package. Before attempting to tackle that, I think that I need a few more tests. So far, I have just one test and, without more, I am just speculating on how best to make this work.

I start by adding a few more tests against the /widgets resource in my test server. This is for a Dart library that can run a test server against which browser tests can run. I am extracting this from Hipster MVC, so the functionality is already in place—I just need tests.

With only a few bumps, this goes fairly well. For example, creating a bunch of records and reading the URL root for the current resource can be represented as a series of schedules that create 3 resources, request the URL root, and probe the expectation that there be 3 resources in the REST-like interface:
    test("can GET the list of records", (){
      schedule(()=> post(URL_ROOT, '{"test": 1}'));
      schedule(()=> post(URL_ROOT, '{"test": 2}'));
      schedule(()=> post(URL_ROOT, '{"test": 3}'));
      var response = schedule(()=> get(URL_ROOT));

      schedule(() {
        response.then((json) {
          var list = JSON.parse(json);
          expect(list.length, 3);
        });
      });
    });
To aid in readability, I have defined post() and get() helpers:
Future<String> get(url)=> HttpRequest.getString(url);

Future<HttpRequest> post(url, data) {
  return HttpRequest.request(url, method: 'post', sendData: data);
}
I do the same for delete() and put(). I rather wish that Dart would do this for me—it seems something that I will want (and have to do myself) quite often. Regardless, the helpers are fairly easy. I work through GET, POST, and PUT:
PASS: /widgets - can POST new records
PASS: /widgets - can GET the list of records
PASS: /widgets/:id - can GET existing records
PASS: /widgets/:id - can PUT updates to existing records
And then I hit DELETE.

I could probably come up with a better way to test this, but I choose to schedule a delete, then a schedule to request the missing resource, and finally a schedule to check my expectation that the response is a 404:
    test("can DELETE existing records", (){
      schedule(()=> delete('$URL_ROOT/$id'));

      var response = schedule((){
        return HttpRequest.request('$URL_ROOT/$id');
      });

      schedule(() {
        response.then((req) {
          expect(req.status, 404);
        });
      });
    });
This is very similar to the test above—the only real differences are the delete() and then checking for a client error. And yet, it fails wildly:
FAIL: /widgets/:id - can DELETE existing records
  Caught ScheduleError: "Instance of 'HttpRequestProgressEvent'"
  Stack trace:
  | dart:async                                                                                                                             _createTimer.<fn>
  | ../../../../../../mnt/data/b/build/slave/dartium-lucid64-full-trunk/build/src/dart/tools/dom/src/native_DOMImplementation.dart 167:17  _Timer._Timer.<fn>
  
  Error detected during task in queue "tasks":
  * #0
  * #1
  > #2
  | 
  | Stack trace:
  | | package:scheduled_test/scheduled_test.dart 288:33                                                                                      schedule
  | | ../browser_test.dart 90:30                                                                                                             main.<fn>.<fn>
  | | dart:async                                                                                                                             _createTimer.<fn>
  | | ../../../../../../mnt/data/b/build/slave/dartium-lucid64-full-trunk/build/src/dart/tools/dom/src/native_DOMImplementation.dart 167:17  _Timer._Timer.<fn>
  * #3
There is a lot of information in there, but the only truly important bit was that an exception was thrown in the form of an HttpRequestProgressEvent.

It takes me a bit to figure this out, but I finally address this by catching the error in the second schedule:
    test("can DELETE existing records", (){
      schedule(()=> delete('$URL_ROOT/$id'));
      var response = schedule((){
        return HttpRequest.request('$URL_ROOT/$id')
          .then((_) { fail('Successful request to non-existent resource?!'); })
          .catchError((e)=> e.target);
      });
      // Check expectation down here...
    });
  });
The HttpRequest.request() static method returns a future that would normally complete with an instance of HttpRequest for additional processing (e.g. checking the response status, parsing the response test, etc). In this case, I do not expect a normal completion, so I add an explicit fail() for the “happy path” that should never be reached. Then, in catchError(), I return the event error's target—which is the HttpRequest instance.. The catchError() method returns a future that completes with the return value—the HttpRequest instance.

With that, I can finish my test my scheduling the expectation to be processed once the error has been caught and completed:
    test("can DELETE existing records", (){
      schedule(()=> delete('$URL_ROOT/$id'));
      var response = schedule((){
        return HttpRequest.request('$URL_ROOT/$id')
          .then((_) { fail('Successful request to non-existent resource?!'); })
          .catchError((e)=> e.target);
      });

      schedule(() {
        response.then((res) {
          expect(res.status, 404);
        });
      });
    });
For me at least, this was deceptively hard to track down. The hardest part was realizing the implication of an instance of an event—HttpRequestProgressEvent—being thrown. The implication is that the target property on that instance contains the HttpRequest that I needed to check things. It also took me a bit to realize that catchError() returns a future that completes with the value returned by the supplied callback.

But once I understood, the resulting test is rather elegant. The three schedules that I originally intended are in place. I even have a failsafe for a unexpected successful request. I am still unsure if using Scheduled Test in browser tests like this makes much sense, but the results are fairly pleasant so far.


Day #830

No comments:

Post a Comment