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