DEV Community

Namah Shrestha
Namah Shrestha

Posted on

Chapter 2: The Need Of A Project Structure In Python Testing

2.1 Understanding the problem with import statements.

  • Our test file looks something like this:

    import pytest
    from app import simple_calculator_function
    
    def test_simple_calculator_function() -> None:
        assert simple_calculator_function("5*(4+5)") == 45 # test addition and multiplication
        assert simple_calculator_function("10 - (100/2)") == -40 # test subtraction and division. 
        assert simple_calculator_function("'a' + 'b'") == 'ab' # test string concatenation
    
  • The import statement from app import simple_calculator_function assumes that app.py is on the same folder as the test_app.py and therefore this kind of import works.

  • We do not want to depend on this feature for tests. We want our tests to run no matter where they are.

  • Here, to make sure that the import works, we would need to make sure that test_app.py and app.py are in the same directory.

  • In case they are in different directories, we need to make sure we run both tests and the application from a common working directory. This working directory is outside both tests and src directories
    In this case, the import statement in the test_app.py file would be:

        from <common_working_directory>.<app_directory>.app import simple_calculator_function.
    

    In this case, the tests also need to be run from the common_working_directory. The test directory would be: <common_working_directory>/tests/test_*.py.

  • This stops us from running the tests from anywhere and now we have to depend on having the same working directory as well.

  • We need to make sure that import works from anywhere and doesn't depend on the directory structure.

  • The way to make that happen is to make your project an installable project.

2.2 Solution to the problem with import statements.

  • The solution to the import problem is to turn your project into an installable package.
  • We need to setup before applying the solution. This is what we will do in this section.
  • So lets move on with a new folder structure. We create src/simple_calculator/ and tests/ directories.

    - src/
        - simple_calculator/
            - __init__.py
            - app.py
    - tests/
        - __init__.py
        - test_app.py
    - .gitignore
    - LICENSE
    - README.md
    

    The app.py is placed inside src/simple_calculator/ along with an __init__.py file and test_app.py is placed inside tests/ along with an __init__.py file.

  • As soon as we create the new directory we get an import error on the test file when we run it.

    • Unresolved Reference 'app'
    • This is because the test file can no longer find app. They are in different directories.
  • To make it find the app, we can import from the project directory's root and run tests also from the same directory like we mentioned eariler. This would mean the import statement inside test_app.py would look like:

    from src.simple_calculator.app import simple_calculator_function
    
  • Like we mentioned earlier, the problem with this is that we need to run the test also from the project directory.

  • Again the idea is that would like to run our tests from anywhere without worrying about directory structures for imports.

  • To enable that we can expect our code to function as a library so that imports can happen from anywhere.

  • This means making our application installable. So that the following import works after installing our package in the virtual environment with pip.

    from simple_calculator.app import simple_calculator_function
    
  • Now no matter where the test is it can use the same import statement everywhere. This solves the problem of having to depend on directory structure for imports.

2.3 Making our application installable.

  • To make our application installable, we need to add a bunch of configuration files.
    • This is an open issue in the Python community.
    • The idea is, we shouldn’t need these many files to make our application installable.
    • Things could have been easier maybe, but, that is how it is at the moment.
  • NOTE: The structure that we will follow using setup.py, setup.cfg and pyproject.toml are not required to be the same always. setup.py perfectly supports entire functionalities. We can write the entire configurations on any one of these files if we have to. We are just organising our code better.
  • If you look at the documentation of setuptools: https://setuptools.pypa.io/en/latest/index.html. Then you’ll see that each and every configuration field in the example files has a counter part in each of the file types.
  • The community is pushing the configuration more towards the toml file and just leaving the metadata in the cfg file.

2.3.1 Understanding the pyproject.toml file.

  • The first file we will look at is pyproject.toml.
  • In the early days of Python, there was only one way to install packages. We needed a setup.py file.
  • But nowadays, there are several options such as Poetry and other such examples.
  • We can still stick to using the setup.py way and that is what we will be doing in this article.
  • We can do this by inserting a build-backend to our [build-system] in our pyproject.toml file.

    [build-system]
    requires = ["setuptools>=42.8", "wheel"]
    build-backend = "setuptools.build_meta"
    
  • We have setup build-backed to use setuptools.build_meta, this will make our project run code in setup.py. The build-backend requires setuptools and we mention that in the requires section.

  • So the next file to look at is setup.py, because our build-backend is setuptools.

2.3.2 Understanding the setup.py file.

  • Next is the setup.py file.
  • In the early days, setup.py used to be the place containing the installation script.
  • It would do everything required to do in order to install a python package.
  • We can run arbitrary code inside setup.py. Since it is a python script.
  • This is seen as a security risk and therefore, more and more code is being stripped out of the setup.py file and put into one of these other configuration file.
  • Let’s create a basic setup.py file.

    import setuptools
    
    if __name__ == "__main__":
        setuptools.setup()
    
  • This basic file is going to allow us to install our package in editable mode. Since, we have mentioned setuptools.build_meta in build-system in pyproject.toml, when we run the install script, setup.py will be executed.

2.3.3 Understanding the setup.cfg file.

  • To store the metadata of the project such as Title and Description, we create a setup.cfg file.
  • The file could look as follows:

    [metadata]
    name = something
    description = just some dummy codebase
    author = Coding with Zim
    license = MIT
    license_file = LICENSE
    platforms = unix, linux, osx, cygwin, win32
    classifiers = 
        Programming Language :: Python :: 3
        Programming Language :: Python :: 3 :: Only
        Programming Language :: Python :: 3.6
        Programming Language :: Python :: 3.7
        Programming Language :: Python :: 3.8
        Programming Language :: Python :: 3.9
    
    [options]
    packages =
        something
    install_requires =
        requests>=2
    python_requires = >=3.6
    package_dir =
        =src
    zip_safe = no
    
  • NOTE: In [options] we have new line after = sign. This signifies that there can be more than one of these values. So, packages, install_requires, package_dir can have multiple values.

  • packages are the names of packages that we are creating.

  • install_requires are the names of requirements. Need to look at how to do this with requirements.txt.

  • python_requires denotes the versions of python supported.

  • package_dir is the directory where our application module lives. Inside the src directory. There might be other names. We are just going with the naming convention.

  • zip_safe is no. No idea what it is for now.

  • Why use this file instead of putting everything in setup.py?
    Since this is just a configuration file and not a python script, we don’t have to worry about it executing arbitrary code which was the case with setup.py. This is what the community wants. They want to push everything into different configuration files.

  • Then we add a requirements.txt file which contains all our dependencies.

    requests==2.26.0
    

    In our case, it only contains requests for demo purposes.
    NOTE: In setup.py we gave a version >=2. In requirements.txt , we specify the actual version. That is best practice.

  • With this our project structure looks something like this:

    - src/
        - something/
            - __init__.py
            - app.py
    - tests/
        - __init__.py
        - test_app.py
    - .gitignore
    - LICENSE
    - pyproject.toml
    - README.md
    - requirements.txt
    - setup.cfg
    - setup.py
    
  • Now we should be able to install it with pip by the following command:

    your_project_folder$ pip install -e . # e means editable i guess.
    

Top comments (4)

Collapse
 
thomasbnt profile image
Thomas Bnt ☕

Please avoid full capital letters in title, it gives the impression of aggressiveness.

Collapse
 
zim95 profile image
Namah Shrestha

Thanks will do

Collapse
 
anikethsdeshpande profile image
Aniketh Deshpande

"-e ." means install the current directory in editable mode.

Collapse
 
zim95 profile image
Namah Shrestha

Thank you. I shall update the same.