# 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