Differences
This shows you the differences between two versions of the page.
| Both sides previous revision Previous revision Next revision | Previous revision | ||
| indigo_2024.1_documentation:plugin_testing_tutorial [2024/10/18 13:50] – [Environment Variables] davel17 | indigo_2024.1_documentation:plugin_testing_tutorial [2024/10/21 18:51] (current) – [Folder Structure] davel17 | ||
|---|---|---|---|
| Line 1: | Line 1: | ||
| - | ====== Plugin Testing | + | ====== Plugin Testing |
| - | Once you have developed a functioning plugin, it's a good idea to add automated methods to test your code before publishing updates. | + | Once you' |
| ===== Python Unit Testing ===== | ===== Python Unit Testing ===== | ||
| Line 6: | Line 6: | ||
| ===== Unit Testing Indigo Plugins ===== | ===== 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 " | + | Because Indigo runs plugins in a separate thread (to avoid having a misbehaving plugin bring down the whole system), plugin unit tests that need access to the IOM have to be constructed a little bit differently. For example, while some tests can be run in the " |
| ==== Testing Elements ==== | ==== Testing Elements ==== | ||
| Line 12: | Line 12: | ||
| - The ''// | - The ''// | ||
| - The ''// | - The ''// | ||
| - | - An IDE that supports unit testing - which not required, having an IDE that supports unit testing can be very helpful. | + | - An IDE that supports unit testing - while not required, having an IDE that supports unit testing can be very helpful. |
| === Testing Structure === | === Testing Structure === | ||
| Line 18: | Line 18: | ||
| === Environment Variables === | === 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 ''// | + | 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 ''// |
| .ENV file | .ENV file | ||
| Line 28: | Line 28: | ||
| ACTION_GROUP_FOLDER_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 guide, we will store them in a ''// | ||
| + | |||
| + | < | ||
| + | ../Server Plugin | ||
| + | |_ Tests | ||
| + | |_ __init__.py | ||
| + | |_ test_my_plugin.py | ||
| + | |_ ... | ||
| + | </ | ||
| + | Of course, you can put them anywhere your plugin can see them. Depending on your development environment, | ||
| + | |||
| + | === Main Plugin === | ||
| + | You'll need to add a few things to your plugin to support this testing 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=" | ||
| + | < | ||
| + | < | ||
| + | </ | ||
| + | </ | ||
| + | * A Callback Method - create a callback method in your plugin (for example, ''// | ||
| + | < | ||
| + | def my_tests(self, | ||
| + | from Tests import test_my_plugin | ||
| + | tests = test_my_plugin.TestPlugin() | ||
| + | |||
| + | def process_test_result(result, | ||
| + | if result[0] is True: | ||
| + | self.logger.info(f" | ||
| + | else: | ||
| + | self.logger.info(f" | ||
| + | |||
| + | # ===================================== Test Execute Action ===================================== | ||
| + | test = tests.test_plugin_action(self) | ||
| + | process_test_result(test, | ||
| + | </ | ||
| + | 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 ''// | ||
| + | |||
| + | * 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 ''// | ||
| + | |||
| + | < | ||
| + | import dotenv | ||
| + | import indigo | ||
| + | import logging | ||
| + | import os | ||
| + | from unittest import TestCase | ||
| + | |||
| + | dotenv.load_dotenv() | ||
| + | LOGGER = logging.getLogger(" | ||
| + | ACTION_GROUP_FOLDER = int(os.getenv(" | ||
| + | ACTION_GROUP_EXECUTE = int(os.getenv(" | ||
| + | |||
| + | 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, | ||
| + | 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' | ||
| + | |||
| + | 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. | ||
| + | |||
| + | === Running Your Tests === | ||
| + | Once you have your tests written, you'll need a way to run them. The easiest way is to create an Action item in the Indigo UI to call your (hidden) testing action. This can easily be done with a short embedded script: | ||
| + | |||
| + | < | ||
| + | plugin_id = " | ||
| + | plugin = indigo.server.getPlugin(plugin_id) | ||
| + | |||
| + | try: | ||
| + | if indigo.PluginInfo.isRunning(plugin): | ||
| + | plugin.executeAction(" | ||
| + | else: | ||
| + | indigo.server.log(" | ||
| + | except Exception as error: | ||
| + | indigo.server.log(f" | ||
| + | </ | ||
| + | |||
| + | That's all you need to run tests that access the IOM. If all went according to plan, you should see something like: | ||
| + | < | ||
| + | | ||
| + | My Indigo Plugin | ||
| + | My Indigo Plugin | ||
| + | My Indigo Plugin | ||
| + | ... | ||
| + | </ | ||
| + | === Even More Tests === | ||
| + | You can, of course, build tests that run independent of the IOM such as: | ||
| + | * testing the structure of supporting files (like a constants file), or | ||
| + | * using Indigo' | ||
| + | |||
| + | These approaches are a bit more " | ||