Local Testing
For Nom Nom 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 Nom Nom App structure. As mentioned in the
Creating Your First Nom Nom 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 Nom Nom App demonstrates a single function called hello_world that runs when the Hello World action is selected in your Nom Nom 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 Nom Nom App has to be imported from ..executable. This example assumes that your Nom Nom 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 Nom Nom 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 Nom Nom 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 Nom Nom 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 Nom Nom 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 Nom Nom App.