Help designing unit tests?

How do people write unit tests with Ignition? I have finally, FINALLY, refactored this project and completely decoupled the business logic from the GUI and so the first thing I want to do is write some unit tests so anytime we make a change, we can check all the usual processes and make sure nothing unexpectedly broke, move away from the "production is the QA environment" paradigm we have going on now.

This app is 99% CRUD routes for various db forms. So right now I am writing functions that are testing the creation/update/deleting of records and checking I get the appropriate result.

Here's my first attempt -

def customerTests():
	# Create a new customer, customer name TestCustomer1000
	newCustomer = {...}
	newCreateResult = forms2.Forms.Customer.create(newCustomer)
	print 'creating customer'
	assert newCreateResult.success == True
	print 'passed'
	# Update the new customer to TestCustomer1001
	updateCustomerData = {...}
	# newCreateResult.result is the idx of the newly created one
	customerUpdateResult = forms2.Forms.Customer.update(updateCustomerData, newCustomer, newCreateResult.result)
	print 'updating..'
	assert customerUpdateResult.success == True
	print 'passed'
	# Try to update customer name to existing Customer Name- this should fail
	updateToExistingCustomerName = {...}
	shouldFailUpdate = forms2.Forms.Customer.update(updateToMesser, updateCustomerData, newCreateResult.result)
	print 'trying fail update'
	assert shouldFailUpdate.success == False
	print 'passed'
	# Delete the new customer
	newDeleteResult = forms2.Forms.Customer.delete(newCreateResult.result)
	print 'deleting..'
	assert newDeleteResult.success == True
	print 'passed'

And then I would call this and just make sure no assertion error comes up. I am wondering if this is how most people do it or if there is other better methods for writing unit tests specifically within the context of Ignition.

While this won't make you design better tests, it can be helpful to write them.

click for code
import ast

"""
Basic test helper

Import test in your file: from utils.test import test

Decorate your functions to be tested:
@test
def test_foo():
    foo = "foo"
    bar = get_bar()
    assert foo == bar, "expected {}, got {}".format(foo, bar)

Call the function individually, or use run_tests() to run all the marked functions in modules:
run_tests(module_1, module_b)
"""

class TestFinder(ast.NodeVisitor):
	def visit_Module(self, node):
		self.test_funcs = []
		self.generic_visit(node)

	def visit_FunctionDef(self, node):
		if "test" in (deco.id for deco in node.decorator_list):
			self.test_funcs.append(node.name)
	
	def get_testfuncs(self):
		return self.test_funcs

def test(func):
	"""
	Decorator to mark functions to be tested.
	
	Simply import the decorator (from Tests.utils import test) and add @test before the function
	"""

	def wrapper():
		try:
			func()
		except AssertionError as e:
			message = "failed ({})".format(e)
		else:
			message = "passed"
		return message
	return wrapper


def run_tests(*modules):
	"""
	Run all the functions marked as tests (with the test decorator) in every packages passed as parameter.
	"""
	for m in modules:
		print("{}\ntesting functions in {}...".format("-"*20, m.name))

		tree = ast.parse(m.code)
		f = TestFinder()
		f.visit(tree)
		funcs = f.get_testfuncs()

		if not funcs:
			print("No functions marked for testing found in {}".format(m.name))
		for f in funcs:
			try:
				message = getattr(m, f)()
				print("{:.<40}{}".format(f, message))
			except BaseException as e:
				print(repr(e))

image

Write your tests as simple functions, decorate them with @test, then run run_test(module1, module2, etc) wherever you want (usually the script console though).
I haven't used it in a while, and if I remember correctly there were a few quirks. I may or may not have fixed them.

edit: forgot to format code :X

note: You can use run_tests() to run all the tests in the modules passed as arguments, or just call a function that's decorated with @test directly

5 Likes

Something went wrong with your code formatting there.

What went wrong with the formatting was that there was no formatting :smiley:

3 Likes

I've been trying to find a way for this to run in all the loaded modules in the Project Library without having to type the module names into the test each time I run it. Is there a nice way to do this?

I've tried some things along the lines of

and

but it seemed like there should be an easier way. I'm sure that with enough digging through, you could determine which modules are being loaded through the Project Library, which are being imported, and which are automatically loaded, but this seems like an unnecessary amount of work for something that should be fairly trivial.

Apologies if this should be in a separate thread, but it seemed relevant to this thread and anyone who might stumble on it in the future.

Thanks!

I can't investigate this now, but worst case scenario you could call an external script that runs ls -R or something similar to get the list of scripts filepaths, then process them into a list that can be used by the function. I guess.

You can enumerate the entries in sys.modules, using type() to pick out the ones that are Ignition modules and packages. Recurse into the latter.

1 Like