How to built-in tests into a source file

George Shuklin
Python Pandemonium
Published in
3 min readFeb 5, 2018

There are many cases when you have no luxury of having a full-scale Python project. All you allowed to is to have a self-contained script consisting of a single file. No setup.py, no tox.ini, nothing.

Sometimes those files are embedded into other projects and a cost of creating a full-fledged delivery pipeline is too high. Or it can be a minor Nagios check you don’t want to bother to package and maintain.

Even in a script mode, tests are great. No! They are necessity. Period.

But where one can put them? As I said, there is a restriction: ‘a single self-contained file’. That means, you need to put tests into this exact file you want to test.

And, indeed, a documentation for py.test provides many examples of tests been placed right after function they test. Those example, though, are …em… non-production grade. They import pytest, mock, and many other modules you may want to avoid importing in a production environment.

So, how can you place tests into a source code without causing any disturbances for your script?

Structure of a script

Most scripts are created without an entry points mechanism, therefore, they usually have this boilerplate at the bottom:

if __name__ == "__main__":
main()

Following stanzas are executed at run time by Python interpreter:

  • import statements
  • function decorators with arguments
  • function/class definitions (*)
  • any code outside of functions/classes (at top-level).

* Note: A function’s definition IS a statement for execution. It creates new object with function’s name and it’s code inside. It does not run the function itself.

Structure of a test

Pytest supports many types of tests, but I’ll focus only on ‘function-type tests’, where each test is a function with name started with test.

Pytest care about following things in file with tests:

  • It imports file with tests. That means, all lines from the ‘structure of a script’ chapter are executed. ‘name=='__main__’’ is false, therefore, main is not executed.
  • It tries to run setup_module() function if it’s present.
  • It scans file for classes and functions started with Test and execute them one by one.

Contradiction

  • we don’t want to import pytest or mock or any other test-related modules at the ‘normal execution time’
  • we need to import them at the test time

Solution

I developed a following pattern for ‘build-in’ tests.

# normal script code here
# ...
if __name__ == "__main__":
main()
# --- tests ---
def setup_module():
global pytest
global mock
import pytest
import mock
def test_foo():
with mock.patch(...):
with pytest.raises(...):
main()

I use setup_module function to inject pytest and mock modules into the global namespace. But this code will be executed only in pytest mode and will be ignored by a normal script execution.

To run tests for a script with a name ‘myscript.py’ just execute pytest:

pytest myscript.py

This trick has one limitation: One couldn’t use any of nice pytest decorators, including @pytest.mark.parametrize, which is really sad. Those decorators are executed at function declaration time, and that happens every time script runs. Therefore, no @pytest.decorators or @mock.patch at all.

Outside of that limitation, resulting code can be tested as usual.

--

--

George Shuklin
Python Pandemonium

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