12
Dependencies

'Dependencies' are a very advanced feature, albeit a little complex. You should have a reasonably good grasp of QF-Test, especially for things like control flow, variable binding and error handling, before you start using them. However, when properly implemented, 'Dependencies' will feel almost like magic when you run several non-related 'Test-cases' and all setup and cleanup is handled automatically. 'Dependencies' are also crucial for running tests in a QF-Test Daemon as described in chapter 40.

To illustrate the following sections, two test-suites doc/tutorial/dependencies.qft and demo/carconfig/carconfig_en.qft are provided. The former illustrates the various concepts for 'Dependencies' and the latter is a simple real-world example. The SWT demo test-suite swt_addressbook.qft and the data driver demo datadriver.qft also make use of 'Dependencies'. Single-stepping through these suites in the debugger, looking at the variable bindings and examining the run-logs should help you to familiarize yourself with this feature.

12.1
'Dependency' basics

Conceptually a 'Dependency' describes a set of preconditions that have to be met before a test can be executed. These preconditions are often dependent on each other, for example parameter settings may have to be read which are required for starting the SUT. The target Window for a specific test-case cannot be opened before the SUT is running, and so on.

'Dependencies' can be defined in two places: Generic 'Dependencies' that will often be reused or serve as the basis for other 'Dependencies' can be implemented just like a 'Procedure' and placed below the 'Procedures' node, for example inside a 'Package' node called "Dependencies". Their fully qualified name is built analogous to a 'Procedure' name and 'Dependencies' can be referenced using a 'Dependency reference' almost exactly like a 'Procedure call'.

Alternatively, 'Dependencies' can be implemented at the beginning of a 'Test-suite', 'Test-set' or 'Test-case' node. In addition to having their own 'Dependency', 'Test-cases' and 'Test-sets' can inherit the 'Dependency' from their parent node.

Each 'Dependency' should always take care of only one specific precondition and refer to other 'Dependencies' to ensure that the more basic preconditions on which it depends are taken care of. This is done implicitely by inheriting the 'Dependency' of a parent node or explicitely with a 'Dependency reference'.

The actual implementation is done in 'Setup' and 'Cleanup' nodes inside the 'Dependency'. The 'Setup' node should always be implemented in a way that ensures the minimum of effort to fulfill the requirement. For example, the 'Setup' to start the SUT should first check whether the SUT is already connected or not and execute the SUT startup sequence only in the latter case. The reason for this will be explained below. 'Setup' and 'Cleanup' nodes should always be prepared to handle errors that do not affect the outcome. For example, the 'Cleanup' node that terminates the SUT should terminate cleanly if the SUT is already disconnected.

12.2
The dependency stack

When a 'Dependency' is executed, QF-Test linearizes the 'Dependency' and its ancestors to create a dependency stack. For example, if 'Dependency' D depends on 'Dependencies' B and C which in turn depend on 'Dependency' A, the stack will look like [A,B,C,D]. If this is the first 'Dependency' to be executed, there will be no previous dependency stack and QF-Test will simply execute the 'Setup' nodes of the 'Dependencies' from most basic to most specific, i.e. A-B-C-D.

Dependency stack A-B-C-D
Figure 12.1:  Dependency stack A-B-C-D

After the 'Test-case' that required the 'Dependency' has been executed, part of the dependency stack may be rolled back. If any of the 'Dependencies' on the stack has the 'Forced cleanup' attribute set, it and all of the following 'Dependencies' will have their 'Cleanup' sequences executed, this time in the opposite order. If, in the example above, C has 'Forced cleanup' set, first D's 'Cleanup' node will be run, then C's 'Cleanup' node. After that, the remaining dependency stack will look like [A,B].

Forced cleanup
Figure 12.2:  'Dependency' C has "forced cleanup"

Now let's say another 'Test-case' is executed that depends on a 'Dependency' E which in turn depends on A, so the new target dependency stack is [A,E]. Now QF-Test compares the current dependency stack [A,B] to the new target stack [A,E] and finds the first point of difference, which is B vs. E in this case. Next the old dependency stack will be unrolled to this point from right to left, so B's 'Cleanup' node is executed. The current stack now looks like [A]. Then dependency E is added which results in [A,E].

Dependency stack A-E
Figure 12.3:  Rollback and build up stack to A-E

The next point is crucial: To get from the cleaned up old dependency stack [A] to the new target stack [A,E], QF-Test doesn't simply execute the 'Setups' of the rest of the new dependency stack - E in this case. Instead, the 'Setup' nodes of the whole new target stack are executed, here first A's 'Setup' node and then E's. This repeated execution is the main reason why 'Setup' nodes should always be implemented to do as little as possible.

Why such a weird asymmetry? Let's assume that A is the 'Dependency' responsible for bringing up the SUT. In the normal case for the previous example, the SUT will already be running and if A's 'Setup' is properly implemented it will notice that and there will be very little overhead. However, QF-Test has no control over what has happened since the last time A's setup was run. You might have closed the SUT manually or it might have been closed as a side effect of the previous test or even crashed. In this case, if A's setup were not run again, E's precondition could never be satisfied and the whole 'Test-case' would fail along with all subsequent tests that also rely directly or indirectly on 'Dependency' A. Thus, always executing all 'Setup' nodes on the target dependency stack is the only way to ensure proper setup in all circumstances.

If get stuck or lost you can clear the dependency stack manually in two ways: By selecting the »Run«-»Rollback dependencies« or the »Run«-»Reset dependencies« menu item. The former will completely roll back the dependency stack, executing the 'Cleanup' nodes of all 'Dependencies' that were left on the stack, while the latter will simply clear the stack without executing anything.

12.3
Error escalation

Another thing that is just grand about 'Dependencies' is the convenient way that errors can be escalated without any additional effort. Let's again consider the example from the previous section after the first dependency stack has been initialized to [A,B,C,D] and the 'Setups' have been run. Now what happens if the SUT has a really bad fault, like going into a deadlock and not reacting to user input any longer?

When a 'Cleanup' node fails during rollback of the dependency stack, QF-Test will roll back an additional 'Dependency' and another one if that fails again and so on until the stack has been cleared. Similarly, if one of the 'Setups' fails, an additional 'Dependency' is rolled back and the execution of the 'Setups' started from scratch. In the example above, the deadlocked client would cause failures until the rollback of the SUT 'Dependency' terminates the SUT and causes a new one to be started in the next round.

Error escalation
Figure 12.4:  Exception in forced cleanup sequence of C causes B to clean up

For this to work it is very important to write 'Cleanup' sequences in a way that ensures that either the desired state is reached or that an exception is thrown and that there is a more basic dependency with a more encompassing 'Cleanup'. For example, if the 'Cleanup' node for the SUT 'Dependency' just tries to cleanly shut down the SUT through its File->Exit menu without exception handling and further safeguards, an exception in that sequence will prevent the SUT from being terminated and possibly interfere with all subsequent tests. Instead, the shutdown should be wrapped in a Try/Catch with a Finally node that checks that the SUT is really dead and if not, kills the process as a last resort.

With good error handling in place, 'Test-cases' will rarely interfere with each other even in case of really bad errors. This helps avoid losing a whole night's worth of test-runs just because of a single error.

12.4
Special variables

A 'Dependency' can in turn depend on the values of certain variables. For example, you may want to test your SUT with different JDK versions. Thus, the JDK would be a characteristic variable for a 'Dependency' that brings up the SUT. If you have a current dependency stack [A] and the target stack is also [A], but last time A was executed with one JDK value and now some other JDK is requested, QF-Test must roll back the 'Dependency' to terminate the current SUT and ensure that a new one with the proper JDK is started.

Characteristic variables
Figure 12.5:  Change in characteristic variable causes cleanup of A

All this is handled fully automatically if you add the JDK variable to the list of 'Characteristic variables' of the 'Dependency'. The values of the 'Characteristic variables' are always taken into account when comparing dependency stacks and two 'Dependencies' on the stack are only considered identical if the values of all 'Characteristic variables' from the previous and the current run are equivalent. Consequently it is also possible for a 'Dependency' to directly or indirectly refer to the same base 'Dependency' with different values for its 'Characteristic variables'. In that case the base 'Dependency' will appear multiple times in the linearized dependency stack.

Furthermore, QF-Test stores the values of the 'Characteristic variables' during execution of the 'Setup' of a 'Dependency'. When the 'Dependency' is rolled back, i.e. its 'Cleanup' node is executed, QF-Test will ensure that these variables are bound to the same value as during execution of the 'Setup'. This ensures that a completely unrelated 'Test-case' with conflicting variable definitions can be executed without interfering with the execution of the 'Cleanup' nodes during 'Dependency' rollback. Consider for example the commonly used "client" variable for the name of an SUT client. If a set of tests for one SUT has been run and the next test will need a different SUT with a different name, the "client" variable will be changed. However, the 'Cleanup' node for the previous SUT must still refer to the old value of "client", otherwise it wouldn't be able to terminate the SUT client. This is taken care of automatically as long as "client" was added to the list of 'Characteristic variables'.

12.5
Error handling

Besides supporting automatic escalation of errors a 'Dependency' can also act as an error or exception handler for the tests that depend on it. 'Catch' nodes, which can be placed at the end of a 'Dependency', are used to catch and handle exceptions thrown during a test. Exceptions thus caught will still be reported as exceptions in the run-log and the report, but they will not interfere with subsequent tests or even abort the whole test-run. This is very similar to the 'Implicitly catch exceptions' attribute of 'Test' nodes, but more specific.

An 'Error handler' node is another special node that may be added to a 'Dependency' after the 'Cleanup' and before the 'Catch' nodes. It will be executed whenever the result of a 'Test-case' is "error". In case of an exception, the 'Error handler' node is not executed automatically because that might only cause more problems and even interfere with the exception handling, depending on the kind of exception. To do similar things for errors and exception, implement the actual handler as a 'Procedure' and call it from the 'Error handler' and the 'Catch' node. 'Error handlers' are useful for capturing and saving miscellaneous states that are not automatically provided by QF-Test. For example, you may want to create copies of temporary files created during execution of your SUT that may hold information pertaining to the error.

Error handling
Figure 12.6:  Execution of 'Catch' and 'Error handler' nodes

Only the topmost 'Error handler' that is found on the dependency stack is executed, i.e. if in a dependency stack of [A,B,C,D] both A and C have 'Error handlers', only C's 'Error handler' is run. Otherwise it would be difficult to modify the error handling of the more basic 'Dependency' A in the more specialized 'Dependency' C. To reuse A's error handling code in C, implement it as a 'Procedure'.