This is an old revision of the document!
Plugin Testing Tutorial
Once you have developed a functioning plugin, it's a good idea to add automated methods to test your code before publishing updates. Code testing is considered a best practice, and Python provides built-in tools to help facilitate testing. There are many ways to formulate your tests, and this guide demonstrates only one way to do it. The approach described below has advantages within the Indigo plugin framework because it allows direct access to the IOM (Indigo Object Model). It is extremely important to become familiar with the IOM and Indigo plugin development before attempting to tackle this guide.
Python Unit Testing
The main Python library for code testing is called unittest. There are lots of examples of unit testing online and it's probably a good idea to familiarize yourself with the unittest library before going any further with this guide. The main documentation for the unittest library is available at https://docs.python.org/3/library/unittest.html and is a great place to start learning about unit tests. Those who are familiar with unit testing in Python can skip ahead.
Unit Testing Indigo Plugins
Because Indigo runs plugins in a separate thread (to avoid having a misbehaving plugin bring down the whole system), plugin unit tests have to be constructed a little bit differently. For example, while some tests can be run in the “traditional” way, tests that need access to the IOM can't be run from within an IDE or from the command line without using the Indigo Integration API (which doesn't expose the full suite of IOM commands). However, it is possible to construct tests that do allow for full IOM access through the plugin itself.
Testing Elements
The following example relies on several testing elements:
- The
unittestlibrary - this is a standard Python library and should be already available. - The
python-dotenvlibrary - used for creating and managing Python environment variables. - An IDE that supports unit testing - which not required, having an IDE that supports unit testing can be very helpful.
Testing Structure
As mentioned above, this is only one way to accomplish plugin unit testing within the Indigo environment. It's meant to be simple and straightforward enough to get started, but allows lots of room for customization to different plugin frameworks. This example is written with references to PyCharm, but other IDEs should work equally well.
Environment Variables
While optional, it is a good idea to create an environment variable framework that allows you to create and make references to elements of your development environment. Once you have installed python-dotenv, create a .ENV file at the Server Plugin level of your plugin (or other location that Indigo's environment path search will find it). It is a plain text file. The advantage of using an environment file is that it is a great place to store references that are unique to your system and – for shared development – each developer can have their own individual environment. IMPORTANT! Remember to add the .ENV file to your .gitignore list.
.ENV file
# The .ENV file can contain comments SECRET_CODE=zm76%g215^8sdhe BASE_URL=https://localhost:8176/v2/api/ ACTION_GROUP_ID=12345678 ACTION_GROUP_FOLDER_ID=12345678
Folder Structure
While optional, it's probably best to organize your test files and keep them separate from your main plugin files. For the purposes of this tutorial, we will store them in a Tests subfolder of the Server Plugin folder. For example,
../Server Plugin
|_ Tests
|_ __init__.py
|_ test_my_plugin.py
|_ ...
Of course, you can put them anywhere that your plugin can see them. Depending on your development environment, you may also want to add the Tests folder to your .gitignore file to reduce the footprint of your published plugin.
Main Plugin
You'll need to add a few things to your plugin to support this approach.
- A Plugin Action Item - add a plugin action to your Actions.xml file that will be used to run the tests. It is recommended that you hide the action so it's not visible to users.
<Action id="my_tests" uiPath="hidden"> <Name>Run All Tests</Name> <CallbackMethod>my_tests</CallbackMethod> </Action>
- A Callback Method - create a callback method in your plugin (for example,
my_tests) that will be used to run your tests.
def my_tests(self, action: indigo.PluginAction = None) -> None:
from Tests import test_my_plugin # your unit test file(s)
tests = test_my_plugin.TestPlugin()
def process_test_result(result, name):
if result[0] is True:
self.logger.info(f"{name} tests passed.")
else:
self.logger.info(f"{result[1]}")
# ===================================== Test Execute Action =====================================
test = tests.test_plugin_action(self)
process_test_result(test, "Execute Action")
It's common practice with unit testing to have your tests run silently unless there is a problem; however, this example writes a short note to the Indigo Events log for demonstration purposes. This is of course optional and you can make your tests as silent or verbose as you want.
With these two items created, you have access to the IOM commands you'll need to test your plugin.
Test File Structure
This is where the bulk of your testing code will reside. You could add your tests directly to your plugin but, as mentioned above, it offers a way to segregate your testing code from your published plugin to reduce its footprint by adding them to your .gitignore file. There is, of course, wide latitude (and debate) on how to structure tests and this tutorial tries to evade all that and provide a simple example just to get you started.
- an init() file - to make your Tests available for module level imports.
- one or more test files - that will contain your testing code.
Test File Structure
The test_my_plugin.py file contains your testing code. If you're familiar with unit testing (which you should be if you're this deep in the tutorial), the structure of these tests can be as complex as you like. A simple example:
import dotenv
import indigo # noqa
import logging
import os
from unittest import TestCase
dotenv.load_dotenv()
LOGGER = logging.getLogger("Plugin")
ACTION_GROUP_FOLDER = int(os.getenv("ACTION_GROUP_FOLDER"))
ACTION_GROUP_EXECUTE = int(os.getenv("ACTION_GROUP_EXECUTE"))
test_case = TestCase()
class TestPlugin(TestCase):
def __init__(self):
super().__init__()
@staticmethod
def test_plugin_action(plugin):
"""
Run the plugin action to ensure it executes successfully.
"""
try:
result = indigo.actionGroup.execute(ACTION_GROUP_EXECUTE)
test_case.assertIsNone(result, "Action group execute didn't return None")
return True, None
except AssertionError as error:
return False, error
# More tests and more test methods as needed.
It's not necessary to write every conceivable test at the outset. Instead, focus on the key functions of your plugin such as creating devices, testing URL calls, testing JSON parsing and so on. It's also a good idea to run some tests that will exercise your plugin's ability to respond to adverse conditions such as malformed payloads. When writing unit tests, you may even find bugs in your code that you didn't know were there!
Once you have your core testing framework established and functional, it's a good idea to get in the habit of adding new tests over time as plugin errors are identified. For example, if a user encounters a traceback error–in addition to fixing the source of the problem–add a new unit test that will help you catch future errors before any other users encounter them.