Wednesday, August 21, 2013

Client Server Scheduled Tests in Dart


When I actually used my “real fake” HTTP test server (code-named plummbur_kruk), I found it wanting. Nothing too horrible, but it does need a few higher-level API methods. Since this is a server for running tests, I will, of course, write tests to verify the new features.

The new features include: (1) a way to create individual records, (2) a way to delete records, and (3) a guard to ensure that all data is removed when the test server restarts. All of this is possible already, but not easy. #1 and #2 are possible via HttpRequest calls. #3 is accomplished by simply removing the noSQL file.

Since all of this is done in Dart, I can immediately try out the results in code that is using my library—before I have even published it to Dart Pub. Dart rocks.

The test for #1, creating records in the test DB, is:
    test("can create records", (){
      schedule(()=> Kruk.create('{"name": "Sandman"}'));

      var ready = schedule(()=> get('${Kruk.SERVER_ROOT}/comics'));
      schedule(() {
        ready.then((json) {
          var list = JSON.parse(json);
          expect(list.first['name'], 'Sandman');
        });
      });
    });
I am using the scheduled_test library here, which makes all asynchronous steps run in serial. In other words, it makes it much easier to keep track of what runs and when. In this case, I schedule the Kruk.create() call to create a JSON record, then I schedule a GET from the server, and finally, I schedule the response from the server (the server is started by the same Bash script that runs the tests).

This fails because there is no static create() method in Kruk:
CONSOLE MESSAGE: FAIL: The Mighty Kruk can create records
  Caught ScheduleError:
  | No static method 'create' declared in class 'Kruk'.
  |
  | NoSuchMethodError : method not found: 'create'
  | Receiver: Type: class 'Kruk'
  | Arguments: [...]

  Stack trace:
  | ../kruk_test.dart 23:26 
I make this pass by defining the create() method:
class Kruk {
  static String SERVER_ROOT = 'http://localhost:31337';
  // ...
  static Future<HttpRequest> create(String json) {
    return HttpRequest.request(
      '${SERVER_ROOT}/widgets',
      method: 'post',
      sendData: json
    );
  }
}
Just like that, I have a nice Kruk.create() method to create records in my real fake server.

The test and implementation for Kruk.deleteAll() is nearly identical to Kruk.create(). The server supports this operation via a DELETE request to /widgets/ALL. I could have made this more generic to support deletes of individual records, but I do not want to support this unless I know that it will be useful.

Testing that the backing database is wiped in between restarts is a bit trickier. The methods in the Kruk class are all meant to be run in the browser. Code in the browser cannot access the file system, so I am relegated to placing the wiped test file into "dart:io" tests. Happily, I already have some of those, so I can just add this new test in there:
  group("Restarting", (){
    test("removes file DB", (){
      var server;
      schedule(() {
        Server.main().then((s) { server = s; });
      });

      var responseReady = schedule(() {
        return new HttpClient().
          postUrl(Uri.parse("http://localhost:31337/widgets")).
          then((request) {
            request.write('{"name": "Sandman"}');
            return request.close();
          });
      });

      schedule(() { server.close(); });

      schedule(() {
        return Server.main().then((s) { server = s; });
      });

      schedule(() {
        expect(new File('test.db').existsSync(), isFalse);
      });

      schedule(() { server.close(); });
    });
  });
I am also using scheduled test in here. I have to create a number of schedules to start the server, write some data to ensure that the test database will exist, close the server, start up a new instance, and finally check that the database file has been removed.

Getting this to pass is simpler: I tell the server to removeDb() when it closes:
main() {
  return HttpServer.bind('127.0.0.1', 13317)..then((app) {
    app.listen((HttpRequest req) { /* ... */ }, onDone: removeDb);
  });
}

removeDb() {
  var db_file = new File('test.db');
  if (!db_file.existsSync()) return;
  db_file.deleteSync();
}
With that, I have the tests passing for the features that I hoped to add. This seems a good stopping point for tonight. Unless I have a problem integrating these features back into the codebase that needs them, I will move on to other topics tomorrow.



Day #850

No comments:

Post a Comment