Test Driven Development Startup with PySide & Pytest

Introduction

A supplementary article on "Test Driven Development Startups with PySide & Pytest" at the PySide Study Group. And a note for test-driven development with PySide and Pytest.

image.png

Development environment

Windows 10 Python 3.7.7 Pytest

Sample file

GitHub

Initial maintenance

Directory and file preparation

directory


root
  |- sample
  |    |- __init__.py
  |    |- gui.py
  |- tests
  |    |- __init__.py
  |    |- conftest.py
  |    |- unit
  |        |- test_gui.py
  |- requirments.txt

The gui.py that is the source of the tool here is as follows.

sample/gui.py


import sys
from PySide2 import QtCore
from PySide2 import QtWidgets


class SampleDialog(QtWidgets.QDialog):

    def __init__(self, *args):
        super(SampleDialog, self).__init__(*args)
        
        self.number = 0

        self.setWindowTitle('Hello, World!')
        self.resize(300, 200)

        layout = QtWidgets.QVBoxLayout()

        self.label = QtWidgets.QLabel(str(self.number))

        layout.addWidget(self.label)

        self.button = QtWidgets.QPushButton('Add Count')
        self.button.clicked.connect(self.add_count)
        self.button.setMinimumSize(200, 100)

        layout.addWidget(self.button)

        self.setLayout(layout)
        self.resize(200, 100)


    def add_count(self):
        self.number += 2
        self.label.setText(str(self.number))


def main():
    app = QtWidgets.QApplication.instance()
    if app is None:
        app = QtWidgets.QApplication(sys.argv)
    gui = SampleDialog()
    gui.show()
    
    app.exec_()
    

if __name__ == '__main__':
    main()
    

The appearance when executed is as follows. image.png

Write the required modules in requirements.txt

PySide2
Pytest

Create a venv environment

  1. Go to the root directory
  2. python -m venv .venv
  3. Enable venv environment: .venv \ Script \ Activate.bat
  4. pip install -r requirements.txt

Connect the path to the module with conftest.py

tests/conftest.py


import os
import sys

sys.path.append(os.path.dirname(os.path.abspath(__file__)))

The Pytest official [recommended] to install using pip install -e . with setup.py (https://docs.pytest.org/en/latest/goodpractices.html" However, depending on the environment (in my case, the tool release environment of the Techcnial Artist team (including DCC tools)), it may not always be possible to provide a pip environment.

In such a case, you may not want to take an extra directory structure, so use conftest.py and put the path to root when executing Pytest.

Of course, in the case of a distribution environment that assumes pip install, it is better to place setup.py under root so that pip intall -e . can be done so that pip install can be done.

Similarly, if you have modules that you need for external tools such as Maya or in-house libraries, you should go through the path here.

Test code implementation

Test file with Pytest

For the time being, the following are the basic behaviors to remember. (Actually, there are more, so please refer to the document)

Write a test

tests/unit/test_gui.py


import sys

from PySide2 import QtCore
from PySide2 import QtWidgets
from PySide2 import QtTest


def test_add_count():
    from sample import gui

    app = QtWidgets.QApplication.instance()
    if app is None:
        app = QtWidgets.QApplication(sys.argv)
    gui = gui.SampleDialog()
    gui.show()

    QtTest.QTest.mouseClick(gui.button, QtCore.Qt.LeftButton)
    n1 = gui.number

    QtTest.QTest.mouseClick(gui.button, QtCore.Qt.LeftButton)
    n2 = gui.number
    assert abs(n2 - n1) == 1

Use QtTest

Here's QtTest, which PySide provides a module for testing.

The user recognizes that "by clicking the button", the number on the label goes up by 1, so to make this behavior correct, "by clicking the button" is simulated on the test. Need to be done. Here, QtTest provides a function to simulate its operation by using QtTest.QTest.mouseClick ().

In other words, in the above test_add_count, QtTest does the following:

  1. In the first QtTest.QTest.mousClicke (), "Left mouse click the gui button"
  2. Store the value of the number class variable at that time as n1
  3. In the following QtTest.QTest.mousClicke (), "Left mouse click the gui button"
  4. Store the value of the number class variable in the second clicked state as n2
  5. Use assert to compare the two stored values and make sure the difference is 1.

This allows you to ensure that the changes you make when you click the ** button are correct, with a single increment of the count **.

Execute the test command

Just run the command to run the test in the root directory for now.

pytest .

Then, the following result is obtained.

======================================= test session starts ========================================
platform win32 -- Python 3.7.7, pytest-5.4.1, py-1.8.1, pluggy-0.13.1
rootdir: D:\Develop\Python\_learn\test_pytest_pyside
collected 1 item

tests\unit\test_gui.py F                                                                      [100%]

============================================= FAILURES =============================================
__________________________________________ test_add_count __________________________________________

    def test_add_count():
        from sample import gui

        app = QtWidgets.QApplication.instance()
        if app is None:
            app = QtWidgets.QApplication(sys.argv)
        gui = gui.SampleDialog()
        gui.show()

        QtTest.QTest.mouseClick(gui.button, QtCore.Qt.LeftButton)
        n1 = gui.number

        QtTest.QTest.mouseClick(gui.button, QtCore.Qt.LeftButton)
        n2 = gui.number
>       assert abs(n2 - n1) == 1
E       assert 2 == 1
E        +  where 2 = abs((4 - 2))

tests\unit\test_gui.py:22: AssertionError
===================================== short test summary info ======================================
FAILED tests/unit/test_gui.py::test_add_count - assert 2 == 1
======================================== 1 failed in 0.67s =========================================

To explain this, As you can see in the code on the test, add_count has ** the developer's intention to want the difference between the values on the first run and the next run to be 1. However, if you look at the source code, it says self.number + = 2, ** contrary to the developer's intention **, the difference in the result of the processing executed" by clicking the button " It can be seen that the result of 2 has been obtained.

Now, if you change this to self.number + = 1 and set it to" increase by 1 (that is, the difference that always increases is 1) ", you will get the following result when you executepytest.

======================================= test session starts ========================================
platform win32 -- Python 3.7.7, pytest-5.4.1, py-1.8.1, pluggy-0.13.1
rootdir: D:\Develop\Python\_learn\test_pytest_pyside
collected 1 item

tests\unit\test_gui.py .                                                                      [100%]

======================================== 1 passed in 0.70s =========================================

In this way, the result of the successful test was obtained.

Summary

As mentioned above, Pytest automatically picks up the test code, and by using conftest.py, you can prepare the environment settings you want to assume in advance, so various exclusive environments can be automatically created with less code. You will be able to build with. PySide has a function called QtTest in advance, and you can see that you can simulate the behavior of Gui by using this function.

If you look closely, Pytest has a lot more features, and QtTest also has a lot of features, so I hope you can implement various tests through this startup.

Recommended Posts

Test Driven Development Startup with PySide & Pytest
Test Driven Development with Django Part 3
Test Driven Development with Django Part 6
Test Driven Development with Django Part 2
Test Driven Development with Django Part 1
Test Driven Development with Django Part 5
Unit test flask with pytest
Test standard output with Pytest
[Test Driven Development (TDD)] Chapter 21 Summary
Controlling test reruns with Luigi + pytest
Primality test with Python
Strengthen with code test ⑦
Strengthen with code test ⑨
Use Mock with pytest
Strengthen with code test ③
Strengthen with code test ⑤
Strengthen with code test ④
Primality test with python
Strengthen with code test ②
Strengthen with code test ①
Strengthen with code test ⑧
Strengthen with code test ⑨
Tutorial for doing Test Driven Development (TDD) in Flask-2 Decorators
Tutorial for doing Test Driven Development (TDD) in Flask ―― 1 Test Client