Tuesday, November 20, 2012

Deep Async Testing in Dart

‹prev | My Chain | next›

Today I hope to complete the conversion of my dart-dirty file-based datastore to a HashMap.

I left off yesterday with the inability to define various getters (length, isEmpty, etc) with the new, non-parenthesis syntax. In other words, the only way that I could define those methods was with empty parentheses:
class Dirty {
  // ...
  int length() => _docs.length;
  bool isEmpty() => _docs.isEmpty;
  // ...
}
At the time, I thought this was due to an older Dart SDK. In the cold light of a new day, it is a simple matter of not including the get keyword:
class Dirty {
  // ...
  int get length => _docs.length;
  bool get isEmpty => _docs.isEmpty;
  // ...
}
Regardless, I am using an older version of the SDK which is still using the old library/import syntax. After installing the latest, I have to convert from the old syntax:
#library('dart_dirty');

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

class Dirty {
  // ...
}
Instead, I use the new syntax, which has markedly fewer hashes and parentheses:
library dart_dirty;

import 'dart:io';
import 'dart:json';

class Dirty {
  // ...
}
It feels weird typing anything in Dart that does not include parentheses. I suppose that was part of the motivation behind the change—to make library directives that much more distinct from Dart-proper. It still reminds me a bit too much of Python, but what are you going to do?

Anyhow, after updating my unit test file, my library, and re-running pub (no changes were need in the packaging, but the layout appears to have changed), I am ready to finish off the HashMap implementation:
➜  dart-dirty git:(master) ✗ dart_analyzer lib/dirty.dart
file:/home/chris/repos/dart-dirty/lib/dirty.dart:6: Concrete class Dirty has unimplemented member(s) 
    # From Map:
        Object putIfAbsent(String, () -> Object)
        Object remove(String)
        void clear()
     5: 
     6: class Dirty implements HashMap<String, Object> {
              ~~~~~
I start with the remove() method since I think that will be the most difficult. Actually, in-memory remove is easy, I can verify it with the following test:
    test("can remove a record from the DB", () {
      var db = new Dirty('test/test.db');

      db['everything'] =  {'answer': 42};
      db.remove('everything');

      expect(
        db['everything'],
        isNull
      );
    });
And make that test pass by simply removing the record from the private _docs instance variable:
class Dirty {
  // ...
  Object remove(String k) {
    return  _docs.remove(k);
  }
 // ...
}
The tricky part will be removing that from the filesystem. That is not quite true, I would guess that it would not be difficult to remove the record from the filesystem store, but it probably will not be easy to write a test for it.

In the test, I need to:
  1. create a record, then close the DB to save to the filesystem
  2. Re-open the DB so that I can delete the record, then close the DB to dump to the filesystem store again
  3. Re-open the DB and check my expectation that the record is gone
This would all be fairly easy if any of it were blocking, but every step of the way is asynchronous. Fortunately, I am not completely out of luck, thanks to Dart's unittest library. In particular, the expectAsync0 wrapper is my friend.

I set up three functions inside my test case. Those three function will correspond to the three step above. The trick is that the first function will include the expectation that, when it closes/saves the database, then the second function that removes the key from the data store will be called. Similarly, when I close/save the local datastore after removing the record, I expect that the callback supplied to the close() method will in turn call the actual test, which verifies that the key is gone.

It is all a bit confusing (and I should really convert some of this callback madness to Dart Futures), but I think the test actually reads fairly well:
    test("can remove a record from the filesystem store", () {
      expectKeyIsGone() {
        var db = new Dirty('test/test.db');
        expect(
          db['everything'],
          isNull
        );
      }

      removeKey() {
        var db = new Dirty('test/test.db');
        db.remove('everything');
        db.close(expectAsync0(
          expectKeyIsGone
        ));
      }

      addKey() {
        var db = new Dirty('test/test.db');
        db['everything'] = {'answer': 42};
        db.close(expectAsync0(
          removeKey
        ));
      }

      addKey();
    });
That seems to do the trick as I get the expected failure. Although the record is removed from the in-memory store, it is not removed from the filesystem store:
➜  dart-dirty git:(master) ✗ dart test/dirty_test.dart
unittest-suite-wait-for-done
FAIL: removing can remove a record from the filesystem store
  Expected: null
       but: was <{answer: 42}>.
And, as I guessed, it is quite easy to get that passing. In addition to removing the record from the _docs private instance variable, I add the key being removed to the queue of records to be written to the filesystem:
class Dirty {
  // ...
  Object remove(String key) {
    var object = _docs.remove(key);
    _queue.add(key);
    _maybeFlush();
    return object;
  }
  // ...
}
Since the key is no longer in _docs, when _maybeFlush() does flush to the filesystem, it flushes a null and the record will be deleted when the datastore is re-read.

With that, my increasing test suite 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
PASS: reading can read a record from the DB
PASS: reading can read a record from the DB stored on the filesystem
PASS: removing can remove a record from the DB
PASS: removing can remove keys from the DB
PASS: removing can remove a record from the filesystem store

All 7 tests passed.
I call it a night here. Up tomorrow, I need to convert those close() callbacks to Dart Futures.


Day #575

No comments:

Post a Comment