3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

PySide & Pytest での テスト駆動開発 スタートアップ

Posted at

こちらは、以前(2021/01/10)に別サービスで展開した内容を Qiita に移行したものです。

はじめに

PySide勉強会での、「PySide & Pytest で テスト駆動開発スタートアップ」の補足記事。
および、PySideとPytestでテスト駆動開発をするためのメモ。

ay82u5stv7ygo5uvotgtfvz6es86.png

開発環境

Windows 10
Python 3.7.7
Pytest

サンプルファイル

GitHub

初期整備

ディレクトリとファイルの準備

ディレクトリ
root
  |- sample
  |    |- __init__.py
  |    |- gui.py
  |- tests
  |    |- __init__.py
  |    |- conftest.py
  |    |- unit
  |        |- test_gui.py
  |- requirments.txt

ここでのツールのソースとなるgui.pyは以下のようなものを用意した。

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()
    

実行した際の見た目は、次の様になる。

uaikyvvxwi8jgnq9klca6wibg54g.png

requirements.txt に必要モジュールを書く

PySide2
Pytest

venv環境を作成する

  1. rootディレクトリに行く
  2. python -m venv .venv
  3. venv環境を有効にする: .venv\Script\Activate.bat
  4. pip install -r requirements.txt

conftest.pyでモジュールへのパスをつなぐ

tests/conftest.py
import os
import sys

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

Pytest公式では、setup.pyを用いた、pip install -e .を使って、インストールする事が推奨されているが、それぞれの環境により(僕の場合は、Techcnial Artistチームの(DCCツールも含めた)ツールリリース環境の場合)、必ずしもpip環境を提供するわけにはいかない場合がある。

そういった場合に、余計なディレクトリ構成を取りたくない事があるので、conftest.pyを使用して、Pytestの実行時にrootにパスを通しておく。

もちろん、 pip install を前提とした配布環境の場合は pip intall -e . を出来るように、 setup.pyをroot下に配置し、 pip install 出来るようにしておくと良い。

また、同様に、Mayaなどの外部ツールや社内ライブラリなどで必要なモジュールがあるみたいな時には、ここでパスを通しに行くと良い。

テストコードの実装

Pytestでのテストファイル

とりあえず基本的な挙動で覚えておくのは以下のところ。
(実際はもっとあるのでドキュメント参照されたし)

  • Pytestは、test_*.py や *_test.pyというファイルを検索して、自動で取得しにいく。
  • さらに、そのファイルの中のtest_*という関数を探して実行する。
  • さらに、その関数をまとめたい場合には、Test*(Ex: TestGui)といったクラスを作ると、これもまたPytestが自動で拾いに行ってくれる。

テストを書いていく

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

QtTestを使う

ここで出てくるのが、QtTestで、PySideはこうしてテスト用のモジュールを提供してくれている。

ユーザーは、「『ボタンをクリックすることによって』、ラベルの数字が1上がる」という事を認識するので、この動作が正しくなるように、『ボタンをクリックすることによって』というのをテスト上でシミュレートする必要がある。
ここでQtTestは、 QtTest.QTest.mouseClick() を使う事によって、その動作をシミュレートする機能を提供する。

つまり、上記のtest_add_countでは、次のことをQtTestで行っている

  1. 最初のQtTest.QTest.mousClicke()で、「guiのボタンを左マウスクリックする」
  2. その時のnumberクラス変数の値をn1として格納する
  3. 次のQtTest.QTest.mousClicke()で、「guiのボタンを左マウスクリックする」
  4. 二回目のクリックした状態でのnumberクラス変数の値をn2として格納する
  5. assertでその二つの格納した値を比較し、差が1であることを確認する

これにより、 ボタンをクリックした際の変更が、正しくカウントの上昇が1づつであるということを保証することができる

テストコマンドを実行する

テストを実行する際のコマンドをルートディレクトリでとりあえずこれを実行すればいい。

pytest .

すると、次の様な結果が得られる。

======================================= 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 =========================================

これを説明すると、
テスト上のコードを見ての通り、add_countは、最初の実行と次の実行時の値の差が1であってほしいという開発者の意図があるのがわかる
しかしながら、ソースコードを見てみると、 self.number += 2 としてあり、開発者の意図に反して、『ボタンをクリックすることによって』実行された処理の結果の差が2の結果を出してしまっていることが分かる。

では、この self.number += 1 に変え、「1づつ増える(つまり常に増える差は1)」にしてみると、pytest . を実行した際に、次の結果が得られる。

======================================= 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 =========================================

この様にして、テストの成功の結果が得られた。

まとめ

Pytestは先のように、自動でテストコードを拾ってきたり、conftest.pyを使用することによって、あらかじめ前提としたい環境設定も用意することができるので、様々な排他的環境を少ないコードで、自動で構築できるようになる。
PySideは、QtTestという機能をあらかじめ用意しており、これを使用することでGuiの挙動をシミュレートできることがわかる。

よくよく調べていくと、Pytestにももっともっと機能が豊富にあり、QtTestも同じく機能をたくさん持っているので、このスタートアップを経て、様々なテスト実装を行えて良ければと考える。

3
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?