Saturday, November 17, 2012

Async Testing of Dart Writes

‹prev | My Chain | next›

Today, I continue my effort to create a dirt simple Dart datastore, affectionately known as dart-dirty. Yesterday, I was able to create the DB file. Today, I would like to be able to store records in it.

To really verify that writes are working, I will have to be able to read from the datastore. For now, I will simply try to test that writes increase the size of the database. To write, I first create a database, then set a value for a given key. The following test should do the trick:
    test("can write a record to the DB", () {
      var db = new Dirty('test/test.db');
      db.set('everything', {'answer': 42});
      expect(
        new File('test/test.db').lengthSync(),
        greaterThan(0)
      );
I set the value of the key 'everything' to be a hash. I will serialize it as JSON and store it in the DB. If I then check the size of the database file, it should be greater than zero. When I run the test, it fails because I lack a set method:
Caught NoSuchMethodError : method not found: 'set'
  Receiver: Instance of 'Dirty'
  Arguments: ["foo", Instance of 'LinkedHashMapImplementation<String, dynamic>']
...
I add a definition for set to the Dirty class:
class Dirty {
  // ...
  void set(String key, value) {
  }
  // ...
}
Now, the failure message has changed:
FAIL: writing can write a record to the DB
  Expected: a value greater than <0>
       but: was <0>.
Perfect! Now all that I have to do is write to the database file and my test should be passing.

I follow the node-dirty convention of setting a queue of keys to be written. I also stick with the maybe-flushing intermediate step to prevent multiple writes of the same keys:
#library('dart_dirty');

#import('dart:io');
#import('dart:json');

class Dirty {
  // ...
  void set(String key, value) {
    _keys.add(key);
    _docs[key] = value;
    _queue.add(key);
    _maybeFlush();
  }

  _maybeFlush() {
    if (flushing) return;
    _flush();
  }

  _flush() {
    flushing = true;

    _queue.forEach((key) {
      String doc = JSON.stringify({'key': key, 'val': _docs[key]});
      _writeStream.writeString("$doc\n");
    });

    flushing = false;
  }
}
Unfortunately, that does not work. When I run my tests, I am still told that the database file size is zero:
FAIL: writing can write a record to the DB
  Expected: a value greater than <0>
       but: was <0>.
It turns out that I am not giving the output stream a chance to write the JSON string before checking the file size. I need the file size expectation to get checked after the write stream is closed. To accomplish that, I will need two things: a database close() method and a way for the test to be sure that the output stream really is closed before checking. Dart does not have a blocking close() method for streams, so I am going to need to make use of the on-close callback that Dart streams do have.

First I add a close() method that accepts an optional callback:
class Dirty {
  // ...
  void close([cb]) {
    _writeStream.onClosed = cb;
    _writeStream.close();
  }
  // ...
}
If the stream's onClosed property is defined, it will invoke said callback when the stream is closed—that is, it will invoke the callback when the filesize has increased.

Testing this might be tricky if not for Dart's expectAsync0 testing method. Dart provides the expectAsync0 wrapper method for asynchronous methods that take zero arguments (there are also expectAsync1 and expectAsync2 wrappers). Dart does just what you might expect when this method is used—it waits until this method is invoked to test expectations.

So I wrap my file size expectation in expectAync0 and pass that as the callback to my new close() method:
    test("can write a record to the DB", () {
      var db = new Dirty('test/test.db');
      db.set('everything', {'answer': 42});
      db.close(expectAsync0(() {
        expect(
          new File('test/test.db').lengthSync(),
          greaterThan(0)
        );
      }));
With that, my test is passing:
➜  dart-dirty git:(master) ✗ dart test/dirty_test.dart
unittest-suite-wait-for-done
PASS: new DBs creates a new DB
PASS: writing can write a record to the DB

All 2 tests passed.
unittest-suite-success
Yay! I am writing to my local datastore and can verify it. I manually check the contents of my test database to verify that there is JSON in it so I look to be in good shape so far.

I will worry about reading the data back in tomorrow. For now, I finish by fixing yesterday's fixture clean-up code. I was able to remove databases from previous runs yesterday. But, due to Dart's asynchronous testing, I could not figure out how to remove tests after test runs or in between individual tests. A bit of digging turned up the setUp and tearDown methods:
test_write() {
  group("writing", () {

    setUp(removeFixtures);
    tearDown(removeFixtures);

    test("can write a record to the DB", () { /* ... */ });

  });
}

removeFixtures() {
  var db = new File('test/test.db');
  if (!db.existsSync()) return;
  db.deleteSync();
}
I should have known that something like this existed—I just needed to read the unittest documentation.

With better tests and working writes, I call it a day here. Tomorrow I will get started on the rather more difficult ability to read data from the the filesystem.


Day #572

No comments:

Post a Comment