Showing posts with label Unit Test. Show all posts
Showing posts with label Unit Test. Show all posts

Tuesday, 29 August 2017

A context manager for testing whether an object has changed

Firstly an apology for the break of well over a year in this blog; I was busy with other projects and took my eye off this series.

In my current python project, I have a class which will under some conditions create a clone of an instance of itself and then make changes to the cloned instance.

It is critical to the future operation that the the changes are only made to the clone, and that the original is not changed; and I wanted to find a simple way to test this.

My first try was to do this (as an example) :

from copy import copy

class ClassUnderTest:
    def __init__(self, value):
        """Example Class to be tested"""
        self.x = value

    def operation(self,value):
        """Create a new instance with the x attributed incremented"""
        self.x += value
        clone = copy(self)
        return clone

def test1():

    inst1 = ClassUnderTest( 5 )
    inst_copy = copy(inst1)

    inst2 = inst1.operation(1)

    assert inst1 == inst_copy

    assert inst2.x == 6

By taking a copy of the instance being tested, we can compare a before and after version of the instance. In this case inst1 is reference to the instance being tested, so if the operation changes the instance (as it does in this case), then inst1 will change. The copy though wont change at all, so comparing the two objects (as in the first assert statement), will confirm if the operation has changed the original instance rather than the new instance.

This approach has a number of issues :
  1. It is a pain to do this across 20 or 30 test cases
  2. It isn't that readable as to what this test is doing or why
  3. It only works if the class under test implements the __eq__ and __hash__ methods
I realized that a solution with a context manager would at least in part solve the with first issue;  if I could write something like this :

def test1():

    inst1 = ClassUnderTest( 5 )

    with Invariant(inst1):
       inst2 = inst1.operation(1)

    assert inst2.x == 6

Assuming the context manager works - more on that in a bit - this is eminently more readable. It is clear that the expectation that within the with block, is that the inst1 is invariant (i.e. it doesn't change). There is far less boiler plate, and the test function is clearly executing tests, rather doing stuff to make the test possible.

My 1st version of the invariant context manager was :

class Invariant():
    def __init__(self, obj):
        """Context manager to confirm that an object doesn't change within the context"""
        self._obj = obj
        self._old_obj = copy(obj)

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        # Don't suppress exceptions
        if exc_type:
            return False

        if self.obj != self._old_obj:
            raise ValueError(
                    'Objects are different :\n{original!r}\n     now : {now!r}'.format(
                        self._obj, self._old_obj)) from None

The issue with this version is that it still relies on the class implementing __eq__ and __hash__ methods; and the exception that is raised will only inform you that the instance has changed, not which attributes have changed.

A better approach would be to compare each attribute individually and report any changes - thus this version :

class Invariant():
    def __init__(self, obj):
        """Conext manager to confirm that an object doesn't change within the context

           Confirms that each attribute is bound to the same value or to a object
           which tests equal this is a shallow comparison only - it doesn't test
           attributes of attributes.
        """
        self._obj = obj
        self._old_obj = copy(obj)

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        # Don't suppress any exceptions
        if exc_type:
            return False
        
        # Compare each attribute
        for att in self._obj.__dict__:
            if getattr(self._obj, att, None) == getattr(self._old_obj, att, None):
                continue
            else:
                raise ValueError(
                    '{name} attribute is different : \n'
                    'original : {original!r}\n'
                    '     now : {now!r}'.format(
                            name=att,
                            now=getattr(self._obj, att,None),
                            original=getattr(self._old_obj, att,None) ) ) from None

This is far better but it does have a few issues still - left for the reader :
  • It requires attributes of the class to have implemented __eq__ and __hash__; not an issue if all attributes are simple builtin classes (int, float, list etc ..)
  • It will report each changed attribute one at at time, not all at once
  • While it will spot any attributes that have either changed or have been added to the instance, it wont detect any attributes which have been deleted.
  • It won't work with classes which use __slots__
I hope you find this useful.

Saturday, 24 October 2015

Python Weekly # 2 : 10 things to learn as a Python Developer

10 things to learn as a Python Developer

Whether you are contributing to Open source projects, or simply creating applications for your own use, there are a number of tools that you really should learn - theses tools will make your life so much easier.

These are not recommendations for beginners - In fact I would strongly suggest as a beginner you don't worry about extra tools until you are confident with the basic language capabilities - including how to write Object Oriented Code, and how to separate your code into modules and Python Packages.
  1. An IDE  - A good IDE can shave significant time off your development cycle. There are many good IDEs available : IDLE, Eclipse, PyCharm (to name but a few).  My personal favourite is PyCharm - I will explore why in another post. There are few projects which will insist collaborators using a particular IDE, although of course some work environments may have standard IDEs which you need to use.
  2. Choose a unit test framework or two and get familiar with then. The ability to design, construct and maintain unit tests is invaluable in any serious development project, as you will find your code quality increases (as does your confidence in the code), if you have a set of test cases that you can execute on a regular basis. unittest (Python 3.5), doctest (Python 3.5) and nose (if you are using unittest) are a few examples. 
  3. A mocking framework : Mocking is an incredibly powerful technique which allows you to replace a module with artificial version, allowing you fine control over your testing, and allowing you to test your code in ways that might otherwise be impossible (or at least very difficult) to reproduce. A common mocking framework is mock (from Python 3.3 onwards this is part of the standard library)
  4. Test Driven Development : A very powerful technique which effectively boils down to defining what your code will do before you start writing it. The difference is that instead of defining the code in a document, you define your code in terms of unit tests which will pass once your code is complete. During development regularly run your tests, so you can understand what is left to develop.
  5. A Debugger : It is highly unlikely that your code will be developed bug free - so when you find a bug, knowing how to use the debugger will be significant. Certainly using a debugger well is far more efficient than splattering your code with multiple print statements. Most IDEs (see above) will come with an inbuilt debugger, and of course you can use pdb (Python 3.5). Remember once you find and fix the bug - write a test case for it, and include it in your standard test cases that you run.
  6. Data Persistence : Any reasonably complex project will probably have a need to store and retrieve data between executions, and for that you will need to decide how that data is stored and retrieved. Options include : csv files (Python 3.5), json (Python 3.5), pickle (Python 3.5) or sqlite (Python 3.5). Personally I tend to use pickle for small low volume objects, and sqllite for larger volume data storage.
  7. Documentation : Don't forget to document your code, and do it as you go. Make sure your code is readable, and that the comments and documentation strings explain what your code does and why (in preference to how it works). Also make sure you have good overall documentation on your complete application - README files, user guides etc. It is surprising how quickly you can forget a piece of code that you wrote a few weeks ago - and end up staring at it for hours trying to work out what it does.
  8. Virtualisation : You will probably want to ensure that you isolate your code as you test it, to ensure that you don't destroy your working system. If you don't have a separate test machine, then this could be as simple as creating a clean working directory, but you might well need to use virtualenv or even virtualbox depending on what your code does and how far you want to isolate your testing.
  9. Change Control : Even if you have no intention of publishing your code, making use of a good change control system is highly recommended - if nothing else it provides you a long term undo for all of your code. If you learn to use long lived branches, you can keep your next development independent of your working version. Examples include : git, mercurial, and subversion (also called svn). If you decide to contribute to Open source projects though, you will need to be familiar with the change control system that they use.
  10. Packaging : Even if you are just creating code for your own use, if you want to ensure a clean installation every time, you probably want to look at packaging your code - either using the Python Packaging tools, or Debian Packaging tools (depending on how your code needs to be installed).