Software:

  • Java (version 1.8 currently)
  • Gradle (version 2.0 or later)
  • CycleServer 4.9+
  • Eclipse (recommended)

Before you go any farther, first go through the installation of eclipse, gradle, and CycleServer.

There is also a CycleCloud Plugin Development video covering some initial setup and plugin development topics.

Python Plugins

Unit Tests

Unit tests for Python code in CycleServer are written using the standard unittest framework. They can be run individually from Eclipse or in bulk from the command line. They are normally run as part of the gradle build command, but they can be skipped with gradle build -x
test
. All unit tests must be in src/test/python. The filename is not important, but by convention is is item_test, where item is the unit being tested. The file itself must contain a class that extends unittest.TestCase, either directly or indirectly:

import unittest

# this can be named anything
class MyTest(unittest.TestCase):
    def test_addition(self):
        assert 2 + 2 == 4

Note: you can have more than one test class in a file if needed.

Since src/test/python is on the path, you cannot name your package with the same name as the code in src/main/python. Typically, if you have python files in src/main/python/foo/bar, then you would have matching tests in src/test/python/foo_test/bar. (Note this also means you must have __init__.py files in src/test/python/foo_test.)

Running Tests

The tests are run as part of the normal gradle build command, and they are run whenever the source changes. You can force the tests to run with:

$ gradle cleanTest test

You can run a single Python test with the following command:

$ gradle test -Dpytest.single=TEST_PATTERN

This is modeled after Gradle’s standard test.single property. Currently the only pattern that is supported is the name of the test class:

$ gradle test -Dpytest.single=CustomTest

This would run every class named “CustomTest” in every Python test file. The pytest.single property (for Python tests) and test.single property (for Java tests) are complementary: if you specify pytest.single and not test.single, no Java tests are run, and vice-versa. If you specify both it will filter both Python and Java tests accordingly.

Writing Test Cases

To test using CycleServer APIs, you have to extend plugintest.TestCase instead. A more realistic test might be the following:

from application import datastore, records
import plugintest

# sample test class
class MyTest(plugintest.TestCase):

def setUp(self):
    plugintest.TestCase.setUp(self)
    datastore.defineType("Host", "Name")

def test(self):
    record = records.create("Host", "machine.example.com")
    record.setString("OpSys", "Linux")
    datastore.save(ad)
    assert datastore.get("Host", "machine.example.com").getAsString("OpSys") == "Linux"

def test2(self):
    # each test sees a clean datastore
    assert datastore.get("Host", "machine.example.com") is None

This test stores data in the datastore and checks it. (A real test would call external code to verify it does the right thing with the data that the test stored.)

It is also possible to supply custom “mock” implementations for plugins to stub out functionality that shouldn’t happen in a unit test (like connecting to a remote service). Suppose you have a plugin that can reconfigure machines remotely using a remote interface implemented in a “machine.control” plugin:

# controller.py
from machine import control
from application import datastore

def configureMatching(opsys):
# configure all the matching machines
    for machine in datastore.find("Host", 'OpSys == "%s"' % opsys):
    control.configure(machine.getAsString("Name"))

Since this is a unit test, we don’t want to involve a remote machine, so we provide a different implementation of machine.control that just stores the names of the machines that were configured:

from application import datastore, records
import plugintest

class MyTest(plugintest.TestCase):

     def test(self):
         datastore.defineType("Host", "Name")

         # store two hosts
         linux = records.create("Host", "host001")
         linux.setString("OpSys", "Linux")
         win = records.create("Host", "host002")
         win.setString("OpSys", "Windows")
         datastore.save([linux, win])

         # define a mock plugin
         control = CustomControl()
         plugintest.registerPlugin("machine.control", control)

         # note: we have to defer importing the controller until now so it sees the custom definition
         import controller
         controller.configureMatching("Linux")

         # only should have done the linux machine
         assert len(control.list) == 1
         assert control.list[0] == "host001"

class CustomControl:
     def __init__(self):
          self.list = []

     def configure(self, hostname):
         self.list.append(hostname)

This test checks that we are properly filtering hosts and sending out the request to configure a machine, without actually involving any other machines.

This can be extended to support multiple tests. This example uses a new instance for each test, but could use a completely separate class.

from application import datastore, records
import plugintest


class MyTest(plugintest.TestCase):

     def setUp(self):
         plugintest.TestCase.setUp(self)
         datastore.defineType("Host", "Name")

         self.mock = CustomControl()
         plugintest.registerPlugin("machine.control", self.mock)

     def test(self):
         import controller

         # store two hosts
         linux = records.create("Host", "host001")
         linux.setString("OpSys", "Linux")
         win = records.create("Host", "host002")
         win.setString("OpSys", "Windows")
         datastore.save([linux, win])

         controller.configureMatching("Linux")

         # only should have done the linux machine
         assert len(self.mock.list) == 1
         assert self.mock.list[0] == "host001"

     def test2(self):
         import controller

         # store two hosts
         linux = records.create("Host", "tux001")
         linux.setString("OpSys", "Linux")
         linux2 = records.create("Host", "tux002")
         linux2.setString("OpSys", "Linux")
         datastore.save([linux, linux2])

         controller.configureMatching("Linux")

         # should have done both machines
         assert len(self.mock.list) == 2
         assert set(self.mock.list) == set(["tux001", "tux002"])

class CustomControl:
     def __init__(self):
         self.list = []

     def configure(self, hostname):
         self.list.append(hostname)

RESTful unit tests

There is special support in CycleServer for testing RESTful plugins. This provides a simple REST client that can be used to directly query the actual RESTful plugin. (The application.restclient can also be used as a real REST client in production.)

For more information on application.restclient see the application.restclient
documentation
. Suppose you have the following simple plugin defined as rest.http_plugin (with WebContent=dynamic in its configuration):

def get(req, res):
    res.write("Hello world")

You could then test it with the following test file:

import plugintest

class MyTest(plugintest.HttpTestCase):
     def setUp(self):
         plugintest.HttpTestCase.setUp(self)
         from rest import http_plugin
         from rest import http_auth_plugin

     def test_get(self):
         response = self.conn.request_get("/rest/http_plugin", headers={'Accept':'text/json'})
         self.assertEqual(200, response.status())
         self.assertEqual("Hello world", response.body())

Note that it extends plugintest.HttpTestCase, which is what sets up the local HTTP server, and that it imports http_plugin in setUp, not at the top of the file. (Importing it directly in the test files would work as well.) This class also provides self.conn, which is an instance of restclient that is pre-instantiated to connect to the port assigned to the HTTP server.