Search code examples
pythonunit-testingcode-coveragepython-unittest

Python interface pattern and unit test code coverage


I'm working in a python project using unittest for testing along with coverage for code coverage.
I use the interface pattern extensively and I noticed that the overall code coverage percentage is heavily influenced by "non tested interfaces".
Consider the following:

class IReader(object):
    @abstractmethod
    def read(self):
        pass


class Reader(IReader):
    def read(self):
        # whatever

I test Reader but (obviously) I do not test IReader so the pass instruction is marked as not covered by tests.

Is there a way to ignore the interfaces from coverage?
Since this is one of my first python project, am I doing this totally wrong?


Solution

  • I don't really see the point of having this read method defined with pass as single instruction. If you don't need it, let it raise NotImplementedError.

    class IReader(object):
        @abstractmethod
        def read(self):
            raise NotImplementedError
    

    As specified in the docs, an abstract method can have an implementation used by children with super(). But in your case, it does nothing. Therefore, unless you have a good reason, you might as well let it raise NotImplementedError.

    (Arguably, a good reason could be a pattern where all your children call super().my_method() for some reason, so you need an implementation for all methods in the abstract class.)

     How to exclude the abstract method from coverage report 

    Regardless, the test coverage is just an indicator you build: the part of the code you want to test that you actually test. Defining the "code you want to test" is up to you.

    You could add a test to check that the abstract method returns NotImplementedError or just passes, if you see an interest in this.

    Or you may think (which seems reasonable) that testing this is pointless, in which case excluding the method from coverage report #pragma: no cover seems like the way to go:

    class IReader(object):  #pragma: no cover
        @abstractmethod
        def read(self):
            pass
    

    The doc page linked above shows how you can also exclude all NotImplementedError methods adding this to your configuration file:

    [report]
    exclude_lines =
        pragma: no cover
        raise NotImplementedError
    

    so that you don't have to add a pragma to each abstract method.


    2020 edit

    I just noticed the @abstractmethod decorator. Since it prevents the class from being instantiated, I wouldn't raise NotImplementedError. See this other answer.

    I'd just leave the method empty. Syntaxically, a docstring is enough

    class IReader(object):
        @abstractmethod
        def read(self):
            """Read data"""