Tuesday, June 4, 2013

Dart Unit Tests

‹prev | My Chain | next›

I have been doing a lot of Dart behavior driven development to drive the features in ICE Code Editor. Amazingly, I have not been using unit tests for this—rather I have gotten away with only full-stack integration tests. That may need to change tonight.

Or maybe not…

I have the actual auto-saving working—I have verified that is works in the UI just fine. My problem is the auto-save test fails—even if I mark it as async:
    solo_test("is on by default", (){
      helpers.createProject('Test Project');
      editor.content = '<h1>test</h1>';

      _test(_) {
        expect(
          editor.store['Test Project']['code'],
          equals('<h1>test</h1>')
        );
      }

      editor.editorReady.then(expectAsync1(_test));
    });
I spend a lot of time figuring this one out. I add a lot of exploratory and debugging code to figure this one out. In then end, the problem is that I should wait to create the project and set the content until the editor is ready. That is, I need to put this code in the async _test() function:
    test("is on by default", (){
      _test(_) {
        helpers.createProject('Test Project');
        editor.content = '<h1>test</h1>';

        expect(
          editor.store['Test Project']['code'],
          equals('<h1>test</h1>')
        );
      }

      editor.editorReady.then(expectAsync1(_test));
    });
I hate it when I spend that much time and the solution turns out to be that easy. What I hate even more is that all of the code that I wrote to try to get this working is now useless. And by useless, I mean that I did not drive it because I wanted or needed it, but rather because I thought it might help. It is never a good idea to keep code like this around—it is too much like programming by coincidence.

So I remove the Store class feature that let me specify different localStorage keys for the datastore.

I am left yet again with nothing but integration tests for my application. But... there was one feature that I added during my exploration that I had planned on adding anyway. It was an onSync stream that saw an event whenever the localStorage data was synced with memory. I have no need for that immediately, but would like to support it as part of the API. Perhaps that can be driven (or rather re-driven) as a unit test.

I already have the feature working, so this will not be driving the feature with tests, but verifying it after the fact. Still, I can ensure that the tests are worthwhile by doing what I did with the auto-save: get the test passing and then try to break the test by removing parts of the feature. I ought to be left with valuable tests and a (still) working feature.

The code that implements the onSync feature looks like:
class Store implements HashMap<String, HashMap> {
  // ...
  void _sync() {
    window.localStorage[codeEditor] = JSON.stringify(projects);
    _syncController.add(true);
  }

  Stream get onSync => _syncController.stream.asBroadcastStream();
  StreamController __syncController;
  StreamController get _syncController {
    if (__syncController != null) return  __syncController;
    return __syncController = new StreamController();
  }
}
The actual onSync property of my Store class is itself the property of a StreamController. As the named suggests, stream controllers are and easy way to get a stream that can be manipulated. It should only be possible to do things to the onSync stream from within the class, which is why the _syncController is declared as private (Dart enforces variables with leading underscores as private).

In the _sync() method that is actually responsible for synchronizing the localStorage with current memory, I perform the actual stream manipulation. In this case, I just add a boolean—the event contents are unimportant (at least for now). To test, I should be able to instantiate a Store object, listen to the onSync stream and try to trigger the sync event.

In unittest-ese, that should look something like:
  group("onSync", (){
    test("it generates a stream action when a sync operation occurs", (){
      var store = new Store()..clear();

      _test(_)=> expect(store.isEmpty, isFalse);

      store.onSync.listen(expectAsync1(_test));

      store['Test Project'] = {'code': 'Test Code'};
    });
  });
I create a new Store instance and clear it—just to be absolutely certain that no data from a previous test run might be lying around. I have a simple _test() function that verifies that the data store is not empty. I need that in a function because, by the very nature of streams, we are looking at asynchronous code. Thus, I need to wrap the _test() function that actually performs the test inside an expectAsync1() (expect, and wait for, an asynchronous call with one argument). Finally, I perform an operation that should trigger the event: setting a new 'Test Project' in the store.

With that, I get a nice, juicy failure:
FAIL: onSync it generates a stream action when a sync operation occurs
  Callback called more times than expected (1).
  ...
Sigh. The clear() that I perform is generating a second sync operation. To get this to pass, I need to tell expectAsync1() that it is OK for two events to happen on the stream:
  group("onSync", (){
    test("it generates a stream action when a sync operation occurs", (){
      var store = new Store()..clear();

      _test(_)=> expect(store.isEmpty, isFalse);

      // Once for clear, once for new Test Project
      store.onSync.listen(expectAsync1(_test, count: 2));

      store['Test Project'] = {'code': 'Test Code'};
    });
  });
With that, I have a passing test. If I intentionally break the application code—by commenting out the add() to the stream controller in _sync(), the I again have a failing test. So it seems like I have a decent feature and unit test to go along with it.

Since the Store class extends HashMap, I could have used the isEmpty test matcher. But, since I am verifying that the data store is not empty, I would need to write the test as:
      _test(_)=> expect(store, isNot(isEmpty));
I am not a fan of the isNot() matcher—it reads awkwardly—especially with another “is” matcher like isEmpty.

Last, but not least, I write some documentation for the onSync property. The only reason that it exists currently is so that other developers might use it, so I'd damn well better document it:
  /// Stream that will see events whenever data is synchronized with
  /// localStorage (create, update, delete).
  Stream get onSync => _syncController.stream.asBroadcastStream();
The triple slashes tell dartdoc that I mean for this to generate documentation:



That seems a fine stopping point for tonight. I have more integration tests that exercise the UI as well as another low-level unit test. Hopefully I can finish off the features needed for beta tomorrow or the next day.


Day #772

No comments:

Post a Comment