Testing Your NND App

Testing Your NND App

Local Testing

For NND Apps created in Python, we recommend using pytest to locally test the code that you have written.  Follow this link for a general overview on how to install and use pytest.  In this article, we will discuss a few suggestions specific to using pytest with the NND App structure.  As mentioned in the Creating Your First NND App knowledge base article, a file called test_executable.py is automatically generated under a folder calls tests within your NND App structure.  This article assumes that you will be adding your test code to this file.

Action Functions versus Helper Functions

The sample code that is automatically generated when you create a new NND App demonstrates a single function called hello_world that runs when the Hello World action is selected in your NND App.  We will refer to this type of function as an Action Function.  Other functions that are not directly tied to actions can also be defined.  These functions are called from within the Action Function code or from other functions of the same nature.  We will refer to these functions as Helper Functions.  Both Action Functions and Helper Functions can be tested with pytest, but the steps to do so differ slightly. 

Importing and Calling Functions

Each function that you want to test in your NND App has to be imported from ..executable.  This example assumes that your NND App code exists in a file called executable.py in a folder one level above the tests folder where your test_executable.py file is.  This is an example of importing an Action Function called hello_world and a Helper function called hello_helper:
      from ..executable import hello_world, hello_helper
Each function with a name that begins with the word test that you define in test_executable.py represents a test to perform when you run pytest.  It is a common convention to include the name of your NND App and the function you are testing in your test function name along with an indicator of what you are testing.  This allows you to easily sort out results when running several test functions at once.  For example, this code will test the hello_world Action Function with a specific name specified in an NND App named Tutorial:
      def test_tutorial_hello_world_with_name():
            args = {"repeat": 3, "name": "Joe"}
            api_calls, results = hello_world(**args)
            assert results == (args["repeat"], f"Hello Joe")
The test above simulates a Nominode Task passing the parameter repeat with a value of 3 and the parameter name with a value of Joe to your NND App.  These parameters and values are stored in a dictionary assigned to a variable named args.  The args variable is unpacked using the ** operator when it is passed in to the hello_world Action Function.  Every Action Function returns a list of Nominode API calls made along with any other return values that it generates.  In the example above, the variable api_calls captures this list and the variable results captures the returned values from the function.  More details about the Nominode API calls later.  The hello_world function that is automatically generated returns two values, a integer reflecting the number of lines printed and a string reflecting what was printed on each line.  The assert line tests if the result values received from the hello_world function match the expected values, forming the basis of the test.

Helper Function Testing

There are two main differences when testing a Helper Function instead of an Action Function.  First, parameter values can be passed directly into the function.  There is no need to build and unpack a dictionary for them.  Second, the Helper Function does not return Nominode API calls, so there is no need for an extra variable to capture them.  Consider the following Helper Function named hello_helper that returns a proper casing of the string passed into it:
      def hello_helper(name):
            return name.title()
The hello_helper function could be tested with a function like this:
      def test_tutorial_hello_helper_with_joe():
            assert hello_helper("joe") == "Joe"

Nominode API Calls

Whenever a function that triggers a Nominode update is called from an Action Function, a Nominode API endpoint is contacted and data is passed to it.  This activity is recorded as a list of tuples in the first return value from the Action Function.  Each tuple corresponds to a single API endpoint call and consists of a string for the endpoint contacted and a dictionary of the parameters and values passed.  Functions that generate Task Execution Log updates and Task Progress updates are examples of functions that generate Nominode API Calls.  For example, if the Hello World function prints to the Task Execution Log two times, the information captured will look like this:
[
	(
		'/execution/log/TEST_UUID', 
		{
			'name': 'engine.hello_world', 
			'msg': 'Hello Joe', 
			'args': [], 
			'levelname': 'INFO', 
			'levelno': 20, 
			'pathname': 'C:\\NominodeApps\\tutorial\\pkg\\executable.py', 
			'filename': 'executable.py', 
			'module': 'executable', 
			'exc_info': None, 
			'exc_text': None, 
			'stack_info': None, 
			'lineno': 46, 
			'funcName': 'hello_world', 
			'created': 1598043092.0867364, 
			'msecs': 86.73644065856934, 
			'relativeCreated': 566.1354064941406, 
			'thread': 5904, 
			'threadName': 'MainThread', 
			'processName': 'MainProcess', 
			'process': 204, 
			'message': 'Hello Joe', 
			'execution_uuid': 'TEST_UUID', 
			'log_version': '0.1.0'
		 }
	), 
	(
		'/execution/log/TEST_UUID', 
		{
			'name': 'engine.hello_world', 
			'msg': 'Hello Joe', 
			'args': [], 
			'levelname': 'INFO', 
			'levelno': 20, 
			'pathname': 'C:\\NominodeApps\\tutorial\\pkg\\executable.py', 
			'filename': 'executable.py', 
			'module': 'executable', 
			'exc_info': None, 
			'exc_text': None, 
			'stack_info': None, 
			'lineno': 46, 
			'funcName': 'hello_world', 
			'created': 1598043092.08775, 
			'msecs': 87.74995803833008, 
			'relativeCreated': 567.1489238739014, 
			'thread': 5904, 
			'threadName': 'MainThread', 
			'processName': 'MainProcess', 
			'process': 204, 
			'message': 'Hello Joe', 
			'execution_uuid': 'TEST_UUID', 
			'log_version': '0.1.0'
		}
	)
]
The value TEST_UUID used in the API endpoint string and for the execution_uuid parameter value in the example above is a fake UUID value because the Action Function is being executed locally and not in the context of an actual Nominode Task.  When executed locally, API endpoint calls are simulated.  No actual calls are made to any Nominode.

pytest Switches

The pytest executable has several switches to control the the tests that it executes and the output that it creates.  Three switches in particular that are useful when testing your NND App are the -k-s and --log-cli-level switches.  The -k switch is used to filter which test functions are executed.  Only functions with names that contain the string specified after the -k switch are executed when pytest runs.  Using it along with the function naming suggestions previously mentioned is an effective way to isolate your testing to just the test functions written for a certain NND App function.  The -s switch adds the output from any print() function calls to the normal output of the pytest command.  Printing out variable values, such as the value of the api_calls variable in the example above, can be an effective troubleshooting step.  Using the --log-cli-level switch with the value INFO after it adds the output from any logger.info() function calls to the normal output of the pytest command.  This allows you simulate what a portion of the Task Execution Log would look like for a Task executing your NND App.

                  
    • Related Articles

    • Creating Your First NND App

      In this tutorial, we will be examining the contents of the template NND App generated from the SDK and making it available to Tasks on your Nominode. Prerequisites Miniconda 4.8.3 (or newer, Python 3.7 version*) (*Python 3.7.4 or newer is required.  ...
    • Adding Your NND App to Our Store

      When you deploy an NND App that you have created, it is immediately available to be used on all of the Nominodes within your NND organization.  However, if you want to share your NND App with others outside of your organization, either for free or ...
    • Adding a Connection to Your NND App

      In this tutorial, we will discuss how to add a Connection parameter to the template NND App generated from the SDK and how to use it to connect to Slack.  More information about creating Connections is available in the Managing Connections on a ...
    • Separating UI Code in Your NND App

      This article explains a best practice for moving different portions of your NND App code into different files.  Specifically it focuses on moving the portion of the code that controls the user interface to a different file, but the techniques ...
    • Using an External Process in Your NND App

      This article explains how to leverage an external process from your NND App.  Specifically it discusses how to use the Scrapy python package.  It is assumed that the reader is already familiar with the information in the Creating Your First NND App ...