Mechanics of Running Project Tests

So far we’ve run “toy” tests where 1) we’re not actually testing any external code and 2) all of the test code is defined in a single file.

In the real world, it’s almost certain that you’ll be running the tests of a multifile project. Let’s see how you might do that.

Checking Out the Repository

Check out the repository of the PyPA “sampleproject” into your virtual environment directory:

$ cd $VENV
$ git clone https://github.com/pypa/sampleproject.git

The result should be a sampleproject directory. Change directory into that directory:

$ cd sampleproject

Installing the Project

Let’s then install the project into our virtualenv:

$ python setup.py develop

Running The Tests

Now that we have it installed, let’s run its tests:


$ cd $VENV/sampleproject
$ python -m unittest discover -s ./tests

Here’s the output we see:

F
======================================================================
FAIL: test_failure (test_simple.TestSimple)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/chrism/projects/carscom/training/tmp/sampleproject/tests/test_simple.py", line 9, in test_failure
    self.assertTrue(False)
AssertionError: False is not true

----------------------------------------------------------------------
Ran 1 test in 0.000s

FAILED (failures=1)

If we take a look at the file within sampleproject/tests/test_simple.py, we see indeed that there is a test defined within it, and it’s the test that got run. It failed because it’s making an incorrect assertion. So if you see this, it worked.

What Did I Just Do?

You ran the tests of a more “real-world” project than ones we’ve created so far, although it’s realness is still pretty sparse.

The project we checked out is the official “sample” project of the Python Packaging Authority (see https://packaging.python.org/en/latest/distributing.html). It is filled with comments in all of its files. Python packaging is a complex topic, but for our purposes, we just need to know that the setup.py file is important. It’s what we use to install our package.

The projects that you work on in your real job will also likely have a setup.py and will follow a similar directory layout.

Alternate Way of Running The Tests

I like to be able to run tests a slightly different way, because I don’t like having to remember the python -m unittest command line switches. To do so I change the project’s setup.py.

At line 111, after the trailing comma after entry_points={}, add a carriage return and this line:

test_suite="tests",

The result should look something like this:

# ...

    entry_points={
        'console_scripts': [
            'sample=sample:main',
        ],
    },

    test_suite="tests",
)

After we’ve done this, it is possible to run the tests like this:

$ python setup.py test

And the output you’ll see is something like this:

running test
running egg_info
writing requirements to sample.egg-info/requires.txt
writing sample.egg-info/PKG-INFO
writing top-level names to sample.egg-info/top_level.txt
writing dependency_links to sample.egg-info/dependency_links.txt
writing entry points to sample.egg-info/entry_points.txt
reading manifest file 'sample.egg-info/SOURCES.txt'
reading manifest template 'MANIFEST.in'
writing manifest file 'sample.egg-info/SOURCES.txt'
running build_ext
test_failure (tests.test_simple.TestSimple) ... FAIL

======================================================================
FAIL: test_failure (tests.test_simple.TestSimple)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/chrism/pytraining/sampleproject/tests/test_simple.py", line 9, in test_failure
    self.assertTrue(False)
AssertionError: False is not true

----------------------------------------------------------------------
Ran 1 test in 0.000s

FAILED (failures=1)

As you notice, the output is slightly different than the output of the tests executed via python -m unittest. We can make them more similar by passing the -q flag.

python setup.py test -q

Now we see similar output to python -m unittest.

running test
running egg_info
writing requirements to sample.egg-info/requires.txt
writing sample.egg-info/PKG-INFO
writing top-level names to sample.egg-info/top_level.txt
writing dependency_links to sample.egg-info/dependency_links.txt
writing entry points to sample.egg-info/entry_points.txt
reading manifest file 'sample.egg-info/SOURCES.txt'
reading manifest template 'MANIFEST.in'
writing manifest file 'sample.egg-info/SOURCES.txt'
running build_ext
F
======================================================================
FAIL: test_failure (sample.tests.test_simple.TestSimple)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/chrism/projects/carscom/unittest_training/realworld/sampleproject/sample/tests/test_simple.py", line 9, in test_failure
    self.assertTrue(False)
AssertionError: False is not true

----------------------------------------------------------------------
Ran 1 test in 0.000s

FAILED (failures=1)

Which Way?

I personally prefer to stick a test_suite into my setup.py so I just need to remember, for whatever project I’m working on, that I can always run python setup.py test -q. It’s just easier to remember than the unittest command line switches. That said, it really makes very little difference, and alternate Python testing frameworks that we’ll discuss later provide additional ways to do it that are sometimes even easier.

Fixing It

Move the directory named tests from the sampleproject directory into the sampleproject/sample directory.

$ mv tests/ sample/

However, when we try to run the tests again:

$ python -m unittest discover -s ./tests/

We get this output:

Traceback (most recent call last):
  File "/home/chrism/opt/Python-2.7.10/lib/python2.7/runpy.py", line 162, in _run_module_as_main
    "__main__", fname, loader, pkg_name)
  File "/home/chrism/opt/Python-2.7.10/lib/python2.7/runpy.py", line 72, in _run_code
    exec code in run_globals
  File "/home/chrism/opt/Python-2.7.10/lib/python2.7/unittest/__main__.py", line 12, in <module>
    main(module=None)
  File "/home/chrism/opt/Python-2.7.10/lib/python2.7/unittest/main.py", line 94, in __init__
    self.parseArgs(argv)
  File "/home/chrism/opt/Python-2.7.10/lib/python2.7/unittest/main.py", line 113, in parseArgs
    self._do_discovery(argv[2:])
  File "/home/chrism/opt/Python-2.7.10/lib/python2.7/unittest/main.py", line 214, in _do_discovery
    self.test = loader.discover(start_dir, pattern, top_level_dir)
  File "/home/chrism/opt/Python-2.7.10/lib/python2.7/unittest/loader.py", line 204, in discover
    raise ImportError('Start directory is not importable: %r' % start_dir)
ImportError: Start directory is not importable: './tests/'

We have to change the command to point to the right path, which is now sample/tests:

$ python -m unittest discover -s sample/tests

Similarly when we run setup.py test we get an (inscrutable) error:

$ python setup.py test

 running test
 running egg_info
 writing requirements to sample.egg-info/requires.txt
 writing sample.egg-info/PKG-INFO
 writing top-level names to sample.egg-info/top_level.txt
 writing dependency_links to sample.egg-info/dependency_links.txt
 writing entry points to sample.egg-info/entry_points.txt
 reading manifest file 'sample.egg-info/SOURCES.txt'
 reading manifest template 'MANIFEST.in'
 writing manifest file 'sample.egg-info/SOURCES.txt'
 running build_ext
 Traceback (most recent call last):
   File "setup.py", line 111, in <module>
     test_suite="tests",
   File "/home/chrism/opt/Python-2.7.10/lib/python2.7/distutils/core.py", line 151, in setup
     dist.run_commands()
   File "/home/chrism/opt/Python-2.7.10/lib/python2.7/distutils/dist.py", line 953, in run_commands
     self.run_command(cmd)
   File "/home/chrism/opt/Python-2.7.10/lib/python2.7/distutils/dist.py", line 972, in run_command
     cmd_obj.run()
   File "/home/chrism/projects/carscom/training/lib/python2.7/site-packages/setuptools/command/test.py", line 142, in run
     self.with_project_on_sys_path(self.run_tests)
   File "/home/chrism/projects/carscom/training/lib/python2.7/site-packages/setuptools/command/test.py", line 122, in with_project_on_sys_path
     func()
   File "/home/chrism/projects/carscom/training/lib/python2.7/site-packages/setuptools/command/test.py", line 163, in run_tests
     testRunner=self._resolve_as_ep(self.test_runner),
   File "/home/chrism/opt/Python-2.7.10/lib/python2.7/unittest/main.py", line 94, in __init__
     self.parseArgs(argv)
   File "/home/chrism/opt/Python-2.7.10/lib/python2.7/unittest/main.py", line 149, in parseArgs
     self.createTests()
   File "/home/chrism/opt/Python-2.7.10/lib/python2.7/unittest/main.py", line 158, in createTests
     self.module)
   File "/home/chrism/opt/Python-2.7.10/lib/python2.7/unittest/loader.py", line 130, in loadTestsFromNames
     suites = [self.loadTestsFromName(name, module) for name in names]
   File "/home/chrism/opt/Python-2.7.10/lib/python2.7/unittest/loader.py", line 91, in loadTestsFromName
     module = __import__('.'.join(parts_copy))
 ImportError: No module named tests

We need to change setup.py. Change this line:

test_suite="tests",

To:

test_suite="sample.tests",

Note that there’s a dot instead of a slash there. It’s because we’re referring to a module instead of a disk path in setup.py’s test_suite.

Now when we run the tests, we get expected output:

running test
running egg_info
writing requirements to sample.egg-info/requires.txt
writing sample.egg-info/PKG-INFO
writing top-level names to sample.egg-info/top_level.txt
writing dependency_links to sample.egg-info/dependency_links.txt
writing entry points to sample.egg-info/entry_points.txt
reading manifest file 'sample.egg-info/SOURCES.txt'
reading manifest template 'MANIFEST.in'
writing manifest file 'sample.egg-info/SOURCES.txt'
running build_ext
test_failure (sample.tests.test_simple.TestSimple) ... FAIL

======================================================================
FAIL: test_failure (sample.tests.test_simple.TestSimple)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/home/chrism/projects/carscom/training/tmp/sampleproject/sample/tests/test_simple.py", line 9, in test_failure
    self.assertTrue(False)
AssertionError: False is not true

----------------------------------------------------------------------
Ran 1 test in 0.000s

FAILED (failures=1)

Conclusions

  • Put tests inside the package you’re working on, not outside.
  • Reasonable people disagree about this. The people who put tests outside claim that it’s better because the tests don’t ship with the code when it’s distributed to production. I don’t agree that this is better.
  • The test_suite line in a setup.py refers to a module, while the command line path as an argument to -s in python -m unittest discover -s foo refers to a disk path. Yes, it’s not sane.