Python/Jython Unittest

is it possible to use the standard Python/Jython unittest modules with Ignition? My goal is to [eventually] incorporate these unittests in with Jenkins and potentially then EAM, but right now I’m struggling to get even a trivial example to work inside the script console.

The error message I’m receiving:

Traceback (most recent call last):
  File "<buffer>", line 72, in <module>
  File "C:\Users\<username>\.ignition\cache\<gwname>_8088_443_main\C0\pylib\unittest.py", line 766, in __init__
    self.progName = os.path.basename(argv[0])
IndexError: index out of range: 0

The code I’m attempting to run, pulled from here:

class Person:
    name = []

    def set_name(self, user_name):
        self.name.append(user_name)
        return len(self.name) - 1

    def get_name(self, user_id):
        if user_id >= len(self.name):
            return 'There is no such user'
        else:
            return self.name[user_id]


import unittest


class Test(unittest.TestCase):
    """
    The basic class that inherits unittest.TestCase
    """
    person = Person()  # instantiate the Person Class
    user_id = []  # variable that stores obtained user_id
    user_name = []  # variable that stores person name

    # test case function to check the Person.set_name function
    def test_0_set_name(self):
        print("Start set_name test\n")
        """
        Any method which starts with ``test_`` will considered as a test case.
        """
        for i in range(4):
            # initialize a name
            name = 'name' + str(i)
            # store the name into the list variable
            self.user_name.append(name)
            # get the user id obtained from the function
            user_id = self.person.set_name(name)
            # check if the obtained user id is null or not
            self.assertIsNotNone(user_id)  # null user id will fail the test
            # store the user id to the list
            self.user_id.append(user_id)
        print("user_id length = ", len(self.user_id))
        print(self.user_id)
        print("user_name length = ", len(self.user_name))
        print(self.user_name)
        print("\nFinish set_name test\n")

    # test case function to check the Person.get_name function
    def test_1_get_name(self):
        print("\nStart get_name test\n")
        """
        Any method that starts with ``test_`` will be considered as a test case.
        """
        length = len(self.user_id)  # total number of stored user information
        print("user_id length = ", length)
        print("user_name length = ", len(self.user_name))
        for i in range(6):
            # if i not exceed total length then verify the returned name
            if i < length:
                # if the two name not matches it will fail the test case
                self.assertEqual(self.user_name[i], self.person.get_name(self.user_id[i]))
            else:
                print("Testing for get_name no user test")
                # if length exceeds then check the 'no such user' type message
                self.assertEqual('There is no such user', self.person.get_name(i))
        print("\nFinish get_name test\n")


if __name__ == '__main__':
    # begin the unittest.main()
    unittest.main()
Traceback (most recent call last):
  File "&lt;buffer&gt;", line 72, in &lt;module&gt;
  File "C:\Users\&lt;username&gt;\.ignition\cache\&lt;gwname&gt;_8088_443_main\C0\pylib\unittest.py", line 766, in __init__
    self.progName = os.path.basename(argv[0])
IndexError: index out of range: 0

This (along with if __name__ == '__main__':) implies that the unittest module assumes it’s running directly from the command line. Unfortunately, that’s true of nowhere in Ignition - in all cases, including the script console, the “raw” script you write is passed off to a Java method that compiles and executes the code, passing in particular values for Python’s locals() and globals(). At a guess, the unittest module may work work in a pure Jython environment (which is why it’s included in our distribution of the standard library) but you would probably have to do some significant re-engineering to get it to work within an Ignition environment.

1 Like

Thanks, I appreciate it. From the script console I was able to get an alternative calll working, as I suspected something along the lines of command line/main call.

import unittest

def fib(n):
	""" Calculates the n-th Fibonacci number iteratively 
	
	>>> fib(0)
	0
	>>> fib(1)
	1
	>>> fib(10) 
	55
	>>> fib(40)
	102334155
	>>> 
	
	"""
	a, b = 0, 1
	for i in range(n):
		a, b = b, a + b
	return a


	
class FibonacciTests(unittest.TestCase):
    def test_Dummy(self):
		self.assertEqual([0,3.3,'FOIL',5.01],[0,3.3,'FOIL',5.01])
		
    def test_Calculation(self):
        self.assertEqual(fib(0), 0)
        self.assertEqual(fib(1), 0)
        self.assertEqual(fib(5), 5)
        self.assertEqual(fib(10), 55)
        self.assertEqual(fib(20), 6765)

	
suite = unittest.TestLoader().loadTestsFromTestCase(FibonacciTests)
output = unittest.TextTestRunner(verbosity=2).run(suite)

if output.wasSuccessful():
	print 'Passed tests.'
else:
	number_failed = len(output.failures) + len(output.errors)
	print "Failed "+str(number_failed)+ " test(s)"
1 Like

Hi tim,

When I code that’s big enough to warrant testing, I usually develop it as a separate library. See
https://docs.inductiveautomation.com/display/DOC79/Libraries#Libraries-Importing3rdPartyLibraries

Then it’s possible to use unit testing from the console or in an IDE (though tooling support for Jython 2.5 is quite bad at the moment).

2 Likes

Thanks Sander - that is a good idea. May I pick your brain a bit - how do you decide when code is ‘big enough’? Just trying to get a feel for the dev ops and QA side of Ignition development vs traditional development, and some best practices.

1 Like

If it’s a stand-allone function, and the effects are immediately visible in the client, I usually don’t test it since it’s easy enough to confirm the validity of the function.

With stand-allone, I mean the data comes either from the global scope (tags), or from visible client state, and the function only gets called in a specific context.

When functions get linked together, or use data that’s hard to consult, it usually makes debugging harder, so I put it in a library and write tests for it. To me, it’s not about the single function being small or large, it’s about it being isolated or not.

1 Like