Added documentation TESTING.md
Added new documentation regarding the process of conducting new unit tests for the project. Documentation covers what libraries are used in test-driven development, along with how they are used. Documentation covers how to create new unit tests according to a defined standard, as well as using pytest fixtures.
This commit is contained in:
parent
8ca27b5f81
commit
2339aabeb1
268
testing/TESTING.md
Normal file
268
testing/TESTING.md
Normal file
|
|
@ -0,0 +1,268 @@
|
|||
# Testing CLog
|
||||
Clog makes use of a few development libraries for automated testing and unit
|
||||
testing. Those libraries (currently) are primaily pytest, my[py], flake8,
|
||||
and tox.
|
||||
|
||||
Here is a brief understanding of each library for how it's used:
|
||||
|
||||
* Pytest – automated unit testing of each function/method/class structure in
|
||||
the code base. Pytest is also used with the `coverage` extension to indicate
|
||||
how much of the code base is touched by the unit tests.
|
||||
* Mypy – using static type-checking to ensure that type annotations are as
|
||||
accurate as possible. Also identify bad practices that may appear when writing
|
||||
statements/code blocks before refactorisation, and raise errors when mypy is
|
||||
ran.
|
||||
* Flake8 – to keep written code as close to the defined PEP8 standard, this
|
||||
library is used to enforce style guidance. However, not all styling is
|
||||
conventient and can be annoying to deal with when own standards are used. For
|
||||
this reason, some flake8 rules are ignored in configuration files.
|
||||
* tox – ensure that the code base is backwards compatible with earlier versions
|
||||
(currently 3.6 or later) of Python. If any version is listed as failing, then
|
||||
modules should be reconsidered to support older, compatible, syntax up to the
|
||||
latest version of Python.
|
||||
|
||||
With these, Clog can ensure that code behaves the way it is expected, best
|
||||
practices are adopted and forced were appropriate, and it's backwards
|
||||
compatible with earlier versions and not just the latest release.
|
||||
|
||||
## Test-Driven Development
|
||||
If you wish to test the library against the defined systems, then a few
|
||||
prerequisites are required in order to setup the environment correctly. The
|
||||
basic steps are as followed: clone project – setup tools with pip – run tools.
|
||||
|
||||
### Setting Up the Environment
|
||||
It's usually always simpler and easier to clone the repository if you intend to
|
||||
use a library, either for building from scratch or to develop on the library.
|
||||
For this, you will require Git, and to run the following commands:
|
||||
|
||||
```
|
||||
git clone https://git.closedless.xyz/ClosedLess/clog.git
|
||||
cd clog
|
||||
```
|
||||
|
||||
Afterwards, you can run pip to install and setup the required tools and
|
||||
environment settings in editable mode.
|
||||
|
||||
#### For Linux
|
||||
```
|
||||
pip3 install -e .
|
||||
pip3 install -r requirements-dev.txt
|
||||
```
|
||||
|
||||
#### For Windows
|
||||
```
|
||||
pip install -e .
|
||||
pip install -r requirements-dev.txt
|
||||
```
|
||||
|
||||
This will install all the required packages necessarily for carrying out
|
||||
test-driven development. Those are the following packages (with versions):
|
||||
|
||||
* `flake8==4.0.1`
|
||||
* `tox==3.25.0`
|
||||
* `pytest==7.1.2`
|
||||
* `pytest-cov==3.0.0`
|
||||
* `mypy==0.950`
|
||||
|
||||
### Running the Tools and Expectations
|
||||
Assuming that `pip` did not report any errors when installing the required
|
||||
packages, all the tools required should be available from the console window.
|
||||
From either a terminal, Command-Prompt, or PowerShell, the following commands
|
||||
can be ran with the defined inputs:
|
||||
```
|
||||
mypy clog # 'mypy .' will cause mypy to run on the entire workspace
|
||||
flake8
|
||||
pytest
|
||||
tox
|
||||
```
|
||||
|
||||
Each of the above lines are commands to run each tool individually. Mypy is the
|
||||
only command which requires a file, or directory, to point to when it runs.
|
||||
|
||||
Every tool is expected to return no errors or warnings once it has finished
|
||||
executing. If mypy is ran using `mypy .`, then some errors and/or warning may
|
||||
be shown due to static type-checking of modules found in the `testing/` and
|
||||
root directory spaces. These files are not important for ensuring type-checking
|
||||
and may be ignored entirely (this is why `clog` is given as the directory).
|
||||
|
||||
Flake8 is a bit too strict regarding some guidance rules. For this reason,
|
||||
certain rules are defined to be ignored by flake8 when analysing, and
|
||||
additional rules are ignored for specific files. For example, the rules W503
|
||||
and W504 define where a break should occur when dealing with multiline
|
||||
operators. According to PEP8, a line-break should be placed before the operator
|
||||
and not afterwards to help readability. With this, the W504 rules is ignored:
|
||||
|
||||
```py
|
||||
# best practice
|
||||
if (my_var == 1
|
||||
and other_var is not None): # W503 & PEP8 standard
|
||||
# do something...
|
||||
|
||||
# bad practice
|
||||
if (my_var == 1 and
|
||||
other_var is not None): # W504 & non-PEP8 standard
|
||||
# do something...
|
||||
```
|
||||
|
||||
Other rules may be ignored from the configuration, and testing modules are
|
||||
completely ignored using the descriptor tag at the start of the module. To
|
||||
find out which rules are ignored, consult the `setup.cfg` configuration file
|
||||
in the root directory of the project.
|
||||
|
||||
If tox is producing errors, check that all Python interpreters are installed
|
||||
to the local machine, and whose path to executable is located in the Path
|
||||
environment variable. In the case that not all interpreters are available, it
|
||||
would be reasonable to ignore the errors produced by tox and ensure that tox
|
||||
passes for at least the installed interpreters. Note that if development to
|
||||
the project is to be conducted, then **all** interpreters required by tox are
|
||||
to be installed on the local machine to guarantee compatibility of code with
|
||||
earlier versions of Python. (CAUTION: SUBMITTED PRs WHICH **DO NOT** PASS ALL
|
||||
TOX TESTS **WILL BE REJECTED** UNTIL CORRECTED.)
|
||||
|
||||
Pytest is expected to pass 80 tests and skip 1 (v0.1.0). The tests are designed
|
||||
to ensure robustness of each module and its contents within the code base. Some
|
||||
test parameters will check that certain conditions fail purposefully to
|
||||
indicate that erroneous arguments are handled accordingly. If any tests are
|
||||
identified as failing on a local machine, an Issue may be raised, subject to
|
||||
the code base not being modified. In the event that code has been modified,
|
||||
this must be explicitly stated, and any new functionality added **should** have
|
||||
their own unit tests defined appropriately. (CAUTION: SUBMITTED PRs WHICH
|
||||
**DO NOT** PASS ALL PYTEST TESTS **WILL BE REJECTED** UNTIL CORRECTED.)
|
||||
|
||||
## Performing Pytest Unit Testing
|
||||
If you wish to add new features to the project and submit a PR with a new
|
||||
enhancement, then it's important to create unit tests for the additional
|
||||
features that have been added. This section will define how to go about
|
||||
defining new unit tests.
|
||||
|
||||
### Naming Testing Modules
|
||||
If a new module in the code base has been added with new features, then a new
|
||||
module should also be defined in the `testing/` directory. The name of this
|
||||
module should start with the prefix `test_` followed by the suffix
|
||||
`[new module name].py`. Therefore, if a new module created was `helloworld.py`,
|
||||
then the corresponding test module should be named `test_helloworld.py`, and
|
||||
all unit tests related to `helloworld.py` should be placed in this module.
|
||||
|
||||
If a new feature was added to an existing module in the code base, then the new
|
||||
unit tests for this may be added to the respectful, existing test module.
|
||||
|
||||
### Defining Pytest Unit Tests
|
||||
Unit tests are defined using a verbose function header, and potentially a class
|
||||
body to simply group different tests together. For a function to become a
|
||||
pytest unit test, the prefix `test` must be present on a function header. For
|
||||
this reason, the adopted prefix is `test_` followed by a verbose summary of
|
||||
the unit test, using the `snake_case` naming convention.
|
||||
|
||||
Here is a basic example of how to implement a valid unit test for Clog, which
|
||||
is taken in part from existing modules in the code base.
|
||||
|
||||
**`helloworld.py`**
|
||||
```py
|
||||
class Foo:
|
||||
"""A dummy Singleton class"""
|
||||
__instance__ = None
|
||||
|
||||
def __new__(cls):
|
||||
"""Construct a new instance of the class,
|
||||
or return an existing instance."""
|
||||
# check if an instance does not already exist
|
||||
if cls.__instance__ is None:
|
||||
# make a new Singleton instance
|
||||
cls.__instance__ = super(Foo, cls).__new__(cls)
|
||||
|
||||
return cls.__instance__ # always return the Singleton instance
|
||||
```
|
||||
|
||||
**`test_helloworld.py`**
|
||||
```py
|
||||
import pytest
|
||||
|
||||
from helloworld import Foo
|
||||
|
||||
|
||||
bar = Foo() # instantiate a new global Singleton instance
|
||||
|
||||
class TestFoo:
|
||||
"""class represents grouping of unit tests related to Foo."""
|
||||
|
||||
# using pytest `mark.parametrize`, multiple values can be passed
|
||||
# to the function, and if one assertion fails, other values can
|
||||
# still be tested by pytest.
|
||||
@pytest.mark.parametrize(
|
||||
"value, expected", [
|
||||
(Foo(), Foo()),
|
||||
(bar, bar),
|
||||
(Foo(), bar),
|
||||
(bar, Foo())
|
||||
]
|
||||
)
|
||||
def test_new_foo_instance(self, value, expected):
|
||||
"""Verfiy the identity of all `value`s against what is `expected`.
|
||||
If any assertion fails, then the class is not a Singleton."""
|
||||
assert value is expected # assert identity is the same
|
||||
```
|
||||
|
||||
Note, docstrings and comments are not mandatory in testing modules. Given the
|
||||
verboseness of the function header, there's a clear indication of what the
|
||||
test is checking. Comments may be adopted if function bodies contain setup code
|
||||
for a test – most tests will simply contain an `assert` statement.
|
||||
|
||||
### Creating Pytest Fixtures
|
||||
You may want to construct your own fixture for testing to help with assisting.
|
||||
Clog already defines a fixture called `capture_stdpipe`, which uses
|
||||
`monkeypatch` to interrupt a call to write to a standard PIPE, and store the
|
||||
write value to a temporary buffer, which can be accessed when required. All
|
||||
fixtures should be defined in the `conftest.py` module, found in the root
|
||||
directory of the project.
|
||||
|
||||
#### Using the `capture_stdpipe` fixture
|
||||
If you are testing functionality regarding standard PIPE streams, this fixture
|
||||
will be very handy for ensuring integrity of the message's body through a
|
||||
write process to a PIPE, with respect to any formatting. Both `STDOUT` and
|
||||
`STDERR` can be accessed via this fixture.
|
||||
|
||||
**`conftest.py`**
|
||||
```py
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def capture_stdpipe(monkeypatch):
|
||||
stream_buf = {"stdout": "", "stderr": "", "writes": 0}
|
||||
|
||||
def mimic_stdout(chars):
|
||||
stream_buf['stdout'] += chars
|
||||
stream_buf['writes'] += 1
|
||||
|
||||
def mimic_stderr(chars):
|
||||
stream_buf['stderr'] += chars
|
||||
stream_buf['writes'] += 1
|
||||
|
||||
|
||||
monkeypatch.setattr(sys.stdout, 'write', mimic_stdout)
|
||||
monkeypatch.setattr(sys.stderr, 'write', mimic_stderr)
|
||||
|
||||
return stream_buf
|
||||
```
|
||||
|
||||
The stream buffer can hold one single write to both standard PIPE streams
|
||||
before data is then overwritten and lost. To access the data from the buffer in
|
||||
a unit test function, the fixture can be passed as a function parameter.
|
||||
|
||||
**`helloworld.py`**
|
||||
```py
|
||||
def check_that_the_fixture_captures_output(capture_stdpipe):
|
||||
# print will go to STDOUT by default
|
||||
print("Hello World!")
|
||||
# access the buffer and compare
|
||||
assert capture_stdpipe['stdout'] == "Hello World!"
|
||||
```
|
||||
|
||||
New fixtures defined for unit testing can be used in a similar fashion to how
|
||||
`capture_stdpipe` is used. For more information regarding how to use fixtures
|
||||
and further pytest features, consult the official documentations.
|
||||
|
||||
Author: Ethan Smith-Coss.
|
||||
Copyright (c) 2022-23
|
||||
Loading…
Reference in New Issue
Block a user