New testing framework (FLUID-4850 and FLUID-4886) - and a QUnit bug
Antranig Basman
antranig.basman at colorado.edu
Fri Jan 25 23:01:41 UTC 2013
All of the changes to the testing framework for Infusion have now been pushed to trunk - thanks to yzen and
michelled for great review.
As described in the outline JIRA, the main changes are to move the jqUnit API over to a constructor-free
form that more clearly mirrors the real semantic of the underlying QUnit API:
http://issues.fluidproject.org/browse/FLUID-4886
Every person who writes new test cases will now have to use the following form (all existing test cases in
Infusion's image have already been update)
In the past, where you would write
var myTestCase = new jqUnit.TestCase("My Test Cases");
myTestCase.test("My Test Case", function () ....
you will now write
jqUnit.module("My Module");
jqUnit.test("My Test Case", function () .....
All our existing testing utilities have been rationalised, so for the most part now you should ONLY ever be
using methods on the jqUnit namespace when writing your tests - the global definitions with which QUnit
polluted the global namespace have been removed. This includes directives like jqUnit.expect, jqUnit.start,
jqUnit.stop, etc. The definitions which were previous exposed as part of fluid.testUtils.* are now also on
jqUnit.* - e.g. jqUnit.assertLeftHand, jqUnit.assertNode, etc. Note that the namespaced QUnit global is
still exposed for the rare cases when you need to drop into raw QUnit. These is one important case for this
which will now be described.
Comparing Deep Equality in jqUnit/QUnit
=======================================
The question of how to compare deep object trees for equality is somewhat perplexed in QUnit. The default
algorithm which is used is quite powerful, but has a few features which IMO are inappropriate, especially in
environments such as Node.js where stability of built-in constructors can't be relied upon - for example,
every NPM "context" will have its own instances of Array, Object, etc. and so checks like "x instanceof
Array" can't be relied upon to be effective - this is exactly the same issue that we face when transporting
JS object between iframe boundaries in the browser. This is the reason for our specialised utilities
"fluid.isArrayable" etc. which rely on schemes which have already been devised for this purpose in jQuery.
In general in Fluid, we are much more interested in JSON-based structural equality of trees, rather than
verifying some kind of "exact prototypical correspondence". The QUnit team recently responded to this
requirement with discussion on https://github.com/jquery/qunit/issues/279 - recent version of QUnit expose a
new method called "propEqual" which operates structural comparison of this type. One of the updates that are
in the recent merge are to forward all calls from jqUnit.assertDeepEq onto this new QUnit.propEqual method
rather than QUnit.deepEqual as before.
This means that in almost all cases, including in Node.js and cross-iframe situations, the semantics of
comparison are just what we would want and there is no need to think about the choice of jqUnit.assertDeepEq
which carries on working as before. However, there are some issues with the QUnit implementation which mean
that in some special cases you will need to tread carefully. These issues all, in one way or another,
related to comparing structures which may have circular linking within them.
For a start, the implementation of QUnit.propEqual is much less sophisticated than QUnit.deepEqual - in
fact, it amounts to operating a fluid.copy on both arguments given to QUnit.propEqual and then passing them
over to QUnit.deepEqual. In particular, in the case these arguments have circular linkage, this will cause a
stack overflow and bomb the test - and in Firefox at least, this stack overflow will bomb the entire browser
too.
Normally supplying a circularly linked structure to assertDeepEq represents an error in the first place -
but there are a few cases when arguably it doesn't. For example, many DOM nodes hold such circularly linked
structures - especially on Safari. TableOfContents.js held a test case which wanted to compare one array of
DOM nodes for equality with another, and when passed to the new jQUnit.assertDeepEq -> QUnit.propEquals,
this will bomb the test. This is one of the cases when the appropriate test is to drop back into raw QUnit
and issue a QUnit.deepEqual test - I updated this test to that form.
However worse, there is also a BUG. It turned out that part of the "clever" old implementation of
QUnit.deepEqual was some code dedicated to specifically handling the cases of circular linkage. It is
because such code is missing in the new QUnit.propEquals driver that it is now possible for circular linkage
to bomb the test case. However, this deepEqual code, whilst it assured that the tests would not bomb, did
not succeed in providing a consistent semantic for comparison - this resulted in that ultimate horror in a
test framework, the possibility for "false positives". In our call yesterday we talked over the worry of
potential false positives - that is, tests which pass due to an error in the testing framework which should
not pass - and it turns out there is just such a thing in QUnit. In the case where the only difference
between two JSON structures consists of a circular linkage, QUnit.deepEqual will incorrectly compare them as
the same. To some extent, the new default behaviour of bombing out the test case (and maybe even the
browser) could be described as "helpful" since at least it signals there is a problem. I have reported the
issue upstream at qunit as
https://github.com/jquery/qunit/issues/397
This advice boils down to - in cases where there might be circular linkage in the structures you are
comparing (mostly, legitimately I can only think of DOM nodes), do consider dropping back into raw
QUnit.deepEqual - BUT beware in this case that the circular linkage itself might lead to false positives in
your tests. In all cases, think of other ways of expressing your tests that don't require comparing such
structures and just use jqUnit.assertDeepEq instead. IF you find your tests bombing out, try testing them
out in Chrome/Safari rather than Firefox, which have much more forgiving behaviours in the case of a stack
overflow.
More information about the fluid-work
mailing list