Testplan structure is written in a way to promote usage of base classes that provide common functionality. These base classes may be inherited by more complex classes which represent objects in diferent areas: tests, drivers, pools, exporters, etc. Each class accepts configuration and can inherit the configuration of the base class as well.
Key reasons for using this structure:
- Avoid code copy-paste across similar classes.
- Provide a way to define relations between classes (i.e a Multitest has many drivers, a Testplan has many pools).
- Provide a functional .abort() process that takes under consideration the hierarchical dependency of objects.
- Provide a way to define configuration options for the objects while having access to a global configuration.
- Ctrl-C and sigterm/sigint handling without hanging.
- Common logging and runpath(output files of classes) creation logic.
- User programmatic customizations support in various functionality areas.
- Parallel and remote execution.
- Integration of runtime with other languages or UI tools (i.e Java, React).
Config - configuration objects with
default values or user input with **options after performing a validation stage.
Config objects can inherit other Config objects and alter or define additional
- Config instances may have container references that are used to retrieve a config option value if this is not present in the current config. The
Configcontains the rules on whether current or container config option will be used.
Entity is the first and most important
base class to be used by by classes that:
- accept configuration that inherit
- have a status with capability to validate transitions (i.e it’s legal to go from STARTING to STARTED but not from STOPPING to STARTED)
- need to be able to be aborted and a way to be identified as active (not aborted)
- need a reference to a “parent” entity to inherit configuration (i.e MultiTest parent is the TestRunner)
- make use of temporary directory for output files (runpath)
- have dependency entities that need to be aborted before existing entity (i.e drivers need to be aborted before Multitest and pools need to be aborted before TestRunner)
Resource subclass of
Entity class is used to create
temporary helpful resources that simply need to be STARTED and then STOPPED.
They are NOT creating any result at all. I.e drivers are resources of Multitest
and pools are resources TestRunner.
- Execute a number of steps
- Create and provide a RunnableResult that has a “passed” or not state
- May be able to be executed in an interactive mode based on their interactive_handler interactive_runner.
RunnableManager objects will execute
a Runnable object in a seperate thread protecting it from abort signals like
Testplan subclass of
RunnableManager is the main class to be
used as it manages the main
that is the implementation of the actual testing framework and that it’s
TestRunnerConfig options and
that provides a
TestplanResult object representing
the result of the runnable steps execution and also contains a report.
A user can instantiate a plan object directly
plan = Testplan(**options)
and then manually use
plan.run() to execute it
or decorate a main function with
@test_plan decorator and call it.
Testplan accepts a
parser object that will parse
command line options and use these ONLY IF the values are NOT
programmatically hardcoded in **options of the constructor. This is due to the
default arguments of the parser object that makes not obvious to identify if
a parser.attribute is user command line input or just a default value. So
DO NOT hardcode values in constructors if you want them to be overwritten by
command line arguments OR parser needs to change to wrap the defaul values into
DefaultValue() objects similar to
- Testplan has the actual TestRunner testing framework inside it and the framework itself is a runnable that executes steps (i.e start/stop pools, invoke test result exporters etc).
- TestRunner framework has Resources that can be started/stopped that may be local test runners, pools to execute tasks (callable that return actual tests) or the environments container resources to maintain environments of drivers outside of tests. (This is useful also when interacting from other languages that only need to make use of the environment capability of Testplan.)
MultiTest is the native python
testing framework supported by Testplan which is a runnable class that executes
steps (i.e start/drivers drivers, execute python testcases) and creates a
MultitestResult that contains a report. The environment is a collection of
drivers that will be accessed from within the testcases.
Driver base class provide
common functionality to usual drivers like extracting values from logs and
expose attributes via context mechanism so that they can be retrieved at runtime
by other drivers. Also they provide virtual functions like
pre/post_start/stop to be able to be customized by users when implementing a
Task containers are holding the
information for later initialization of actual tests and can be serialized
and dispatched in external python interpreters for the execution.
Pool objects are Executor resources of
Tasks and based on their implementation they execute tests differently.
ProcessPool executes tests in
external process workers in the same host while
RemotePool executes tests in
remote interpreters in different hosts.
TestReport (top level Testplan report)
testsuite, etc..) objects are containing all result information of the tests and
status. They can be serialized and deserialized and this is a requirement as
they are a part of the result objects of runnables that are transferred between
different interpreters in process and remote pools.
testplan/common directory contains common base classes and utilities that may be used by multiple modules in the repository. Code under this directory need to be generic enough and must not have any dependency on testplan code outside tesptlan/common directory structure.
testplan/exporters directory contains actual implementation of exporters of reports including test reports to json, xml, pdf, ui formats.
testplan/report directory contains actual implementation of reports including test reports that can be later used by test exporters.
testplan/runnable directory contains the main runnable testing framework functionality including interactive mode.
testplan/runners directory contains the test runner and task execution pools (i.e thread, process, remote) and task and task results definition.
testplan/testing directory contains the test runnables inluding MultiTest
that execute testsuites and testcases and produce a test report. It also
includes features for tagging, ordering, listing the cases but not all tests
may support them. The base class of all tests is
Test that provides an environment of drivers
and the base class for tests that will execute a binary to run the actual
testplan/base.py module contains the main
Testplan and @test_plan definitions.
tests/functional directory contains all functional tests organised in a directory structure mirroring testplan code structure.
- test/functional/examples contains the tests of the downloadable examples.
tests/unit directory contains all unit tests organised in a directory structure mirroring testplan code structure.
doc/en directory contains all .rst files documenting all logical components.
doc/en/api directory contains the .rst files for automatic API documentation generation from source code.
examples directory contains premade examples demonstrating common use cases making user adaptation easier. A downloadable example need to have its corresponding documentation entry in doc/en/download directory so that the users can access it from the documentation webpage.
Local execution is the default. There are two options:
When adding or scheduling a Test (i.e MultiTest) it is being added/scheduled as a whole in a single executor and it’s down to its internal implementation of how the testsuites/testcases will be executed.
Execution groups can be used in MultiTest testcases so they can run in parallel in groups against the same live environment. Documented here.
Pools are implementing specific execution strategy and can be combined in the same Testplan making it possible for tests to be executed in different threads/processes/hosts or a specific cloud platform. Remote execution can be achieved using RemotePool as documented.
By default, testplan will operate in a “batch” mode - it will run all tests in a pre-defined order (either sequentially or parallelised by some execution pool), produce a hierarchical report tree containing the test results, and then run exporters to convert the report tree into various output forms (JSON, PDF, web UI etc.). Once started to run in batch mode, Testplan needs no further input and will run to completion unless interrupted by some signal.
The interactive mode on the other hand allows tests to be run on-demand, with the results immediately displayed as they are available. Interactive mode is useful for local development - it provides a graphical interface for choosing which testcases to run and allows testcases to be modified and re-run iteratively without having to restart a testing environment.
The main technical challenge introduced by the interactive mode is having to manage updating a mutable report tree as testcases are run in a consistent and thread-safe manner.
When testplan is running interactively, it can be thought of as separated into two main software layers:
- The back-end: a multi-threaded python process, running an HTTP server which presents a REST API for reading and updating the report state.
- The front-end: a web app running in the browser, powered by React JS. In most cases, only one client is expected to connect to the back-end at a time - however, the interactive mode has been architected to allow multiple clients to connect without conflicting.
REST is a very popular API architecture used widely in modern web services. REST means “REpresentational State Transfer” - which, in short, means it is all about managing the transfer of state and not actions. Typically, actions will be triggered as side-effects of state updates. It is very useful to focus on the management of mutable state when dealing with applications running in more than one location. For more information on REST, I recommend reading this article: https://www.vinaysahni.com/best-practices-for-a-pragmatic-restful-api
The Golden Rule underlying the design of the interactive mode is that the back-end is the sole owner of the report state. The front-end will read the report state by making HTTP GET requests, storing a copy locally, and it will request to update the report state on the backend by making HTTP PUT requests. However, it is at the discretion of the back-end to accept or deny update requests. This means the back-end is the sole decider of what happens if the front-end mistakenly sends multiple conflicting updates, or if multiple clients are connected to the same back-end at once. As a corollary of this Golden Rule, the front-end should not update its own copy of the report state unless told to do so by the back-end, through the content of a GET or PUT response.
Interactive back-end design¶
In python-land, the interactive mode is implemented through a few key classes:
TestRunnerIHandleris the overall manager when running interactively. It owns the report tree, which is initialised to an empty skeleton containing testcases but no results. The
TestRunnerIHandlerhandles running individual testcases and merges their results into its report tree when they complete. It owns a thread pool to allow multiple tests to be run (or queued for running) asynchonously. To be thread-safe, a report_mutex is provided by this class which must be held when either reading or mutating any part of the report tree.
TestRunnerHTTPHandlerdefines the REST API and owns an HTTP server which is used to serve the API as well as the static HTML/JS/CSS etc. files required for the front-end web app. The
TestRunnerHTTPHandlerwill call methods on its owning
TestRunnerIHandlerinstance when required to perform actions such as running tests or starting test resources, as a side-effect from accepting certain state update requests (e.g. when updating the status of a testcase to RUNNING, it will trigger that testcase to actually run). It handles all validation of update requests.
Testis the base class all Tests inherit from, and defines abstract methods required for a Test sub-class to be compatible with the interactive mode. Unlike batch mode, where a Test runner will run a pre-defined list of testcases and return a single report sub-tree, for interactive mode a Test runner needs to run testcases iteratively. As mentioned above, the
TestRunnerIHandlerinstance owns the main report tree, so individual Test runners should not directly mutate any part of the report tree - instead, they should yield individual testcase reports as the testcases are run, and let the
TestRunnerIHandlerhandle merging those testcase reports into the main report tree. Currently
MultiTestis the only Test sub-class which correctly implements all methods required to run interactively.
Each of the classes above has unit-tests to cover their respective functionalities in isolation. In addition, there is a functional test (tests/functional/testplan/runnable/interactive/test_api.py) which spins up an interactive testplan with real testcases and tests running tests and reading their results by sending HTTP requests into the REST API.
Interactive front-end design¶
The front-end is actually the exact same web-app used to render test results from the batch mode. The single web-app uses react-router to distinguish between the URLs used for batch or interactive modes and tweak its behaviour in each case. It would have been possible to create a completely separate package for the interactive web-app and extract the common code into a library, however this would significantly increase the complexity of developing and building both UIs so this hybrid approach was chosen instead.
All web-app code can be found under testplan/web_ui/testing/src. The key
component for interactive mode is the
InteractiveReport component. It owns
a copy of the report tree (though as noted above, ultimately the back-end
is the master of the report tree), and handles the necessary API requests to
keep the report state in sync and make API update requests to trigger tests to
run, test environments to start/stop etc.
Currently, the front-end uses a simple short-polling method to keep its report state in sync with the back-end. Every second, it polls the back-end for changes. Since refreshing the report state entirely every second would quickly become untenable as the report grows in size, the back-end does not return the entire report (sub-)tree for each endpoint but rather a “shallow” copy. A shallow copy includes all data associated with that node in the tree, but instead of directly embedding its child entries, only the UIDs of each child is included. That way, multiple API requests are required to query the entire report tree. Further, each node in the report tree has a hash value which can be used to check if it or any of its children have been modified. Therefore, the report tree is updated recursively with the hash value used to short-circuit when there are no modifications down a given branch.
There are many alternative strategies which could have been used to keep the report in sync with the backend. Websockets, Server Sent Events (SSEs) or HTTP long-polling could allow the backend to notify the front-end when some part of the report tree is updated. These techniques would allow the UI to update more quicker and in a more efficient manner than simple short-polling allows, however they would add complexity and need to be carefully designed to not allow the front-end state to become unsynchronised, or allow either back- or front-end to be overwhelmed with pushing notifications when many updates are available at once. Right now, the responsiveness of the UI using simple short-polling is not amazing but (to my mind) “good enough”. We may want to revisit this area when dealing with larger reports or when we wish to polish the UI to be more than simply “good enough”.
The navigation-related UI components are significantly modified for use in the
interactive mode, in order to accomodate the extra buttons to trigger tests to
run or to toggle the state of test environments. The modified components can
be found by searching for names beginning with
The UI code was written using React (JSX), it is highly recommended to first read through the React documentation, in particular the main concepts.
Each React component should have its own file. This file should contain everything the component uses to render the final HTML (JSX code + CSS). We have used Aphrodite to keep the CSS inside the JS file. This improves readability, everything you need to know about a single component is in the one file (save for common utilities or defaults). Each component should be as general as possible to allow it to be reused. We should also strive for simple and small components to enhance readability.
Each component should define its PropTypes. This allows us to typecheck whilst developing & testing, it won’t cause issues in production. This could be extended in future to also work for state.
Utilities & defaults¶
The components and utility functions shouldn’t have hardcoded values, these should be placed in the common defaults file and imported when needed.
Currently the UI code has docstrings on every:
- React component
- Prop type for a component
- Non React function within a component
- Utility function
Each docstring has a description, list of parameters, the return of the function and whether the object is public or private. The docstrings should ideally be updated when the code is changed.
For the directory structure, again only consistency matters:
- Directories are written in upper camel case (e.g. AssertionPane).
- Utility files are written in lower camel case (e.g. basicAssertionUtils.js).
- Component files are written in upper camel case (e.g. BasicAssertion.js).
Each component should have a corresponding test file under a
directory. We are using enzyme and jest for testing. Enzyme allows us to shallow
mount the components, better for unit testing. Jest is a good framework for
component. The snapshot tests are used to quickly check the general HTML layout
of the component is correct. We can then alter the props and check specific
components have changed with other tests. This keeps the tests more readable,
you can see what is meant to have changed easier when reading the tests.
Currently only unit test in future we may want to do functional tests etc.