Recently I started developing in python (v3.8) as one of our customers asked me to implement some functionality based on a code base which is written in python. First of all I was looking for a good way to learn python. As I also thought about getting certified I searched for python certifications and self study preparation courses. Pythoninstitute offers both of them. I started with the course python essentials – part I to get familiar with the syntax and the basic concepts of python.

After getting familiar with python I started analyzing the code base. Unfortunately neither the course nor the code base care about testing. From my point of view, professional software development necessarily includes testing. Testing is an important aspect of the software development process. It not only helps to ensure the expected functionality, it also reduces the fear of having to adjust the code base later on.

Besides deciding which test framework to select, I also had to choose an IDE. Since I’m a convinced JetBrains user, I decided to use PyCharm as IDE. After a short research I chose pytest as test framework as it seems to be the most recommended testing framework.

I won’t write about setting up pytest in PyCharm as JetBrains already did: https://www.jetbrains.com/help/pycharm/pytest.html.

I will instead focus on topics like parametrized tests, fixtures, mocking and working directory challenges.

Parameterized tests

During implementation of unit tests I was looking for a way to create parameterized tests analog DataRow in combination with DataTestMethod in C#/.NET’s testing framework MSTest. Pytest offers the following annotation.

class TestExamples:  # pylint: disable=too-few-public-methods
    """
    class for test grouping purposes
    """

    @pytest.mark.parametrize("test_input, expected", [
        (2, 4),
        (4, 16)
    ])  # pylint: disable=no-self-use
    def test_exp(self, test_input, expected):
        """

        @param self:
        @param test_input: input value for test
        @param expected: expected value
        @return: nothing
        """
        # arrange
        # act
        result = test_input**2
        # assert
        assert result == expected

Fixtures

Another feature I was looking for – known from testing frameworks of other programming languages – was running code before or after tests, test classes, test modules, test packages, … . Pytest offers fixtures to achieve this.

The following code gets executed once at the beginning of the test session.

@pytest.fixture(scope="session", autouse=True)
def before_all():
    """
    code to be executed before all tests
    """
    print()
    print("START before_all ...")

    # CODE TO BE EXECUTED HERE

    print("END before_all SUCCEEDED")

If autouse is set to True the fixture get instantiated even if it’s not explicitly used and it gets instantiated before explicitly used fixtures.

Mocking

The next feature I needed was mocking. The main function of the code to be tested usually gets called by command line with some arguments. The main function then parses these arguments with argparse as follows.

def main() -> None:
    """
    main entry point
    """
    parser = argparse.ArgumentParser()
    parser.add_argument("-v", "--verbose",
                        help="provide verbose progress report",
                        action="store_true")
    parser.add_argument("--service-package-guid",
                        help="ea guid of the service package",
                        required=True)
    args = parser.parse_args()

    ....


if __name__ == "__main__":
    main()

Furthermore the code executed by the main function reads a specific environment variable. So to test the main function I had to mock the arguments and the environment variable as shown below.

@pytest.fixture(scope="session")
def mock_env_vars():
    """
    mock environment variable
    """
    with mock.patch.dict(os.environ, {"PASSWORD": "password"}):
        yield


@pytest.fixture(scope="session", autouse=True)
def before_all(mock_env_vars):  # pylint: disable=unused-argument
    """
    code to be executed before all tests
    """
    print()
    print("START before_all ...")
    # mock sys.argv
    with mock.patch('sys.argv', ['arbitrary',
                                 '--service-package-guid', '{00000000-0000-0000-0000-000000000000}']):
        example.main()
    print("END before_all SUCCEEDED")

The before_all fixture takes the mock_env_vars fixture as argument. That’s how dependencies between fixtures are built. This means the before_all depends on the mock_env_vars fixture. So at the beginning of a test session first the mock_env_vars fixture gets called and then the before_all fixture with mock_env_vars fixture as argument.

Current working directory

While implementing unit tests I faced a problem concerning current working directory. The code to be tested loads a file by name that resides in the same directory (i.e. sample.docx). As my tests reside in a subfolder called tests the before mentioned file can not be found anymore during test execution because the current working directory is set to PROJECT_DIR\tests.

- PROJECT_DIR
   |- Examples.py
   |- sample.docx
   |- tests
        |- TestExamples.py

I resolved this problem by changing the working directory in before_all fixture as follows.

os.chdir(os.path.relpath('..'))

Link to the documentation of the latest stable pytest version

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.