Search code examples
pythontestingnosedoctest

nose-doctest module fixture before module is imported


I use nose for test collection and I also want to use its doctest plugin. I have a module that needs a fixture in order to be importable. Therefore, I cannot use nose's module fixtures, since they are loaded from the module under test. Is there a way to specify module fixtures for nose-doctest outside of the module?

For some use-cases, an option would be to detect being run under doctest and to apply the fixture at the beginning of the module. I'd be interested to hear answers for this use-case as-well.

However, there are situations where this cannot work: When the import fails due to a SyntaxError, no module code is ever run. In my case, I'm mostly developing code that is both compatible with python 2 and python 3 (without 2to3). There are a few python 3 specific modules however, which simply should not be inspected at all by nose, when run under python 2. What would my best option be here?

EDIT: MWE (for the SyntaxError situation)

I have a package with many small modules, some of them use python 3 syntax. Here's the package structure:

~/pckg/
  __init__.py
  py3only.py
  ... (other modules)
  tests/
    test_py3only.py

Some tests are written as unittest.TestCase, but I also want the code examples in the docstrings tested. ~/pckg/__init__.py is empty.

~/pckg/py3only.py:

def fancy_py3_func(a:"A function argument annotation (python 3 only syntax)"):
    """ A function using fancy syntax doubling it's input.

    >>> fancy_py3_func(4)
    8
    """
    return a*2

~/pckg/tests/test_py3only.py:

import sys, unittest

def setup_module():
    if sys.version_info[0] < 3:
        raise unittest.SkipTest("py3only unavailable on python "+sys.version)

class TestFancyFunc(unittest.TestCase):
    def test_bruteforce(self):
        from pckg.py3only import fancy_py3_func
        for k in range(10):
            self.assertEqual(fancy_py3_func(k),2*k)

Testing on python 3, everything gets tested and passes (run from the enclosing folder, e.g. ~):

~ nosetests3 -v --with-doctest pckg
Doctest: pckg.py3only.fancy_py3_func ... ok
test_bruteforce (test_py3only.TestFancyFunc) ... ok

On python 2, the module fixture of ~/pckg/tests/test_py2only.py properly detects the situation and skips the test. However, we get a SyntaxError from ~/pckg/py3only.py:

~ nosetests -v --with-doctest pckg 
Failure: SyntaxError (invalid syntax (py3only.py, line 1)) ... ERROR
SKIP: py3only unavailable on python 2.7.6 (default, Mar 22 2014, 22:59:56)

A function similar to ~/pckg/tests/test_py3only.py:setup_module() could solve that problem, if I could get nose to run that code before it's doctest plugin even attempts to import that module.

It looks like my best bet is to write a proper top-level test script that handles the collection of the tests...


Solution

  • Specific test files, directories, classes or methods can be excluded using nose-exclude nose plugin. It has --exclude-* options.

    To handle missing modules, you have to patch sys.modules using mock.

    F.e, There's a Calc class in mycalc module, but I don't have access to it because it's missing. And there are two more modules, mysuper_calc and mysuper_calc3, the latter is Python 3 specific. These two modules import mycalc and mysuper_calc3 shouldn't be tested under Python 2. How to doctest them out of module, that's in plain text file? I assume this is OP's situation.

    calc/mysuper_calc3.py

    from sys import version_info
    if version_info[0] != 3:
        raise Exception('Python 3 required')
    from mycalc import Calc
    class SuperCalc(Calc):
        '''This class implements an enhanced calculator
        '''
        def __init__(self):
            Calc.__init__(self)
    
        def add(self, n, m):
            return Calc.add(self, n, m)
    

    calc/mysuper_calc.py

    from mycalc import Calc
    
    class SuperCalc(Calc):
        '''This class implements an enhanced calculator
        '''
        def __init__(self):
            Calc.__init__(self)
    
        def add(self, n, m):
            return Calc.add(self, n, m)
    

    Now to mock out mycalc,

    >>> from mock import Mock, patch
    >>> mock = Mock(name='mycalc')
    

    Module mycalc has class Calc which has method add. I test SuperCalc instance add method with 2+3.

    >>> mock.Calc.add.return_value = 5  
    

    Now patch sys.modules and mysuper_calc3 can be conditionally imported within the with block.

    >>> with patch.dict('sys.modules',{'mycalc': mock}):
    ...     from mysuper_calc import SuperCalc
    ...     if version_info[0] == 3:
    ...         from mysuper_calc3 import SuperCalc
    

    calc/doctest/mysuper_calc_doctest.txt

    >>> from sys import version_info
    >>> from mock import Mock, patch
    >>> mock = Mock(name='mycalc')
    >>> mock.Calc.add.return_value = 5
    
    >>> with patch.dict('sys.modules',{'mycalc': mock}):
    ...     from mysuper_calc import SuperCalc
    ...     if version_info[0] == 3:
    ...         from mysuper_calc3 import SuperCalc
    >>> c = SuperCalc()
    >>> c.add(2,3)
    5
    

    The file mysuper_calc_doctest.txt has to be alone in its own directory otherwise nosetests searches for doctest in non-test modules.

    PYTHONPATH=.. nosetests --with-doctest --doctest-extension=txt --verbosity=3
    

    Doctest: mysuper_calc_doctest.txt ... ok


    Ran 1 test in 0.038s

    OK

    A wrapper around nosetests to detect Python 3 which passes .py files without syntax errors to nosetests

    mynosetests.py

    import sys
    from subprocess import Popen, PIPE
    from glob import glob
    
    f_list = []
    
    py_files = glob('*py')
    try:
        py_files.remove(sys.argv[0])
    except ValueError:
        pass
    
    for py_file in py_files:
        try:
            exec open(py_file)
        except SyntaxError:
            continue
        else:
            f_list.append(py_file)
    
    proc = Popen(['nosetests'] + sys.argv[1:] + f_list,stdout=PIPE, stderr=PIPE)
    print('%s\n%s' % proc.communicate())
    sys.exit(proc.returncode)