Unit Testing Sublime Text Plugins

February - 2013

The software development process "Test Driven Development", or TDD, encourages simple designs and inspires confidence. More importantly, it helps produce higher quality code and acts to prevent bugs cropping up in code when changes are made. I've become a huge fan and have begun implementing it for all my new projects. At least, I was trying to until I started to make a Sublime Text 2 plugin. There is very little out there about the development of Sublime Text plugins in general and almost nothing about how to create them using TDD. Perhaps this is because most people developing plugins have enough experience with Python that it's obvious how to implement. For me, as a newcomer to the language, that's not the case.

The code below represents probably 20 hours of research and hacking to get a working test method. It allows the tests to be run directly on the main plugin object. Hopefully, it will help get folks up to speed without having to burn so much time of their own.

To run the test code, create a directory under ~/Library/Application Support/Sublime Text 2/Packages named PluginUnitTestExample. Three files will be placed in that directory for this example. The first, Default.sublime-commands is used to provide an item in the Command Palette (opened with Cmd + Shift + p). Unlikely that you would use this approach in production, but it makes it easy to see what's going on. The PluginUnitTest.py file represents the actual plugin to be tested. In this case, the test calling code is located directly inside the run method. Again, that wouldn't be advisable in production code, but this is just an example to prove the overall concept. The final file, PluginUnitTestCases.py contains the test class. The part that took the longest to figure out is the __init__ method. It's the key to making the test work. To give due credit, the base for the method was found in this Stack Overflow answer.

Default.sublime-commands


[
    {
        "caption": "Plugin Unit Tests",
        "command": "plugin_unit_test"
    }   
]

PluginUnitTest.py


import sublime
import sublime_plugin
import logging
import unittest
import PluginUnitTestCases

class PluginUnitTestCommand(sublime_plugin.TextCommand):

    def run(self, edit):
        '''The method that's called by Sublime Text'''

        ### Open the console window so you can see the output
        self.view.window().run_command("show_panel", {"panel": "console", "toggle": True})

        ### Define the unit test suite
        suite = unittest.TestSuite()

        ### Add the individual tests. 
        suite.addTest(PluginUnitTestCases.PluginUnitTestCase001("test_a", self))
        suite.addTest(PluginUnitTestCases.PluginUnitTestCase001("test_b", self))

        ### Run the suite
        unittest.TextTestRunner().run(suite)


    def get_test_string(self):
        '''Example method to prove tests can see the plugin object'''

        return "the quick brown fox"

PluginUnitTestCases.py

import unittest
import logging

class PluginUnitTestCase001(unittest.TestCase):

    def __init__(self, testname, testObj):
        '''A version of __init__ that loads the object for testing'''

        super(PluginUnitTestCase001, self).__init__(testname)
        self.testObj = testObj


    def setUp(self):
        '''Setup to be called before every test.'''
    
        logging.info("Running: PluginUnitTestCase001.setUp()")

    def tearDown(self):
        '''Tear down to be called after every test.'''

        logging.info("Running: PluginUnitTestCase001.tearDown()")


    def test_a(self):
        '''The first test method.'''

        logging.info("Running: PluginUnitTestCase001.test_a()")
        self.assertEqual(self.testObj.get_test_string(), 'the quick brown fox')

    def test_b(self):
        '''Another test method.'''
    
        logging.info("Running: PluginUnitTestCase001.test_b()")
        self.assertNotEqual(self.testObj.get_test_string(), 'jumps over')

This code works on my machine which is running Sublime Text 2.0.1 Build 2217 on Mac OS X 10.7.5. Your milage may vary, but hopefully not so much that you can't use this approach.