Skeleton for pytest test

George Shuklin
2 min readNov 10, 2016

--

(please see update to this post: link)

There are few things I want to do in my tests with pytest:

  1. To test coverage aiming 100% code coverage.
  2. To place tests inside ‘tests’ directory with source code in ‘appname’ directory in my git repository.

Coverage tests requires pytest-cov plugin (apt-get install python-pytest-cov), but more importantly their require specific command line arguments for the pytest — we need to provide filename to cover.

Placing tests in ‘tests’ directory cause another pain: we can not import from ‘../appname’.

Directory structure

Before we continue: my approach require specific placement for source code and tests. Both of them are obvious.

One more requirement may be annoying for some: We need ‘git directory’ name match app name. If not — replace file_to_test with you own version.

../myapp <- git repo
../myapp/myapp <- source code
../myapp/myapp/some_code.py <- code we want to test
../myapp/tests <- tests are here
../myapp/tests/test_some_code.py <- tests for myapp/some_code.py

Skeleton for tests

#!/usr/bin/python
import os
import inspect
import sys
import pytest
# trick to use tests directory and be able to import from '..'
ourfilename = os.path.abspath(inspect.getfile(inspect.currentframe()))
currentdir = os.path.dirname(ourfilename)
parentdir = os.path.dirname(currentdir)
sys.path.insert(0, parentdir)
if __name__ == "__main__":
file_to_test = os.path.join(
parentdir,
os.path.basename(parentdir),
os.path.basename(ourfilename).replace("test_", '')
)
pytest.main([
"-vv",
"--cov", file_to_test,
"--cov-report", "term-missing"
] + sys.argv)

@pytest
.fixture
def module():
from myapp import module
return module
def test_myfunc_simple(module):
assert module.myfunc() == True

(gist: https://gist.github.com/amarao/5340a5522e7350032b7fda1066912732)

As you can see there is beefy introduction before actual test. Let’s discuss it:

  1. ‘trick’ part. It asks for dir where test file is placed (not the current dir!), search for parent dir, and isert it into import path.
  2. ‘__main__’ part: it guesses filename for cov plugin based on test file name (same as test file, but without ‘test_’ in the beginning).
  3. To make 100% coverage possible we need to cover ‘import’ part. We do this by creating setup function (fixture) which imports our module (the module we want to test). We pass this module as variable to every test — and our module would be imported for each test again and again.

Command line output

./tests/test_module.py 
======================== test session starts ==========================
platform linux2 — Python 2.7.12+, pytest-2.9.2, py-1.4.31, pluggy-0.3.1 — /usr/bin/python
cachedir: .cache
rootdir: /home/user/git/myapp, inifile:
plugins: cov-2.2.1, mock-1.1
collected 1 items
tests/test_module.py::test_myfunc_simple PASSED
— — — — coverage: platform linux2, python 2.7.12-final-0 — — — — — — —
Name Stmts Miss Cover Missing
myapp/module.py 15 3 80% 19, 22, 25
======================== 1 passed in 0.01 seconds =====================

We can run a normal py.test and get

Conclusion

I don’t like ‘skeleton’ approach to write a code. ‘Skeleton’ is just a nick-name for ‘copy-and-paste’. But this is test territory and small copypaste may be better than ‘elegant framework on top of other elegant framework’.

This template (except for fixture/test part, obviously) may be used without changes in any test file (if project follows path/names convention from above) in any project.

--

--

George Shuklin

I work at Servers.com, most of my stories are about Ansible, Ceph, Python, Openstack and Linux. My hobby is Rust.