I've been learning about unit testing with pytest recently, trying to figure out what is and isn't a valuable thing to test. I'm very new to it, so I thought a review would be a good way to learn about holes I'm leaving and mistakes I'm making.
So, I'm primarily interested in the test functions being reviewed, though I'm open to feedback on the functions being tested too.
The tools.py
functions are just common functions I often need across my scripts, so I wanted to collect them in the library for convenience. A progress bar for giving feedback on how far along a loop has run, a function to ensure that a folder exists and a quick connection function.
Note: Shotgun is a third party service I often hook into. A Shotgun object is just a tool for calling queries and doing updates on an external database. For these purposes, you mostly just need to know that it's an object that needs to authenticate.
I tried to focus my testing on things that could be broken by changes, or are likely to go wrong without immediately being noticed. So here's the scripts:
test_tools.py
import pytest
import os
from time import sleep
from tools import get_shotgun, create_folder, ProgressBar
@pytest.fixture(scope='module')
def shotgun():
return get_shotgun()
def test_get_shotgun_returns_value(shotgun):
assert shotgun is not None, "Shotgun object was not returned"
def test_get_shotgun_credentials(shotgun):
shotgun.find_one("Project", [])
def test_create_folder_invalid_path_raises_errors():
with pytest.raises(OSError):
create_folder("\\\\\\")
def test_create_folder_swallow_existing_folder_exception():
# Folder of current file definitely exists.
folder = os.path.dirname(__file__)
create_folder(folder)
def test_create_folder_returns_original_path():
folder = "dummy"
while os.path.isdir(folder):
folder = folder + '_'
assert folder == create_folder(folder), "Original path was not returned"
try:
os.rmdir(folder)
except OSError:
print("Couldn't delete temporary test folder.")
raise
def test_ProgressBar_validate_input():
with pytest.raises(ValueError):
ProgressBar("zero")
with pytest.raises(ValueError):
ProgressBar(None)
with pytest.raises(ValueError):
ProgressBar(-1213)
def test_ProgressBar_finishing_above_full():
print('\n')
bar = ProgressBar(100)
for _ in range(16):
sleep(0.1)
bar.iter(12)
def test_ProgressBar_context_manager():
print('\n')
with ProgressBar(10) as bar:
for _ in range(10):
sleep(0.1)
bar.iter(1)
def test_ProgressBar_context_manager_exiting():
print('\n')
with ProgressBar(10) as bar:
for i in range(10):
sleep(0.1)
bar.iter(1)
if i > 6:
break
def test_ProgressBar_context_manager_not_mask_errors():
print('\n')
with pytest.raises(ValueError):
with ProgressBar(10) as bar:
int('')
tools.py
import errno
import os
import sys
from shotgun_api3 import Shotgun
class ProgressBar(object):
"""A simple class for printing messages about progress.
Takes an int `full`, to indicate the intended total progress to make.
`iter()` increases current progress value and prints the result.
A context manager is also supported, allowing
with ProgressBar(123) as bar
"""
def __init__(self, full):
try:
self.full = int(full)
if self.full < 1:
raise ValueError
except (TypeError, ValueError):
raise ValueError("ProgressBar can only take int values > 0.")
self.progress = 0
self.completed = False
@property
def finished(self):
return self.progress >= self.full
def iter(self, increment=1):
"""Increases progress and prints a message showing current progress.
increment defaults to 1, but can increase by any value.
Having more than `full` progress isn't validated, just counted as the
bar being fully finished.
"""
self.progress += increment
sys.stdout.flush()
if self.finished:
self.finish()
return
string = '=' * int((self.progress * 50.0) / self.full)
percent_progress = int((self.progress * 100.0) / self.full)
message = "\r|{:50}|In progress: {}%".format(string, percent_progress)
sys.stdout.write(message)
def finish(self):
if not self.completed:
sys.stdout.write("\r|{0:50}|COMPLETE\t\t\n".format("=" * 50))
self.completed = True
def __enter__(self):
sys.stdout.flush()
message = "\r|{:50}|Initialising".format(0)
sys.stdout.write(message)
return self
def __exit__(self, *exception_values):
if self.finished:
self.finish()
return
string = '=' * int((self.progress * 50.0) / self.full)
percent_progress = int((self.progress * 100.0) / self.full)
message = "\r|{:50}|Exited at: {}%\t\n".format(string, percent_progress)
sys.stdout.write(message)
def get_shotgun():
SERVER_PATH = "REDACTED"
SCRIPT_NAME = "REDACTED"
SCRIPT_KEY = "REDACTED"
return Shotgun(SERVER_PATH, SCRIPT_NAME, SCRIPT_KEY)
def create_folder(filepath, isfile=False):
"""Creates folder for file or folder at `filepath` if necessary.
`isfile` is a boolean to signal whether the filepath is a file or folder.
"""
if isfile:
folder = os.path.dirname(filepath)
else:
folder = filepath
try:
os.makedirs(folder)
except OSError as e:
if e.errno != errno.EEXIST:
raise e
return filepath
You can run the test with py.test test_tools.py
, the -v
and -s
flags are helpful. You may also need to pip install pytest
first.