Today's Community Meeting on Testing

Antranig Basman antranig.basman at colorado.edu
Thu Dec 6 03:38:47 UTC 2012


Today we had an edifying community meeting on the subject of our new testing infrastructure - now the 3rd in 
a series of probably 4 meetings. This thread was suggested by an early post of Yura's - can be seen at
http://old.nabble.com/Testing-Framework-Community-Meeting-to34626170.html


The goals of the new framework include:

i) To facilitate the testing of demands blocks that may be issued by integrators against components deployed 
in a particular (complex) context
ii) To automate and regularise the work of "setup" and "teardown" in complex integration scenarios, by 
deferring this to our standard IoC infrastructure
iii) To simplify the often tortuous logic required when using the "nested callback style" to test a 
particular sequence of asynchronous requests and responses (via events) issued against a component with 
complex behaviour
iv) To facilitate the reuse of testing code by allowing test fixtures to be aggregated into what are 
becoming the 2 standard forms for our delivery of implementation - a) pure JSON structures which can be 
freely interchanged and transformed, b) free functions with minimum dependence on context and lifecycle


I presented the implementation I have so far, which is now good enough to demonstrate the approach we want 
to take for iii), allowing testing of event sequences. The quadratically increasing complexity of doing this 
by hand typically deters us from writing thorough tests of this kind - the following heroic code written by 
Yura for testing a CollectionSpace component illustrates the route by which this complexity increases:

                 "recordEditorReady.test": {
                     path: "listeners",
                     listener: function (admin) {
                         var recordRenderer = admin.adminRecordEditor.recordRenderer;
                         jqUnit.assertEquals("Selected username is", "Reader", 
locateSelector(recordRenderer, "screenName").val());
                         jqUnit.notVisible("Confiration dialog is invisible initially", 
admin.adminRecordEditor.confirmation.popup);
                         locateSelector(recordRenderer, "screenName").val("New Name").change();
                         admin.adminRecordEditor.confirmation.popup.bind("dialogopen", function () {
                             jqUnit.isVisible("Confirmation dialog should now be visible", 
admin.adminRecordEditor.confirmation.popup);
                             admin.events.onSelect.addListener(function () {
                                 admin.events.recordEditorReady.addListener(function (admin) {
                                     jqUnit.assertEquals("User Name should now be", "Administrator", 
locateSelector(admin.adminRecordEditor.recordRenderer, "screenName").val());
                                     start();
                                 }, undefined, undefined, "last");
                             });
                       .....
(available at https://github.com/collectionspace/ui/blob/master/src/test/js/AdminUsersTest.js#L586-594 )

The outer level shows a testing framework devised by Yura to start to attack this issue - although the outer 
layer of event registration has been unwound, in the body of the listener we can see a set of callbacks 
nested FOUR deep in order to issue an integration test making assertions about a sequence of 4 events.

A declarative structure would allow us to flatten this nesting into a simple array of sequential assertions. 
Here is an example from the test cases I showed today:

fluid.defaults("fluid.tests.asyncTester", {
     gradeNames: ["fluid.test.testCaseHolder", "autoInit"],
     testCases: [ {
         name: "Async test case",
         tests: [{
             name: "Rendering sequence",
             expect: 2,
             sequence: [ {
                 func: "fluid.tests.startRendering",
                 args: ["{asyncTest}", "{instantiator}"]
             }, {
                 listener: "fluid.tests.checkEvent",
                 event: "{asyncTest}.events.buttonClicked"
             }]
         }
         ]
     }]
});

(available in my FLUID-4850 branch at 
https://github.com/amb26/infusion/blob/bc7a6a414d251a640e868aca38a9977f474cc9be/src/webapp/tests/test-core/testTests/js/TestingTests.js#L132-L149 
)

The interesting aspects of this "test fixture holding grade" are as follows -

i) It is a pure configuration grade with no implementation code, offering good potential for reusability
ii) The block of configuration "sequence" is a flat array, where each element of the array corresponds to a 
sequential state of the component under test.

The "sequence" array may hold "fixture directives" of a small variety of types. We can currently imagine a 
repertoire of 4 or 5 record types, which themselves can be assigned to one of two broader categories - 
"executor" records which actively interact with the component under test, and "binder" records which 
register listeners responding to events fired by the component under test. Any sequence of these records may 
appear in any order. The types we imagine, indexed by a "duck typing field" system slightly reminiscent of 
that used by the Fluid Renderer, are as follows:


func (executor): Execute a free function with arguments IoC-resolved against the tree

listener (binder): Register a listener to a Fluid event good for ONE firing only at the appropriate point in 
the sequence

jquery (binder): Register a listener to a jQuery event against a DOM element resolvable in the tree

changeListener (binder): Register a listener to a ChangeApplier event fired by a model-bearing component in 
the tree

jqueryTrigger (executor): Trigger a jQuery event from a DOM node resolvable in the tree


We covered the overall idiom of the test framework in our meeting two weeks ago, but a few points have been 
clarified since then. The overall scheme is that arbitrarily sized chunks of application code under test 
will be embedded together with "fixture holders" of the type shown above together in the same overall 
component tree, corresponding to a "testing environment". The setup and teardown for all of the test cases 
held in these fixtures will proceed by the standard IoC semantics for construction and destruction of 
component trees. The root of this tree will hold a component with the grade "fluid.test.testEnvironment" 
with a specific name chosen by the fixture author which is suitable to issue demands blocks whose scope 
consists of just this environment. The "fixture holders" scattered around the tree arbitrarily each have the 
grade "fluid.test.testCaseHolder" - each of these contains configuration coding for a set of jqUnit (qunit) 
test case "modules" and "test cases" which will be dispatched to the standard jqUnit framework once the 
construction of the overall component tree is complete.

A few issues came up relating to the potential mismatch between this setup/teardown model, at the unit of 
whole component trees and qunit "modules", and the native one operated by qunit which only operates at the 
level of individual test cases. Our tentative decision is to sidestep the native teardown model completely 
in favour of one operated by this new framework in a dedicated way. The native model comprises three parts - 
i) setup/teardown at the level of JavaScript globals, checking for global namespace pollution - ii) 
setup/teardown of markup nested inside a DOM node with the hard-coded ID "qunit-fixture" (formerly "main" in 
earlier versions of qunit) - iii) user-supplied setup/teardown functions to a module which are operated for 
each individual TestCase within the module.

Part i) is healthy enough, although there should be no instances of such global pollution caused by Fluid 
components (unless their execution happens to cause further components to become defined - not expected with 
our current code idioms). ii) will be replaced by a new scheme allowing ANY selector to be registered at the 
root of the environment as the markup to participate in setup/teardown on the lifecycle of the entire 
environment, rather than individual test cases, iii) will be replaced by the overall action of the 
construction and destruction of the component tree.


Together with the presentation, there were a number of interesting questions from the group which I 
reproduce here (not necessarily in order):


Testing onCreate:
================

Justin asked how the system could be used to test the action of the "onCreate" event of a component in the 
tree under test. I replied that the creation of the component under test would need to be deferred by means 
of the "createOnEvent" directive, and then an executor block inserted into the fixture description to 
operate that event within the fixture sequence, as in the following sketch:

fluid.defaults("fluid.tests.myTestTree", {
     gradeNames: ["fluid.test.testEnvironment", "autoInit"],
     events: {
         startCat: null
         },
     components: {
         cat: {
             createOnEvent: "startCat",
             type: "fluid.tests.cat"
         },
         catTester: {
             type: "fluid.tests.catTester"
         }
     },

and then in the tester:


fluid.defaults("fluid.tests.catTester", {
     gradeNames: ["fluid.test.testCaseHolder", "autoInit"],
     testCases: [ {
         name: "Late cat tester",
         tests: [{
             name: "Rendering sequence",
             expect: 2,
             sequence: [ {
                 func: "{myTestTree}.events.startCat.fire",
                 }, {
                 event: "{cat}.events.onCreate",
                 listener: "fluid.tests.catCreationTester"
                 },
                 etc.


Spectrum between unit tests and integration tests:
=================================================

Michelle asked about how appropriate this system was for expressing unit tests as considered against 
integration tests, how these two situations could be identified by those reading the code, and how we would 
make recommendations about what tests to write. After some discussion, we resolved on a few things:

i) There is a spectrum between unit tests and integration tests, which can be roughly identified by the SIZE 
of the component tree appearing in the "component under test" part of the overall environment test. If this 
tree consists of a single component, the test situation would more clearly have the character of a "unit 
test". A large tree or indeed an entire application present in the environment would represent a complex 
integration test.
ii) One purpose of the framework would be to bridge between the worlds of unit and integration tests, 
allowing the same code and idioms to be usable in both worlds. However, it's clear that in general the IoC 
testing system is more appropriate and more economical the closer the situation represents an "integration 
testing scenario"
iii) That said, even a single Fluid component which is event-driven could benefit significantly from having 
its tests written in the new style - this enables us to write tests which are easier to read intent from, as 
well as being more powerful than those we could write before - easily testing a PARTICULAR sequence of 
events, rather than just placing ad hoc constraints on sequencing in the way we often would in our old-style 
tests, usually involving some kind of scrawling into suitably scoped variables - the following style of code 
will be familiar to us all:

         var that = fluid.tests.eventParent3();
         var received = {};
         that.eventChild.events.relayEvent.addListener(function(arg) {
             received.arg = arg;
         });
         that.events.parentEvent1.fire(that); // first event does nothing

iv) Non-event driven code which just implements ALGORITHMS (such as the model transformation system) can 
continue to be profitably written in the plain old jqUnit style - however -
v) Given that "new" testing code is reduced to the status of free functions holding jqUnit assertions, there 
is a lot of scope for sharing and reusing these fixture functions between code written in plain jqUnit style 
implementing unit tests, and IoC style implementing integration tests - for example, the following fixture 
function in the TestingTests we looked at:

fluid.tests.startRendering = function (asyncTest, instantiator) {
     asyncTest.refreshView();
     var decorators = fluid.renderer.getDecoratorComponents(asyncTest, instantiator);
     var decArray = fluid.values(decorators);
     jqUnit.assertEquals("Constructed one component", 1, decArray.length);
     asyncTest.locate("button").click();
};

could just as easily be invoked from within a manual jqUnit test as by the IoC testing framework.

Appropriate unit of reusability:
===============================

Alex expressed the concern that the global namespace might endlessly clutter up with numerous specifically 
named fragments of dedicated testing functions (in the "free" style we just looked at). We turned to an 
analysis of Yura's very complex testing code (near the beginning of this post), and decided that the best 
outcome would be if the ability to write such free functions ended up in a better factoring of testing 
responsibilities rather than proliferating numerous similar functions. In fact, once all the complexity of 
event binding were removed from this code, we would discover that the actual testing assertions issued 
mostly consisted of single jqUnit assertions - which could be issued directly from the fixture sequence 
rather than requiring a new dedicated free function to be written. Other opportunities for reuse could also 
be more clearly identified - for example, the AdminUsersTest.js file shown above would reveal opportunities 
centred around the existing "locateSelector" function as used often in the following pattern:

                         locateSelector(recordRenderer, "screenName").val("New Name").change();

Testing particular instance of event firing:
===========================================

Justin asked a question relating to a situation encountered in Decapod where he needed to test (say) the 
SECOND instance of firing an event (in this case, a rendering event) whilst ignoring the first, and asked 
whether this kind of thing would be assisted by the framework. I replied that this was just the kind of use 
case for which it was designed - a sequence record, say, holding just fluid.identity or jqUnit.assert could 
be supplied for the first event firing, and a more complex one for the second event, verifying that 
particular pieces of markup had indeed been rendered.


Dealing with "dropped sequence" failures:
========================================

JURA highlighted the frequently unhelpful behaviour of (j)qunit on encountering a failure in an asynchronous 
test - this simply causes the UI to hang, without any clear indication of whether there really are more 
tests or what the expected operation which failed to occur was. To be clear, this is a failure cause by some 
path where an asyncTest fails to cause the "QUnit.start()" operation to be invoked, through a particular 
event failing to be fired - rather than a direct failure held within code.

I replied that this problem scenario was to a certain extent a fundamental and irremovable feature of any 
asynchronous testing framework - purely by virtue of being asynchronous, an event's occurence would be 
unexpected and could never be finally deemed not to have occured. However, I suggested that as a result of 
the clear declarative nature of the "sequence" structure, the new framework could make it considerably 
clearer than formerly which the "missing event" was by providing information as to the most recently 
correctly operated sequence point. This could (should) even be highlighted somehow in the existing QUnit UI 
as a separate diagnostic to help the user pinpoint the expected and missing event.

Yura mentioned that another approach could be to assist the user in setting timeouts on events - I commented 
that this was certainly valuable, but could itself be the cause of false positive test failures in the case 
the browser was running slowly. However, it would be something that would be easy to add to the existing 
declarative structure for "binding" sequence records by adding an extra "timeout" field - far easier than 
writing the requisite timeout handling code by hand.



I feel there were some other interesting and useful questions but can't bring them to mind right now, 
perhaps if someone recalls, they could add them in replies.



Implementation status:

I still have some work to do to bring the implementation to a usable status. On the to-do list:

i) Implement the other 3 record types (jquery, changeListener, jqueryTrigger) and apply much more thorough 
testing to ensure that all of the different patterns of sequential appearance are handled correctly in terms 
of the binding and execution sequence chosen by the framework
ii) Fix the current very hacked system for chaining together the execution of several test environments in 
sequence - given the markup setup/teardown system described above, this needs to be properly serialised to 
avoid corrupting the document
iii) Implement the described markup setup/teardown system!
iv) Ensure that something sensible happens when the user selects just a single test, or a test case filter 
to be operated via the HTML UI provided by qunit. This should be possible (perhaps by using our now freed-up 
slot for qunit per-test setup/teardown functions) without needing to hack on the underlying qunit code!

This should be all ready in time for the final testing meeting next week!

Cheers,
Antranig



More information about the fluid-work mailing list