Google Groups no longer supports new Usenet posts or subscriptions. Historical content remains viewable.
Dismiss

Simplify async code-flows and unify unit tests

68 views
Skip to first unread message

Mike de Boer

unread,
May 7, 2013, 9:49:49 AM5/7/13
to Mozilla dev-platform mailing list mailing list
TLDR; in bug 867742 I requested to adopt two JS modules, Async.jsm and AsyncTest.jsm, in mozilla-central/toolkit/modules. The whole story can be read below as well as at https://gist.github.com/mikedeboer/5495405. I posted about this subject before on firefox-dev: https://mail.mozilla.org/pipermail/firefox-dev/2013-April/thread.html#268

...
The thing I always do when I encounter a large code base, is roam though it and make a mental map of the structure; the good parts and ones that would benefit from some TLC. A part I noticed did not yet receive the amount of attention it deserves are unit tests and asynchronous code handling. I'll be blunt about it, here's what I did: I ported the async NodeJS library to a Mozilla-style module and on top of that I built a unit testing library, inspired by Mocha (among others). I can imagine that the fellas I just mentioned don't make you recall anything, so here are two links:

https://github.com/caolan/async
https://github.com/visionmedia/mocha
First of all, the Github numbers will show you that they are crazy popular NodeJS libraries. And well-documented. And well-written. And awesome.

Here's the thing I ended up with: https://github.com/mikedeboer/mozAsync The lib folder therein contains two files:

Async.jsm - the port of the async library
AsyncTest.jsm - the unit testing library The test folder contains the unit tests (erm, duh) and also serve as great examples of how to use the two modules!
Ok, example time! I really like this one from the async module:

// Sorts a list by the results of running each value through an async iterator.
Async.sortBy(["file1", "file2", "file3"], function(file, callback){
fs.stat(file, function(err, stats){
callback(err, stats.mtime);
});
}, function(err, results){
// results is now the original array of files sorted by
// modified date
});
Note: this one uses a NodeJS API, but you can mentally replace that with an async MozFile one.

I've told some of you before that I'm not a big fan of Promise libraries (if not, please read the 10 reasons to NOT use a Promise library: https://gist.github.com/mikedeboer/5305020). However, this library is not meant to replace them, but to augment the ecosystem with another option. There are many developers out there that feel uneasy about using Promises as long as they're not a first-class primitive in SpiderMonkey. If that's you, you can use this: a minimalistic, functional utility library.

What about them unit tests?

Design goal: create a simple, minimalistic framework that provides a clear, unified API to create unit tests that is as easy to use for sync code flows as for async ones. Rationale: unit tests are 'hot' code. They are modified as often - perhaps even more - as functionality it covers changes, especially in TDD environments. They serve as documentation for the code they cover. When tests fail, they usually don't on 'your computer', but somewhere else, like on the build infrastructure. When that happens, someone will open the unit test and try to understand what is going on. In all these cases it is hugely beneficial to a) know that most of the unit tests are written in the same structure and b) are structured in such a way that they're easy to read by someone other than you.

This is an example of a minimal test:

AsyncTest([
{
name: "Minimal Test",
reporter: "tap",
tests: {
"it should execute this test": function(next) {
Assert.equal(typeof next, "function", "'next' should be a callback function");
next();
},

"! it should NOT execute this test": function(next) {
Assert.ok(false, "BAM!");
next();
},

"it should be aware of the correct context": function() {
Assert.ok(this["it should execute this test"], "The function ought to be accessible");
}
}
}
]);
There are a couple of interesting things going on here:

You can pass an Array of test suites or just one Object
Suites can have names
Tests can be described freely
It mixes fine with ANY assertion style (Mochi or XPCShell)
Prefix a test with ! to exclude it from the suite. This is something that is practically useful when doing TDD.
Prefix a test with > to only run that test and ignore the others. It is meant to signal out a test case and run only that specific one. This is something that is practically useful when doing TDD.
If the test is sync, you can forget about the next function (callback) completely - the library takes care of it; you can even mix async and non-async tests
You can choose the style of reporting by setting the reporter property to dot or tap. The reporters progress and spec are under development. More information about what 'TAP' is can be found at: http://testanything.org
But more importantly, there are several things that usually need to happen before a test can be run, like opening a tab, load a page and wait for it to load, etc. AsyncTest unifies scenarios like this in the following way:

Each suite may have one or more of the following functions:
setUpSuite - run only once before any test is executed
setUp - run once before each test
tearDownSuite - run only once when all tests are done
tearDown - run once after each test
These functions are executed in the context of the tests, so the this is the same as the this in test functions
This takes care of all the flows a test suite might need to implement.

For another example, please take a look at the following two files:

https://github.com/mikedeboer/mozAsync/blob/master/examples/browser_aboutHome.orig.js
https://github.com/mikedeboer/mozAsync/blob/master/examples/browser_aboutHome.js
This is the browser_aboutHome.js unit test code vs. a rewritten version with AsyncTest.

The NLOC difference is minimal (~100 lines), but the more significant improvement here is the structure that is proposed as uniform.

To top this off, you can set a flag in each suite: notify: true to present you with a Growl notification once the suite is done to report the results!

That shows that this library is meant to bring back the fun into creating unit tests.


Mike de Boer.

Tetsuharu OHZEKI

unread,
May 7, 2013, 11:10:13 AM5/7/13
to Mike de Boer, Mozilla dev-platform mailing list mailing list
For only making tests codes (mochitest), I think it's good that we
introduce the way to write async test uniformly.

However, I seem we need consider about it for some reason.

1. We have already task.jsm modules in our source tree. It's promises
based design and it has been used in our code. the difference of
styles will causes some problem and it's not good for project. Even if
we need to mix some different paradigms for async code, we separate a
paradigm per each modules.

2. We may need to consider DOM Future
http://dom.spec.whatwg.org/#futures and the discussions on es-discuss
about introducing Future pattern to ECMAScript standards. This is the
tide which we cannot ignore about. If ECMAScript or DOM Standard adopt
them as formal standards, its design pattern will be main stream and
following it will be benefits.

2013/5/7 Mike de Boer <mde...@mozilla.com>:
> _______________________________________________
> dev-platform mailing list
> dev-pl...@lists.mozilla.org
> https://lists.mozilla.org/listinfo/dev-platform
>



--
saneyuki_s
saneyuk...@gmail.com

Mark Hammond

unread,
May 7, 2013, 10:04:23 PM5/7/13
to Mike de Boer
On 7/05/2013 11:49 PM, Mike de Boer wrote:
> TLDR; in bug 867742 I requested to adopt two JS modules, Async.jsm
> and AsyncTest.jsm, in mozilla-central/toolkit/modules. The whole
> story can be read below as well as at
> https://gist.github.com/mikedeboer/5495405. I posted about this
> subject before on firefox-dev:
> https://mail.mozilla.org/pipermail/firefox-dev/2013-April/thread.html#268

I don't see the advantage to this, even after re-reading that thread.
It seems like the biggest advantage is that it addresses *your* aversion
to using promises while ignoring the fact that promises and Task.jsm are
being used fairly extensively in our tests (and more and more in the
production code). To my mind, this alone makes your claim:

| most of the unit tests are written in the same structure and b) are
| structured in such a way that they're easy to read by someone other
| than you.

somewhat suspect - unless you are also proposing to remove promises from
the existing tests? Combine this with the non-obvious features such as
magic prefixes for the test descriptions and I'm concerned this would
make the cognitive overhead of understanding our tests worse rather than
better.

While I appreciate the sentiment and would always encourage discussions
which aim to make things better, I see more potential downsides than
upsides in this proposal.

Just-my-2c ly,

Mark

Joshua Cranmer 🐧

unread,
May 8, 2013, 12:17:21 AM5/8/13
to
On 5/7/2013 8:49 AM, Mike de Boer wrote:
> I've told some of you before that I'm not a big fan of Promise libraries (if not, please read the 10 reasons to NOT use a Promise library: https://gist.github.com/mikedeboer/5305020).

For what it's worth, I am very unpersuaded by your argumentation.
Rebutting your reasons:
1. Promises aren't meant to solve the async problem. In effect, they
take code which would be written in a continuation passing style and
rewrite that into a more natural serial chaining.
2. I'll grant you this, but this is a casualty of CPS code in general.
3. I don't understand your complaint here, and this may be because I'm
trying to compare promises with similar use cases and you are not.
4. This is no more a problem than CPS code in general has. Actually,
promises helps this by being able to propagate an exception to the last
rejection handler. In effect, it brings back the notion that a careful
API consumer can fix the problem of a careless API author.
5. This is true for general code on the web. This is not true for
Mozilla code, where we have a single promises implementation.
6. Ditto. This also strikes me as "I didn't write this, therefore it
sucks" syndrome.
7. It has been my experience that the environment of testing is often
such a diverse field that everything breaks down into ad-hoc algorithms.
Look through all of the xpcshell tests in comm-central to see what I mean.
8. Again, this strikes me as "I didn't write this, therefore it sucks."
See also below.
9. All API tends to be inherently viral. It's hard to mix sync and async
code, for example. At some level, most code is basically a glorified way
to move data from one API to another.
10. Paraphrasing, this is "Promises are bad because they force you to
write the promises model" (with an implicit "and that model is bad").
It's hard not to take this as "Promises are bad because I hate promises"

It's also interesting to note that your points 5, 6, and 8 are slippery
slope arguments. Change a few words:

1. C++ implementations differ a LOT from each other, most often in the
area of semantics. This makes it so that you may know one
implementation, but can not use another.
2. The most-often used C++ implementations are not easy to contribute
to. It isn't bug free (no software is). If you encounter one, the
code is complex enough, so you'd have to wait until a more active or
seasoned developer may be able to fix it.
3. in general - and more towards an opinion: you shouldn't use a helper
library that you wouldn't be able to write yourself upon first
glance. It might help you at first, but not in the long run.

Therefore, we should write all of our code in native machine code
because we can't rely on C++ implementations, right? (There is a very
long history of having to work around buggy compilers in our code, and
probably many more problems have been uncovered in trying to build
portable C/C++ code than you have seen between all promise libraries).


> You can pass an Array of test suites or just one Object
> Suites can have names
> Tests can be described freely
> It mixes fine with ANY assertion style (Mochi or XPCShell)
> Prefix a test with ! to exclude it from the suite. This is something that is practically useful when doing TDD.
> Prefix a test with > to only run that test and ignore the others. It is meant to signal out a test case and run only that specific one. This is something that is practically useful when doing TDD.
> If the test is sync, you can forget about the next function (callback) completely - the library takes care of it; you can even mix async and non-async tests

Forgive me for saying this, but it sounds like using this test suite is
a lot more painful for not using it. The tests that I need to write tend
to come in two categories. Either they're a series of functions which
run for an asynchronous amount of time, which yields tests like
<http://mxr.mozilla.org/comm-central/source/mailnews/news/test/unit/test_internalUris.js>,
or they are running essentially the same code on a large body of test
inputs, like
<http://mxr.mozilla.org/comm-central/source/mailnews/mime/test/unit/test_parser.js>
(large is relative--the rest of those tests are sitting in patches
awaiting review). The former uses a common test framework for
comm-central
(<http://mxr.mozilla.org/comm-central/source/mailnews/test/resources/asyncTestUtils.js>),
while the latter is an example of one my many ad-hoc test frameworks.

--
Joshua Cranmer
Thunderbird and DXR developer
Source code archæologist

sam foster

unread,
May 8, 2013, 9:11:56 AM5/8/13
to
I want to add my +1 to the goal of unifying and streamlining the setting up of test flows and a common assertion syntax.

I know some of the issues you raise with Promises (like getting a useful stack on exceptions) are being discussed and addressed already. I dont have all the context, but ISTM the task/promise boat has already sailed, so step 1 in the unification effort is ferretting out and fixing issues created or not-solved by this approach before adoption is truly widespread.

In the past I've seen real benefit from having some kind of Test or Fixture class/prototype, as well as for a group or suite of tests. It lets you easily share boilerplate for similar tests and gives you a place to hang state and other products of the steps in a test, where they can be tracked and cleaned up during teardown.

In the (mochi)tests we'e been writing in browser/metro, we've found need for a few things to facilitate test writing and useful running and reporting behavior, including:

* async setUp, i.e. setUp is itself a Task
* various helpers for events, observers and other asynchrony tests need to navigate
* method spies/stubs
* try/catch hackery to allow a suite to complete and report all errors, rather than falling at the first hurdle

The guts of the test runner-runner are here: https://mxr.mozilla.org/mozilla-central/source/browser/metro/base/tests/mochitest/head.js#668 .. I'm sure each module and project has some unique requirements but a lot of this stuff feels strongly like it should already exist. Count me in on any efforts to fill this gap.

/Sam

On Tuesday, May 7, 2013 2:49:49 PM UTC+1, Mike de Boer wrote:
> TLDR; in bug 867742 I requested to adopt two JS modules, Async.jsm and AsyncTest.jsm, in mozilla-central/toolkit/modules. The whole story can be read below as well as at https://gist.github.com/mikedeboer/5495405. I posted about this subject before on firefox-dev: https://mail.mozilla.org/pipermail/firefox-dev/2013-April/thread.html#268

<snip>

Mike de Boer

unread,
May 8, 2013, 9:11:29 AM5/8/13
to Mark Hammond, Mozilla dev-platform mailing list mailing list
Hi Mark!

Thanks for taking the time to read and reply!

I really don't have an aversion to using Promises at all!
I'm trying to point out that continuation passing style async programming and Promises can co-exist. I love the way how Promises can be neatly combined with Generators and the semantics of the struct appeal to me as well.
It's just the way Promises are required to be used in its current form as a library that bias me towards falling back to continuation passing style.

I'd like to pass you one link that might tell you what I mean: http://mxr.mozilla.org/mozilla-central/source/toolkit/content/Task.jsm#191
This is the part of Task.jsm that developers using it in production code need to be very much aware of: all your code will be executed inside a try…catch block. Last time I checked that represented a potentially massive performance hazard, but please correct me if I'm wrong! (this doesn't mean I think Task.jsm sucks at all, I think it's a great little library which certainly has proven its use)

About the unit tests: I'm not at all proposing to remove Promises from unit tests, far from it! My main point is about proposing to unify the structure of our tests and to promote reusability of code, whereas much test bootstrapping code is now duplicated across the various tests.

The magic prefixes are simply for added convenience to the developer who is wary of using block comments to disable and/ or isolate tests temporarily. I might've explained that poorly! I merely wanted to point out we can do so much more to promote TDD-style development.

Again, thanks for your time,

Mike.

Mike de Boer

unread,
May 8, 2013, 9:55:46 AM5/8/13
to Joshua Cranmer 🐧, dev-pl...@lists.mozilla.org
Hi Joshua!

Thanks for taking the time to read and reply!

Please read my answers inline...

On May 8, 2013, at 6:17 AM, Joshua Cranmer 🐧 <Pidg...@gmail.com> wrote:

> On 5/7/2013 8:49 AM, Mike de Boer wrote:
>> I've told some of you before that I'm not a big fan of Promise libraries (if not, please read the 10 reasons to NOT use a Promise library: https://gist.github.com/mikedeboer/5305020).
>
> For what it's worth, I am very unpersuaded by your argumentation. Rebutting your reasons:
> 1. Promises aren't meant to solve the async problem. In effect, they take code which would be written in a continuation passing style and rewrite that into a more natural serial chaining.
> 2. I'll grant you this, but this is a casualty of CPS code in general.
> 3. I don't understand your complaint here, and this may be because I'm trying to compare promises with similar use cases and you are not.
> 4. This is no more a problem than CPS code in general has. Actually, promises helps this by being able to propagate an exception to the last rejection handler. In effect, it brings back the notion that a careful API consumer can fix the problem of a careless API author.
> 5. This is true for general code on the web. This is not true for Mozilla code, where we have a single promises implementation.
> 6. Ditto. This also strikes me as "I didn't write this, therefore it sucks" syndrome.
> 7. It has been my experience that the environment of testing is often such a diverse field that everything breaks down into ad-hoc algorithms. Look through all of the xpcshell tests in comm-central to see what I mean.
> 8. Again, this strikes me as "I didn't write this, therefore it sucks." See also below.
> 9. All API tends to be inherently viral. It's hard to mix sync and async code, for example. At some level, most code is basically a glorified way to move data from one API to another.
> 10. Paraphrasing, this is "Promises are bad because they force you to write the promises model" (with an implicit "and that model is bad"). It's hard not to take this as "Promises are bad because I hate promises"
>

I hope I clarified my position well enough in my reply to Mark Hammond earlier. I was actually hoping to not go into this discussion, because my point is not at all about trying to get rid of Promises or something else that silly. Paolo Amadini is working on a constant stack space Promise implementation, which aid us much in both adoption and debuggability (see bug 810490).
I think any further discussion should be focussed to commenting on the Gist.

My main point is that Promise and continuation passing style (CPS) are not mutually exclusive and have their own use cases. I will make an effort to make a conscious choice which of the two to use in my projects now and in the future. The use of CPS in AsyncTest.jsm is an example where I made such a decision consciously and not due to something silly and emotional like "I hate Promises" or "it sucks". For testing synchronous code you can forget about CPS altogether and omit the callbacks.

> It's also interesting to note that your points 5, 6, and 8 are slippery slope arguments. Change a few words:
>
> 1. C++ implementations differ a LOT from each other, most often in the
> area of semantics. This makes it so that you may know one
> implementation, but can not use another.
> 2. The most-often used C++ implementations are not easy to contribute
> to. It isn't bug free (no software is). If you encounter one, the
> code is complex enough, so you'd have to wait until a more active or
> seasoned developer may be able to fix it.
> 3. in general - and more towards an opinion: you shouldn't use a helper
> library that you wouldn't be able to write yourself upon first
> glance. It might help you at first, but not in the long run.
>
> Therefore, we should write all of our code in native machine code because we can't rely on C++ implementations, right? (There is a very long history of having to work around buggy compilers in our code, and probably many more problems have been uncovered in trying to build portable C/C++ code than you have seen between all promise libraries).
>

Hmm, I think you're trying to compare apples with oranges here; please understand that I'm trying to make people aware of potential pitfalls of using Promise polyfills and not the concept, design and theory behind the struct - if they weren't already. If that were my goal, then I might've just shot myself in the foot right away. I've been following the es-discuss threads that subject Promise standardization and the WebIDL DOM Future work with great interest and I believe we're well moving forward here. Work being done by the Async Responsive workgroup led by David Rajchenbach Teller is also important regarding this.

>
>> You can pass an Array of test suites or just one Object
>> Suites can have names
>> Tests can be described freely
>> It mixes fine with ANY assertion style (Mochi or XPCShell)
>> Prefix a test with ! to exclude it from the suite. This is something that is practically useful when doing TDD.
>> Prefix a test with > to only run that test and ignore the others. It is meant to signal out a test case and run only that specific one. This is something that is practically useful when doing TDD.
>> If the test is sync, you can forget about the next function (callback) completely - the library takes care of it; you can even mix async and non-async tests
>
> Forgive me for saying this, but it sounds like using this test suite is a lot more painful for not using it. The tests that I need to write tend to come in two categories. Either they're a series of functions which run for an asynchronous amount of time, which yields tests like <http://mxr.mozilla.org/comm-central/source/mailnews/news/test/unit/test_internalUris.js>, or they are running essentially the same code on a large body of test inputs, like <http://mxr.mozilla.org/comm-central/source/mailnews/mime/test/unit/test_parser.js> (large is relative--the rest of those tests are sitting in patches awaiting review). The former uses a common test framework for comm-central (<http://mxr.mozilla.org/comm-central/source/mailnews/test/resources/asyncTestUtils.js>), while the latter is an example of one my many ad-hoc test frameworks.
>

AsyncTest.jsm as it stands now is not a framework, just a library. It favors convention over configuration and flexibility over constraint or boxing-in a specific technology. It doesn't tie in to Mochi or XPCShell or any other test runner, just augments them.

A common library to do assertions is another thing that's high on my 'OMG WANT!' list, but I don't dare burning my fingers on such a project alone.

In the examples you provided I fail to see how they wouldn't work nicely with AsyncTest.jsm… would it help our mutual understanding if I ported them and check it out together?

> --
> Joshua Cranmer
> Thunderbird and DXR developer
> Source code archæologist
>
> _______________________________________________
> dev-platform mailing list
> dev-pl...@lists.mozilla.org
> https://lists.mozilla.org/listinfo/dev-platform


Again, thanks for your time!

Mike.

Mike de Boer

unread,
May 8, 2013, 10:11:24 AM5/8/13
to sam foster, dev-pl...@lists.mozilla.org
Hi Sam!

Thanks for taking the time to read and reply!

Please read my answers inline...

On May 8, 2013, at 3:11 PM, sam foster <potatos...@gmail.com> wrote:

> I want to add my +1 to the goal of unifying and streamlining the setting up of test flows and a common assertion syntax.
>

Yay!

> I know some of the issues you raise with Promises (like getting a useful stack on exceptions) are being discussed and addressed already. I dont have all the context, but ISTM the task/promise boat has already sailed, so step 1 in the unification effort is ferretting out and fixing issues created or not-solved by this approach before adoption is truly widespread.
>

I hope my explanation and comments in my replies to Mark and Joshua clarified my position regarding this topic!

> In the past I've seen real benefit from having some kind of Test or Fixture class/prototype, as well as for a group or suite of tests. It lets you easily share boilerplate for similar tests and gives you a place to hang state and other products of the steps in a test, where they can be tracked and cleaned up during teardown.
>
> In the (mochi)tests we'e been writing in browser/metro, we've found need for a few things to facilitate test writing and useful running and reporting behavior, including:
>
> * async setUp, i.e. setUp is itself a Task
> * various helpers for events, observers and other asynchrony tests need to navigate
> * method spies/stubs
> * try/catch hackery to allow a suite to complete and report all errors, rather than falling at the first hurdle
>
> The guts of the test runner-runner are here: https://mxr.mozilla.org/mozilla-central/source/browser/metro/base/tests/mochitest/head.js#668 .. I'm sure each module and project has some unique requirements but a lot of this stuff feels strongly like it should already exist. Count me in on any efforts to fill this gap.
>

Yeah, this is what I'm trying to propose… and not only for Mochi-tests, but for any JS unit test now and in the future. In that sense I'm also not trying to 'push' AsyncTest.jsm, it's merely something I wrote & documented for general use that might fit the bill.

> /Sam
>
> On Tuesday, May 7, 2013 2:49:49 PM UTC+1, Mike de Boer wrote:
>> TLDR; in bug 867742 I requested to adopt two JS modules, Async.jsm and AsyncTest.jsm, in mozilla-central/toolkit/modules. The whole story can be read below as well as at https://gist.github.com/mikedeboer/5495405. I posted about this subject before on firefox-dev: https://mail.mozilla.org/pipermail/firefox-dev/2013-April/thread.html#268
>
> <snip>
> _______________________________________________
> dev-platform mailing list
> dev-pl...@lists.mozilla.org
> https://lists.mozilla.org/listinfo/dev-platform


Thanks again!

Mike.

0 new messages