Source code for extools.test

from datetime import datetime
from contextlib import contextmanager
from extools.errors import ExToolsError
from extools.message import ExMessages

[docs]class ExTestError(ExToolsError): """Raised by failed test cases.""" pass
[docs]def asserts(method): def wrapper(self, *args, **kw): self.results[self._current_test]['assertions'] += 1 return method(self, *args, **kw) return wrapper
[docs]class ExTestCase(): """A self running test case class for Extender scripts. ExTestCase can be used to test code in the Extender environment. Test cases will run with access to the current company. Write your tests against the sample data in SAMINC, setup anything else you need on the fly, to make it easy to build a repeatable test environment. :param log_level: level of the built-in logger. :type log_level: int To create your own tests: 1. subclass ExTestCase 2. define the ``.setup()`` and ``.teardown`` methods 3. create as many methods starting with ``test_`` as you'd like 4. run your test suite by creating an instance and calling ``.run()`` Run all the tests in a project using the included ExTestRunner module. Let's say you want to test your set order decription custom function, which sets the order decription to the customer number. .. code-block:: python from extools.view import exview def set_description_to_customer_number(ordnumber): '''Set the order description to the customer number. :param ordnumber: the order number to update. :type ordnumber: str :rtype: None :raises: ExViewError ''' with exview("OE0500", seek_to={"ORDNUMBER": ordnumber}) as exv: exv.update(DESC=exv.get("CUSTOMER")) Using the :py:meth:`extools.view.exview` context manager makes opening, closing, and seeking the view easy. To test it, we will need a record in the SAMINC database that we can change the Description on. The first, ORD000000000001, seems to fit the bill. .. code-block:: python from extools.test import ExTestCase from mymodule import set_description_to_customer_number class MyTest(ExTestCase): # Make the order to work on a constant ORDER_NUMBER = "ORD000000000001" ORDER_VIEW = "OE0500" def setup(self): # Make sure the field isn't already set to the customer number with exview(ORDER_VIEW, seek_to={"ORDNUMBER": ORDER_NUMBER}) as exv: if exv.get("DESC") == exv.get("CUSTOMER"): exv.update(DESC="Description") # The test method must start with ``test_`` to be auto-detected. def test_set_description_to_customer_number(self): # Use the built-in assertions to check the pre- with exview(ORDER_VIEW, seek_to={"ORDNUMBER": ORDER_NUMBER}) as exv: self.assertTrue(not exv.get("CUSTOMER") == exv.get("DESC")) set_description_to_customer_number(ORDER_NUMBER) #and post conditions with exview(ORDER_VIEW, seek_to={"ORDNUMBER": ORDER_NUMBER}) as exv: self.assertTrue(exv.get("CUSTOMER") == exv.get("DESC")) def main(): # To run your tests, instantiate the class and run it! mt = MyTest() mt.run() """ # Try to set rotating indexes to avoid stepping on yourself. INDEX_MAX = 99 _index = 9 def __init__(self, log_level=ExMessages.INFO): self.name = self.__class__.__name__ self.exm = ExMessages(self.name, log_level) self.index = self.generate_index() # { 'test_name': { 'result': True/False, 'assertions': 0 }, ... } self.results = {} self._current_test = None
[docs] def generate_index(self): """Get the next available view index and increment the counter.""" # Avoid the first 10. self._index = (self._index + 1) % self.INDEX_MAX if self._index < 10: self._index = 10 return self._index
[docs] @asserts def assert_true(self, obj): """Assert that something is truthy.""" if not obj: raise ExTestError("{} is not true".format(obj))
[docs] @asserts def assert_equal(self, obj1, obj2): """Assert that two things are equal.""" if obj1 != obj2: raise ExTestError("'{}' ({}) is not equal to '{}' ({})".format( obj1, type(obj1), obj2, type(obj2)))
[docs] @asserts @contextmanager def assert_raises(self, exception): """Assert that a particular block raises a specific exception. .. code-block:: python import mything from mything.errors import MyAwesomeError ... class MyThingTestCase(ExTestCase): ... def test_my_thing(): ... with self.assertRaises(MyAwesomeError): my_thing.do_the_awesome_stuff() """ try: yield except exception: return True except Exception as e: raise ExTestError("{} not raised".format(exception), trigger_exc=e) else: raise ExTestError("{} not raised".format(exception))
[docs] def run(self, with_transaction=True): """Run all the tests in the class and provide a report.""" passed = [] failed = [] try: self.setup_class() for meth in dir(self): if meth.startswith('test_'): self.exm.debug("Running {}".format(meth)) self._current_test = meth self.results[meth] = { 'result': False, 'assertions': 0 } try: self.setup() # with self._db_transaction(): r = getattr(self, meth)() self.results[meth]['result'] = True self.results[meth]['return'] = r passed.append(meth) except Exception as e: failed.append(meth) self.exm.error("{} failed: {}".format(meth, e), exc_info=True) finally: self.teardown() finally: self.teardown_class() self.exm.info("Failing tests ({}):\n{}\nPassing Tests({}):\n{}".format( len(failed), ", ".join(failed), len(passed), ", ".join(passed))) s = "" for (test, data) in self.results.items(): s += "{} ({} assertions): {}\n".format(test, data['assertions'], data["result"], ) self.exm.info(s)
def _db_transaction(self, transaction_name=None): """This won't work because the transaction happens on a per-connection basis. And I can't get to the connection pool, deep in the bowels of sage.""" if not transaction_name: transaction_name = datetime.now().strftime( "%Y%m%d%H%M%S-{}".format(__name__)) try: with exsql() as exs: exs.query("BEGIN TRANSACTION {}".format(transaction_name)) yield exs finally: try: exs.query("ROLLBACK TRANSACTION {}".format(transaction_name)) except: self.exm.error("Failed to rollback {}. Database state may " "be inconsistent.".format(transaction_name)) try: exs.close() except: pass # Do something useful
[docs] def setup(self): """Steps that are run before every test.""" pass
[docs] def teardown(self): """Steps that are run after every test.""" pass
[docs] def setup_class(self): """Steps that are run once before the test suite starts.""" pass
[docs] def teardown_class(self): """Steps that are run once after the test suite ends.""" pass