こちらは、以前(2021/01/10)に別サービスで展開した内容を Qiita に移行したものです。
はじめに
PySide勉強会での、「PySide & Pytest で テスト駆動開発スタートアップ」の補足記事。
および、PySideとPytestでテスト駆動開発をするためのメモ。
開発環境
Windows 10
Python 3.7.7
Pytest
サンプルファイル
初期整備
ディレクトリとファイルの準備
root
|- sample
| |- __init__.py
| |- gui.py
|- tests
| |- __init__.py
| |- conftest.py
| |- unit
| |- test_gui.py
|- requirments.txt
ここでのツールのソースとなる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()
実行した際の見た目は、次の様になる。
requirements.txt に必要モジュールを書く
PySide2
Pytest
venv環境を作成する
- rootディレクトリに行く
python -m venv .venv
- venv環境を有効にする:
.venv\Script\Activate.bat
pip install -r requirements.txt
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が自動で拾いに行ってくれる。
テストを書いていく
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で行っている
- 最初のQtTest.QTest.mousClicke()で、「guiのボタンを左マウスクリックする」
- その時のnumberクラス変数の値をn1として格納する
- 次のQtTest.QTest.mousClicke()で、「guiのボタンを左マウスクリックする」
- 二回目のクリックした状態でのnumberクラス変数の値をn2として格納する
- 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も同じく機能をたくさん持っているので、このスタートアップを経て、様々なテスト実装を行えて良ければと考える。