Unit testing BaseDataObject

Unit testing of BaseDataObject is going to be… interesting, as it stands right now. Testing the matches method, a concrete method that depends on an abstract method (to_data_dict), which, in turn depends on the actual data structure (properties) of a derived class, is either not possible or meaningless in the context of the test case class for BaseDataObject itself:

  • In order to test matches, we have to define a non-abstract class with a concrete implementation of to_data_dict, and some actual properties to generate that resulting dict from/with
  • That derived class, unless it also happens to be an actual class needed in the system, has no relevance in the final system's code, so tests there do not assure us that other derived classes won't have issues in matches
  • Even setting the testing of the matches method completely aside, testing save is similarly pointless, for much the same reason  it's a concrete method that depends on methods that are, at the BaseDataObject level, abstract and undefined

Back when BaseArtisan was being implemented, we defined its add_product and remove_product methods as abstract, but still wrote usable concrete implementation code in both, in order to allow derived classes to simply call the parent's implementation. In effect, we required an implementation of both in all derived classes, but provided an implementation that could be called from within the derived class methods. The same sort of approach, applied to the  matches and save methods in BaseDataObject, would essentially enforce testing requirements on each derived concrete class, while still permitting the use of a single implementation until or unless a need arose to override that implementation. It might feel a bit hacky, but there don't appear to be any downsides to that approach:

  • The methods processed in this fashion still have to be implemented in the derived classes.
  • If they need to be overridden for whatever reason, testing policies will still require them to be tested.
  • If they are implemented as nothing more than a call to the parent class method, they will function and testing policy code will still recognize them as local to the derived class. Our testing policy says those are in need of a test method, and that allows test methods to execute against the specific needs and functionality of the derived class.

Testing save doesn't have to take that approach, however. Ultimately, all we're really concerned with as far as that method is concerned is that we can prove that it calls the _create and _update abstract methods and resets the flags. If that proof can be tested and established in the process of testing BaseDataObject, we won't have to test it elsewhere unless the test policy code detects an override of the method. That would, in turn, allow us to avoid having the same test code scattered across all the test cases for all of the final, concrete classes later on, which is a good thing.

Starting the unit tests for the data_objects module is simple enough:

  1. Create a test_data_object.py file in the project's test_hms_core directory
  2. Perform the two name replacements noted in the header comments
  3. Add a reference to it in __init__.py in that same directory
  4. Run the test code and go through the normal iterative test writing process

The reference to the new test module in __init__.py follows the structure that already exists in our unit test module template making a copy of the two lines starting with # import child_module in the existing code, then uncommenting them and changing child_module to the new test module:

#######################################
# Child-module test-cases to execute  #
#######################################

import test_data_objects
LocalSuite.addTests(test_data_objects.LocalSuite._tests)

# import child_module
# LocalSuite.addTests(child_module.LocalSuite._tests)

That addition adds all of the tests in the new test_data_objects module to the tests already present in the top-level __init__.py test module, allowing that top-level test suite to execute the child module tests:

The tests in test_data_objects.py can also be executed independently, yielding the same failure, but without executing all of the other existing tests:

The iterative process for writing unit tests for data_objects.py is no different than the process that was used for writing tests for the base business objects in the previous iteration: run the test module, find a test that's failing, write or modify that test, and re-run until all tests pass. Since BaseDataObject is an abstract class, a throwaway, derived concrete class will be needed to perform some tests against it. With the exception of the value-oriented testing of the oid, createdand modified properties of BaseDataObject, we have established patterns that cover everything else:

  • Iteration over good and bad value lists that are meaningful as values for the member being tested:

    • (Not applicable yet) standard optional text-line values
    • (Not applicable yet) standard required text-line values
    • Boolean (and numeric-equivalent) values
    • (Not applicable yet) non-negative numeric values
  • Verifying property method associations â€“ getter methods in every case so far, and setter and deleter methods where they are expected
  • Verifying getter methods retrieve their underlying storage attribute values
  • Verifying deleter methods reset their underlying storage attribute values as expected
  • Verifying that setter methods enforce type checks and value checks as expected
  • Verifying that initialization methods (__init__) call all of the deleter and setter methods as expected

Those same three properties (oid, createdand modified), apart from not having an established test pattern already defined, share another common characteristic: all three of them will create a value if the property is requested and doesn't already have one (that is, the underlying storage attribute's value is None). That behavior requires some additional testing beyond the normal confirmation that the getter reads the storage attribute that the test methods start with (using test_get_created to illustrate):

def test_get_created(self):
    # Tests the _get_created method of the BaseDataObject class
    test_object = BaseDataObjectDerived()
    expected = 'expected value'
    test_object._created = expected
    actual = test_object.created
    self.assertEquals(actual, expected, 
        '_get_created was expected to return "%s" (%s), but '
        'returned "%s" (%s) instead' % 
        (
            expected, type(expected).__name__,
            actual, type(actual).__name__
        )
    )

Up to this point, the test method is pretty typical of a getter method test it sets an arbitrary value (because what's being tested is whether the getter retrieves the value, nothing more), and verifies that the result is what was set. Next, though, we force the storage attribute's value to None, and verify that the result of the getter method is an object of the appropriate type a datetime in this case:

    test_object._created = None
    self.assertEqual(type(test_object._get_created()), datetime, 
        'BaseDataObject._get_created should return a '
        'datetime value if it's retrieved from an instance '
        'with an underlying None value'
    )

The test method for the property setter method (_set_created in this case) has to account for all of the different type variations that are legitimate for the property â€“ datetime, int, floatand str values alike for _set_created â€“ and set the expected value accordingly based on the input type before calling the method being tested and checking the results:

def test_set_created(self):
    # Tests the _set_created method of the BaseDataObject class
    test_object = BaseDataObjectDerived()
    # - Test all "good" values
    for created in GoodDateTimes:
        if type(created) == datetime:
            expected = created
        elif type(created) in (int, float):
            expected = datetime.fromtimestamp(created)
        elif type(created) == str:
            expected = datetime.strptime(
                created, BaseDataObject._data_time_string
            )
        test_object._set_created(created)
        actual = test_object.created
        self.assertEqual(
            actual, expected, 
            'Setting created to "%s" (%s) should return '
            '"%s" (%s) through the property, but "%s" (%s) '
            'was returned instead' % 
            (
                created, type(created).__name__,
                expected, type(expected).__name__, 
                actual, type(actual).__name__, 
            )
        )
    # - Test all "bad" values
    for created in BadDateTimes:
        try:
            test_object._set_created(created)
            self.fail(
                'BaseDataObject objects should not accept "%s" '
                '(%s) as created values, but it was allowed to '
                'be set' % 
                (created, type(created).__name__)
            )
        except (TypeError, ValueError):
            pass
        except Exception as error:
            self.fail(
                'BaseDataObject objects should raise TypeError '
                'or ValueError if passed a created value of '
                '"%s" (%s), but %s was raised instead:
'
                '    %s' % 
                (
                    created, type(created).__name__, 
                    error.__class__.__name__, error
                )
            )

The deleter method test is structurally the same test process that we've implemented before, though:

def test_del_created(self):
    # Tests the _del_created method of the BaseDataObject class
    test_object = BaseDataObjectDerived()
    test_object._created = 'unexpected value'
    test_object._del_created()
    self.assertEquals(
        test_object._created, None,
        'BaseDataObject._del_created should leave None in the '
        'underlying storage attribute, but "%s" (%s) was '
        'found instead' % 
        (
            test_object._created, 
            type(test_object._created).__name__
        )
    )

The exact same structure, with created changed to modified, tests the underlying methods of the modified property. A very similar structure, changing names (created to oid) and expected types (datetime to UUID), serves as a starting point for the tests of the property methods for the oid property.

Testing _get_oid, then looks like this:

def test_get_oid(self):
    # Tests the _get_oid method of the BaseDataObject class
    test_object = BaseDataObjectDerived()
    expected = 'expected value'
    test_object._oid = expected
    actual = test_object.oid
    self.assertEquals(actual, expected, 
        '_get_oid was expected to return "%s" (%s), but '
        'returned "%s" (%s) instead' % 
        (
            expected, type(expected).__name__,
            actual, type(actual).__name__
        )
    )
    test_object._oid = None
    self.assertEqual(type(test_object.oid), UUID, 
        'BaseDataObject._get_oid should return a UUID value '
        'if it's retrieved from an instance with an '
        'underlying None value'
    )

And testing _set_oid looks like this (note that the type change also has to account for a different expected type and value):

    def test_set_oid(self):
        # Tests the _set_oid method of the BaseDataObject class
        test_object = BaseDataObjectDerived()
        # - Test all "good" values
        for oid in GoodOIDs:
            if type(oid) == UUID:
                expected = oid
            elif type(oid) == str:
                expected = UUID(oid)
            test_object._set_oid(oid)
            actual = test_object.oid
            self.assertEqual(
                actual, expected, 
                'Setting oid to "%s" (%s) should return '
                '"%s" (%s) through the property, but "%s" '
                '(%s) was returned instead.' % 
                (
                    oid, type(oid).__name__, 
                    expected, type(expected).__name__, 
                    actual, type(actual).__name__, 
                )
            )
        # - Test all "bad" values
        for oid in BadOIDs:
            try:
                test_object._set_oid(oid)
                self.fail(
                    'BaseDatObject objects should not accept '
                    '"%s" (%s) as a valid oid, but it was '
                    'allowed to be set' % 
                    (oid, type(oid).__name__)
                )
            except (TypeError, ValueError):
                pass
            except Exception as error:
                self.fail(
                    'BaseDataObject objects should raise TypeError '
                    'or ValueError if passed a value of "%s" (%s) '
                    'as an oid, but %s was raised instead:
'
                    '    %s' % 
                    (
                        oid, type(oid).__name__, 
                        error.__class__.__name__, error
                    )
                )

With all of the data object tests complete (for now), it's a good time to move the class definitions that were living in the package header file (hms_core/__init__.py) into a module file just for them: business_objects.py. While it's purely a namespace organizational concern (since none of the classes themselves are being changed, just where they live in the package), it's one that makes a lot of sense, in the long run. With the move completed, there is a logical grouping to the classes that reside in the package:

Business object definitions, and items that tie directly to those types, will all live in the hms_core.business_objects namespace, and can be imported from there, for example:

from hms_core.business_objects import BaseArtisan

All members of hms_core.business_objects could be imported, if needed, with:

import hms_core.business_objects

Similarly, functionality that relates to the data object structure that's still in development will all live in the hms_core.data_objects namespace:

from hms_core.data_objects import BaseDataObject

Or, again, all members of the module could be imported with:

import hms_core.data_objects

With the basic data object structure ready and tested, it's time to start implementing some concrete, data persisting business objects, starting with the ones living in the Artisan Application.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.147.65.247