Unit testing should be used to test small units of code in an isolated and deterministic fashion. Unit tests should avoid performing communication with external APIs and should prefer to use mocking. Testing actual interaction with external APIs should be performed via Test Playbooks. Unit testing is currently supported for Python and PowerShell (no JS). This doc outlines Python setup. For PowerShell see here.
In order to work with unit testing, the integration or automation script needs to be developed in package (directory) structure, where the yml file is separated from the python file and resides in its own directory.
To run locally the unit tests we want to setup a virtual environment with all required dependencies (both runtime and development). To achieve this we use Pipenv.
- Install pipenv: Follow the instructions.
- Copy base Pipenv files: Copy the base Pipfile and Pipfile.lock files to the target package directory from: demisto_sdk/commands/lint/resources.
- Install additional runtime dependencies: using:
pipenv install <dependency>. For example:
pipenv install ldap3
- Sync Pipenv: (including dev dependencies) by running:
pipenv sync --dev
- Enable Virtual Env: To enable the Pipenv virtual env in the shell run:
pipenv shell. To exit the virtual env simply run:
You should now have a managed virtual environment to run unit tests locally.
We recommend using PyCharm with the Cortex XSOAR Plugin. This is optional and you can also run/debug unit tests with other ides (such as VS Code), but only PyCharm currently has a dedicated plugin, which can manage the yml file via the UI and also provide remote execution. See: https://plugins.jetbrains.com/plugin/12093-demisto-add-on-for-pycharm. Setup:
- Install the Cortex XSOAR Plugin: Install with-in PyCharm by navigating to
Preferences.. -> Plugins. Or download and install from here
- Open Pycharm: Open PyCharm where the root folder is the folder you wish to develop within.
- Choose Interpreter: Choose the Pipenv interpreter (with all dependencies we setup in the previous step). See: https://www.jetbrains.com/help/pycharm/configuring-python-interpreter.html
- Enable PyTest: We run our unit tests with
pytest. See the following on how to enable PyTest: https://www.jetbrains.com/help/pycharm/pytest.html
main in Integration/Automation#
When writing unit tests you will import the Integration/Automation file in order to test specific files. Thus, there is need to make sure that the file is written in such a way that when importing it will not execute. This can be done with a simple
main function which is called depending on how the file was executed. When the Integration/Automation script is called by Cortex XSOAR it will have the property
__name__ set to either
builtins depending upon the python version. Adding the following code will ensure the script is not run when imported by the unit tests:
Unit tests should be written in a separate Python file named:
<your_choice>_test.py. Within the unit test file, each unit test function should be named:
test_<your name>. More information on writing unit tests and their format is available at the PyTest Docs. Good place to see example unit tests: Proofpoint TAP v2 integration
We use pytest-mock for mocking.
pytest-mock is enabled by default and installed in the base environment mentioned above. To use a
mocker object, simply pass it as a parameter to your test function. The
mocker can then be used to mock both the demisto object and also external APIs. An example of using a
mocker object is available here.
To run your unit tests from the command line simply run from within the virtual env:
It is also possible to run from outside the virtual env by running:
Open the unit test file within PyCharm. You will see a green arrow next to each unit test function. When pressing the arrow you will get a prompt to either Debug or Run the unit test. Set breakpoints as needed and Debug the test.
Sample clip of debugging in PyCharm:
CircleCI build will run the unit tests within the docker image the Integration/Automation will run with. To test and
run locally the same way CircleCI runs the tests, run the
demisto-sdk lint command
Run the script with
-h to see command line options:
Most functions we write have several edge cases. When writing a unit test for this type of function all edge cases need to be tested. For example let's examine the following python function:
A naive unit test will be as follows:
The correct way to test this function is using the @pytest.mark.parametrize fixture:
We declare the inputs and outputs in the following format: 'input, output', [(case1_input, case1_output), (case2_input, case2_output), ...] (Note that more than two variables can be delivered)
After declaring the variables and assigning their values, you need to assign the variables to the test function. In the example above we assign the variables 'string' and 'output' to the test function.
To read more on parametrize fixtures, visit: https://docs.pytest.org/en/latest/how-to/parametrize.html
An example of a test using the paramertrize fixture is avialable here.
If a function is raising an exception in some case we want to test the right exception is raised and that the error message is correct. For example, for testing the following function:
We first need to import the raises function from pytest using this line of code:
Then, we test the exception being raised.
If the function raises a ValueError with proper error message, the test will pass.
demisto-sdk lintby default prints out minimal output. If for some reason it is failing and not clear, run the script with
-vfor verbose output.
When running mypy against python 2 code and the file contains non-ascii characters it may fail with an error of the sort:
can't decode file 'ThreatConnect.py': 'ascii' codec can't decode byte 0xe2 in position 47329: ordinal not in range(128).
To find the character use the following python one liner:
python -c "index = 47329; f = open('Integrations/ThreatConnect/ThreatConnect.py'); d = f.read(); print(d[index-20:index+20])"
The script creates a container image which is used to run pytest and pylint. The container image will be named:
devtest<origin-image>-[deps hash]. For example:
devtestdemisto/python:1.3-alpine-1b9f5bee16a24c3f5463e324c1bb075. You can examine the image if needed by simple using docker run. For example:
If you have faced the error
ValueError: unknown locale: UTF-8 when running
demisto-sdk lint, add these lines to your ~/.bash_profile: