Since you are doing this at the module level, I assume that you have also packaged your modules as a proper Python package using something like setuptools and be deployed onto some environment where the tests will then be executed. Also, you are only partially correct in the assumption where __file__ doesn't exist - it also is not defined for a module imported from a zipped Python egg (which are getting more rare as wheels became the de facto packaging method, but they can and do exist).
There are a number of possible approaches, varying in complexity and trade offs and whether it works depend on how the module(s) to be tested are structured.
1) (Not recommended, but included anyway because sometimes this works best in the cases of simplest examples.) The laziest, but the most stable, self-contained and cross platform way - and it assumes that the file opening methods is only done in that one module that was to be tested and are done using the same call (e.g. open), the usage of extraglobs argument can be used to substitute the open call. e.g.
from io import StringIO
import doctest
import mymodule
files = {
'path/to/file1.txt': '... contents of file1.txt ...',
'path/to/file2.txt': '... contents of file2.txt ...',
}
def lazyopen(p, flag='r'):
result = StringIO(files[p] if flag == 'r' else '')
result.name = p
return result
doctest.testmod(mymodule, extraglobs={'open': lazyopen})
2) Create a real testsuite, rather than using the builtin ones through doctest.testmod
While the shorthand is useful, it is too limited as it is standalone, it cannot be used in conjunction with other testsuites that might get built. Consider creating a dedicated test module (e.g. mymodule/tests.py). I generally prefer creating a directory named mymodule/tests, with unittests named something like test_mysubmodule.py, and a __init__.py that contain the test_suite setup like so
def make_suite():
import mymodule
import os
def setUp(suite):
suite.olddir = os.getcwd() # save the current working directory
os.chdir(targetdir) # need to define targetdir
def tearDown(suite):
os.chdir(suite.olddir) # restore the original working directory
return doctest.DocTestSuite(mymodule, setUp=setUp, tearDown=tearDown)
So we have covered the basic, but targetdir needs to be defined. Again, multiple things you can consider:
1) Create a temporary directory and populate the directory with the required files using setup, and os.chdir to that, and remove the temporary directory in tearDown. Either manually write data stored as strings inside the test module, copy from your project or extract from an archive, but how do we actually get those? Which leads to...
2) If the source files are inside your project, and setuptools is available/installed in the environment, simply use pkg_resources.resource_filename to get the location, and assign targetdir to that. The setUp might now look something like
def setUp(suite):
suite.olddir = os.getcwd()
targetdir = pkg_resources.resource_filename('mymodule', '')
os.chdir(targetdir)
Also, finally, as this is now a real test suite that is produced by the make_suite function within mymodules.tests, the execution of that must be done using a testrunner, which fortunately is included as part of the default unittest framework as a simple command which can be done like so:
$ python -m unittest mymodule.tests.make_suite
.
----------------------------------------------------------------------
Ran 1 test in 0.014s
OK
Also, as that is a real test suite, it can be integrated with the testsuite globbing from the unittest module to combine everything into a single complete test suite for your entire package.
def make_suite():
# ... the other setup code
# this loads all unittests in mymodule from `test_*.py` files
# inside `mymodule.tests`
test_suite = test_loader.discover(
'mymodule.tests', pattern='test_*.py')
test_suite.addTest(
doctest.DocTestSuite(mymodule, setUp=setUp, tearDown=tearDown))
return test_suite
Again, the python -m unittest command may be used to execute the tests returned by the complete test suite.