While navigating the murky waters of writing not only correct but also extendable, easy-to-read, well just let’s say it’s good code, I’ve had a lingering thought. How to I write my code to be testable? What are the guidelines to testing? Here go my considerations from today.
The Environment design pattern
This is the pattern you’d have to use if your software modifies the external world, and you’d like to avoid having to mock half the Universe for it to be testable.
Say you’re writing a solution that requires listing files in a directory and a file deletion. In this case, you should avoid mocking the os module, instead provide it with a class that serves as an accessor, implementing a particular interface, and using it to carry out it’s actions. Example:
import abc
import os
class BaseEnvironment(metaclass=abc.ABCMeta):
@abc.abstractmethod
def list(self, path: str) -> list[path]:
...
@abc.abstractmethod
def rm(self, path: str) -> None:
...
class RealEnvironment(BaseEnvironment):
def list(self, path):
return os.listdir(path)
def rm(self, path):
os.unlink(path)
class MockEnvironment(BaseEnvironment):
def __init__(self, files: list[str]):
self.files = files
def list(self, path):
return [path_ for path_ in self.files if path_.startswith(path)]
def rm(self, path):
if path in self.files:
self.files.remove(path)
Say your class took a BaseEnvironment as an argument by it’s startup. Now, you’ve made it extremely easy to unit test your class, and you don’t even need to mock anything. As usual, exercise proper care when deciding on the Environment design pattern versus just mocking what’s necessary. You’d just instantiate YourAlgorithmClass(RealEnvironment()) during runtime, and YourAlgorithmClass(MockEnvironment(…)) for unit testing. Now the code you’ve written can be called easy to unit test. Utilize the fact that in Python you can declare classes even as you’re writing a new function.
When to use BDD tests and when to test unit
BDD tests should be primarily used towards testing E2E, namely end-to-end.
So let’s try to say when not to BDD test:
- You’re testing a Web application endpoint. Django has it’s APIClient, while Flask has it’s Client as well. It’s also extremely easy to load a fixture for particular tests, also not doing BDD tests allows you less boilerplate. There’s significant boilerplate associated with writing BDD tests.
- Your tests are written entirely by programming folk. You need the textual test description of BDD, so that tests can be written by non-programming stakeholders (users and/or testers). If neither of those write your tests, just do yourself a favour and skip BDD entirely.
When to do BDD tests:
- You’re testing changes that require a few respositories to work together.
- You require an external piece of software (such as a database, although I’ve found that to be easy to produce during unit tests as well, by using such a feature of GitLab as services or you can just as easily use docker-compose (take care to properly cancel your jobs, as GitLab runner won’t clean up after you if you cancel such a task while it’s running).
- There’s added value to typing these tests in plain text. Suppose you have a really handy tester, who can’t wrap his head around Python, but is good enough to type these tests. Note that at some level he’ll always have to type Python code (unless you have a couple of do-everything-BDD-macros).
- The correctness of what you write can be expressed in terms of plain text, for example if you were implementing a specification.
Rules for designing software in Python
- Make your software testable (consider the Environment design pattern).
- Solution that produces less lines of code is almost always the right one.
- Try to intertwine your documentation with your source (eg. consider a pydoc solution such as the excellent Sphinx project). This way you’ll avoid the out-of-date documentation problem (and I’m sure that you’ll agree that documentation just out-of-date is worse than no documentation at all). Instill a feeling in your team that writing documentation is important, good and basically always welcome. I feel that documentation is just as important artifact of programmer’s work as the source, if even not more so. Don’t dump the task of writing documentation to the most junior member of the team (unless you have to).