Unittests für Methoden einer Klasse

Wenn du dir nicht sicher bist, in welchem der anderen Foren du die Frage stellen sollst, dann bist du hier im Forum für allgemeine Fragen sicher richtig.
Atalanttore
User
Beiträge: 407
Registriert: Freitag 6. August 2010, 17:03

Hallo

Ich würde gerne die Methoden einer Klasse mit Unittests ausstatten, aber es funktioniert leider nicht.

main.py

Code: Alles auswählen

class App:
    def __init__(self):

        for i in range(0, 100):
            print(self._multi(i))

    def _multi(self, number):
        return number ** 2


if __name__ == "__main__":
    app = App()


test_main.py

Code: Alles auswählen

import unittest
import main


class TestApp(unittest.TestCase):

    def __setUp__(self):
        self.app = main.App()

    def test__multi(self):
        self.assertEqual(self.app._multi(5), 25)
Fehlermeldung:

Code: Alles auswählen

Testing started at 22:52 ...
/usr/bin/python3.6 /snap/pycharm-community/112/helpers/pycharm/_jb_unittest_runner.py --path /home/ata/source/unittest/test_main.py
Launching unittests with arguments python -m unittest /home/ata/source/unittest/test_main.py in /home/ata/source/unittest

Error
Traceback (most recent call last):
  File "/usr/lib/python3.6/unittest/case.py", line 59, in testPartExecutor
    yield
  File "/usr/lib/python3.6/unittest/case.py", line 605, in run
    testMethod()
  File "/home/ata/source/unittest/test_main.py", line 11, in test__multi
    self.assertEqual(self.app._multi(5), 25)
AttributeError: 'TestApp' object has no attribute 'app'



Ran 1 test in 0.001s

FAILED (errors=1)

Process finished with exit code 1
Was muss man anders machen, damit es funktioniert?

Gruß
Atalanttore
Benutzeravatar
__blackjack__
User
Beiträge: 13079
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

Wie bist Du denn auf den Namen `__setUp__()` gekommen?
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Sirius3
User
Beiträge: 17741
Registriert: Sonntag 21. Oktober 2012, 17:20

@Atalanttore: statt unittest solltest Du Dir pytest anschauen. unittest ist von der Syntax her eher Java.
Atalanttore
User
Beiträge: 407
Registriert: Freitag 6. August 2010, 17:03

__blackjack__ hat geschrieben: Dienstag 12. Februar 2019, 23:25 Wie bist Du denn auf den Namen `__setUp__()` gekommen?
Ursprünglich war das eine `__init__()`-Methode. Beim Austausch des Namens habe ich die Unterstriche dann nicht wieder entfernt :roll: . Danke für den Tipp. Ohne Unterstriche funktioniert der Test.

Code: Alles auswählen

import unittest
import main


class TestApp(unittest.TestCase):

    def setUp(self):
        self.app = main.App()

    def test__multi(self):
        self.assertEqual(self.app._multi(5), 25)

@Sirius3: Danke für den Hinweis auf 'pytest'. Kann man mit 'unittest' und 'pytest' das Gleiche machen oder muss man je nach Anwendungsfall mal das eine und mal das andere nehmen?

Gruß
Atalanttore
Benutzeravatar
__blackjack__
User
Beiträge: 13079
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Atalanttore: pytest kann auch `unittest`-Tests finden und ausführen. Umgekehrt geht nicht, weil `unittest` halt wirklich die Java-Unittest-API hat.

Edit: So könnte das mit pytest aussehen:

Code: Alles auswählen

from pytest import fixture

import main


@fixture
def app():
    return main.App()


def test_app_multi(app):
    assert app._multi(5) == 25
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Benutzeravatar
__blackjack__
User
Beiträge: 13079
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

Nachtrag: Die `__init__()` könnte man mit pytest so testen:

Code: Alles auswählen

def test_app_init(capsys):
    app = main.App()
    captured = capsys.readouterr()
    assert captured.out == '''\
0
1
4
9
16
25
36
49
64
81
100
121
144
169
196
225
256
289
324
361
400
441
484
529
576
625
676
729
784
841
900
961
1024
1089
1156
1225
1296
1369
1444
1521
1600
1681
1764
1849
1936
2025
2116
2209
2304
2401
2500
2601
2704
2809
2916
3025
3136
3249
3364
3481
3600
3721
3844
3969
4096
4225
4356
4489
4624
4761
4900
5041
5184
5329
5476
5625
5776
5929
6084
6241
6400
6561
6724
6889
7056
7225
7396
7569
7744
7921
8100
8281
8464
8649
8836
9025
9216
9409
9604
9801
'''
Allerdings ist es eher unüblich das eine `__init__()` so viele Ausgaben tätigt.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Atalanttore
User
Beiträge: 407
Registriert: Freitag 6. August 2010, 17:03

@__blackjack__: Der `pytest`-Code unten generiert bei mir auf der Konsole keinerlei Ausgaben. Auch wenn ich beide Testfälle zu einem Fehler mache oder den `@fixture`-Dekorator, der mich sehr an `@property` erinnert, einfach durch `@property` ersetze, erscheint weiterhin nichts auf der Konsole. Fehlt da noch ein Methodenaufruf?

Code: Alles auswählen

from pytest import fixture

import main


@fixture
def app():
    return main.App()


def test_app_multi(app):
    assert app._multi(5) == 25


def test_app_init(capsys):
    app = main.App()
    captured = capsys.readouterr()
    assert captured.out == '''\
0
1
4
9
16
25
36
49
64
81
100
121
144
169
196
225
256
289
324
361
400
441
484
529
576
625
676
729
784
841
900
961
1024
1089
1156
1225
1296
1369
1444
1521
1600
1681
1764
1849
1936
2025
2116
2209
2304
2401
2500
2601
2704
2809
2916
3025
3136
3249
3364
3481
3600
3721
3844
3969
4096
4225
4356
4489
4624
4761
4900
5041
5184
5329
5476
5625
5776
5929
6084
6241
6400
6561
6724
6889
7056
7225
7396
7569
7744
7921
8100
8281
8464
8649
8836
9025
9216
9409
9604
9801
'''
Gruß
Atalanttore
Benutzeravatar
__blackjack__
User
Beiträge: 13079
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Atalanttore: Wie führst Du die Tests denn aus? Das Modul selbst ist ja nicht ausführbar.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Atalanttore
User
Beiträge: 407
Registriert: Freitag 6. August 2010, 17:03

@__blackjack__: Ich führe die Tests in PyCharm aus wie hier beschrieben. Die Tests werden von PyCharm auch erkannt (grüner Startbutton erscheint links neben jedem Test), aber es gibt leider keine Ausgabe über das Testergebnis.

Gruß
Atalanttore
Benutzeravatar
__blackjack__
User
Beiträge: 13079
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Atalanttore: Dann ist das wohl mehr eine PyCharm-Frage. Wie bei Programmen selbst auch: Ich führe das immer ohne eine IDE aus.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Atalanttore
User
Beiträge: 407
Registriert: Freitag 6. August 2010, 17:03

@__blackjack__: Wie machst du das genau?

Gruß
Atalanttore
Benutzeravatar
__blackjack__
User
Beiträge: 13079
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Atalanttore: Ich rufe pytest auf, wie in dessen Dokumentation beschrieben.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Atalanttore
User
Beiträge: 407
Registriert: Freitag 6. August 2010, 17:03

@__blackjack__: `pytest` aufzurufen ging ziemlich einfach. Im Verzeichnis der Tests ein Terminal öffnen, `pytest` eintippen und mit Enter bestätigen. Danach liefen die Tests durch.

Dein `pytest`-Code funktionierte wegen eines Bugs in PyCharm nicht auf Anhieb. Mittlerweile habe ich vom JetBrains Support aber einen Workaround (run configuration für Testdatei entfernen) erhalten.
Bug: https://youtrack.jetbrains.com/issue/PY-30052

Gruß
Atalanttore
Atalanttore
User
Beiträge: 407
Registriert: Freitag 6. August 2010, 17:03

Andere Frage: Gibt es zur Kommentierung der einzelnen Testfälle in einem `pytest` eigene Ausdrücke oder nutzt man dafür einfache Python-Kommentare?

Gruß
Atalanttore
Benutzeravatar
__blackjack__
User
Beiträge: 13079
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Atalanttore: Man kann genau wie bei Code auch DocStrings verwenden und das dann auch wie normale Dokumentation weiterverarbeiten.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Atalanttore
User
Beiträge: 407
Registriert: Freitag 6. August 2010, 17:03

@__blackjack__: Okay. Dann weiß ich wieder etwas mehr.


Ich habe nun die Klasse `App` um einen Parameter erweitert, der bei der Instanziierung eines Objektes übergeben werden muss. In der Testdatei (nennt man das so?) habe ich für diesen Parameter einen Wert global definiert. Ist das ein guter Stil?

main.py:

Code: Alles auswählen

class App:
    def __init__(self, exponent):
        self.factor = exponent
        for i in range(0, 10):
            print(self._multi(i, exponent))

    def _multi(self, number, factor):
        return number ** factor


if __name__ == "__main__":
    app = App(10)
test_main.py:

Code: Alles auswählen

from pytest import fixture

import main

exponent = 10

@fixture
def app():
    return main.App(exponent)


def test_app_multi(app):
    assert app._multi(5, exponent) == 9765625


def test_app_init(capsys):
    app = main.App(exponent)
    captured = capsys.readouterr()
    assert captured.out == '''\
0
1
1024
59049
1048576
9765625
60466176
282475249
1073741824
3486784401
'''

Gruß
Atalanttore
Benutzeravatar
__blackjack__
User
Beiträge: 13079
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Atalanttore: Auch in Modulen mit Unittests sollte man sich an die üblichen Konventionen halten und Konstanten komplett gross schreiben. Die Module selbst würde ich mal sagen haben keinen speziellen Namen, denn Unittests müssen ja auch nicht zwingend in extra Modulen stecken. Es gibt auch Leute die sie gleich in die Module mit dem Code hinein schreiben. Mache ich zum Beispiel meistens wenn das kleine Werkzeug sowieso nur aus einem Modul, also eigentlich einer Skriptdatei besteht, und nicht schon zu lang ist.

Ob `EXPONENT` nun semantisch tatsächlich eine (sinnvolle) Konstante ist, kann man schlecht sagen, weil dieses Beispiel an sich ja gar keinen Sinn hat, ausser halt ein Beispiel zu sein. Ist das denn nur für die Tests ein besonderer, konstanter Wert?
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Atalanttore
User
Beiträge: 407
Registriert: Freitag 6. August 2010, 17:03

@__blackjack__: Wie lang ist "nicht schon zu lang"? Ist eine Skriptdatei mit 500 Codezeilen schon zu lang?

`EXPONENT` bzw. `exponent` ist nur ein Beispielwert, weil man beim Instanziieren eines Objektes der Klasse `App` ja einen Wert dafür angeben muss.

Gruß
Atalanttore
Atalanttore
User
Beiträge: 407
Registriert: Freitag 6. August 2010, 17:03

Momentan komme ich bei der Ergründung der Ursache einer Fehlermeldung bei einem pytest nicht voran. Auf Stack Overflow schrieb jemand zu der Fehlermeldung, dass der Speicher ausgeht, aber der Arbeitsspeicher meines Rechners (8 GB) ist nur bis zur Hälfte belegt.

Fehlermeldung:

Code: Alles auswählen

Testing started at 18:35 ...
/usr/bin/python3.6 /snap/pycharm-community/112/helpers/pycharm/_jb_pytest_runner.py --path /home/ata/source/test_bmi.py
Launching pytest with arguments /home/ata/source/test_bmi.py in /home/ata/source

============================= test session starts ==============================
platform linux -- Python 3.6.7, pytest-4.2.1, py-1.7.0, pluggy-0.8.1
rootdir: /home/ata/source, inifile:collected 1 item

test_bmi.py 
Process finished with exit code 134 (interrupted by signal 6: SIGABRT)

test_bmi.py:

Code: Alles auswählen

from pytest import fixture

import main


@fixture
def main_window():
    return main.MainWindow()


def test_bmi_main_window(main_window):
    assert main_window._calculate_BMI(180, 80) == 24.691358024691358

bmi.py:

Code: Alles auswählen

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import sys
from PyQt5.QtWidgets import QApplication, QMainWindow
from PyQt5.uic import loadUi


class MainWindow(QMainWindow):
    def __init__(self, parent=None):
        super().__init__(parent)
        loadUi("bmi.ui", self)

        self._write_size()
        self._write_weight()
        self._write_BMI()

        self.horizontalSlider_size.sliderMoved.connect(self._write_BMI)
        self.horizontalSlider_size.sliderMoved.connect(self._write_size)

        self.horizontalSlider_weight.sliderMoved.connect(self._write_BMI)
        self.horizontalSlider_weight.sliderMoved.connect(self._write_weight)

    @property
    def _size(self):
        return self.horizontalSlider_size.value()

    @property
    def _weight(self):
        return self.horizontalSlider_weight.value()

    def _write_size(self):
        self.output_size.setText(f"{self._size} cm")

    def _write_weight(self):
        self.output_weight.setText(f"{self._weight} kg")

    def _calculate_BMI(self, size, weight):
        return weight / (size / 100) ** 2

    def _write_BMI(self):
        BMI = self._calculate_BMI(self._size, self._weight)
        self.output_BMI.setText(str(round(BMI, 2)))


def main():
    app = QApplication(sys.argv)
    main_window = MainWindow()
    main_window.show()
    sys.exit(app.exec_())


if __name__ == '__main__':
    main()
bmi.ui:

Code: Alles auswählen

<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
 <class>MainWindow</class>
 <widget class="QMainWindow" name="MainWindow">
  <property name="geometry">
   <rect>
    <x>0</x>
    <y>0</y>
    <width>800</width>
    <height>600</height>
   </rect>
  </property>
  <property name="windowTitle">
   <string>Berechnungen</string>
  </property>
  <widget class="QWidget" name="centralwidget">
   <widget class="QTabWidget" name="tabWidget">
    <property name="geometry">
     <rect>
      <x>50</x>
      <y>40</y>
      <width>681</width>
      <height>451</height>
     </rect>
    </property>
    <property name="currentIndex">
     <number>0</number>
    </property>
    <widget class="QWidget" name="tab_2">
     <attribute name="title">
      <string>BMI</string>
     </attribute>
     <widget class="QFrame" name="frame">
      <property name="geometry">
       <rect>
        <x>50</x>
        <y>50</y>
        <width>591</width>
        <height>151</height>
       </rect>
      </property>
      <property name="frameShape">
       <enum>QFrame::StyledPanel</enum>
      </property>
      <property name="frameShadow">
       <enum>QFrame::Raised</enum>
      </property>
      <layout class="QGridLayout" name="gridLayout">
       <item row="0" column="0">
        <widget class="QLabel" name="label_size">
         <property name="text">
          <string>Größe:</string>
         </property>
        </widget>
       </item>
       <item row="0" column="1" colspan="3">
        <widget class="QSlider" name="horizontalSlider_size">
         <property name="toolTipDuration">
          <number>-1</number>
         </property>
         <property name="minimum">
          <number>100</number>
         </property>
         <property name="maximum">
          <number>250</number>
         </property>
         <property name="sliderPosition">
          <number>175</number>
         </property>
         <property name="orientation">
          <enum>Qt::Horizontal</enum>
         </property>
         <property name="tickPosition">
          <enum>QSlider::NoTicks</enum>
         </property>
        </widget>
       </item>
       <item row="0" column="4">
        <widget class="QLabel" name="output_size">
         <property name="text">
          <string>x cm</string>
         </property>
        </widget>
       </item>
       <item row="1" column="0" colspan="2">
        <widget class="QLabel" name="label_weight">
         <property name="text">
          <string>Gewicht:</string>
         </property>
        </widget>
       </item>
       <item row="1" column="2">
        <widget class="QSlider" name="horizontalSlider_weight">
         <property name="minimum">
          <number>1</number>
         </property>
         <property name="maximum">
          <number>300</number>
         </property>
         <property name="singleStep">
          <number>1</number>
         </property>
         <property name="sliderPosition">
          <number>80</number>
         </property>
         <property name="orientation">
          <enum>Qt::Horizontal</enum>
         </property>
         <property name="tickPosition">
          <enum>QSlider::NoTicks</enum>
         </property>
        </widget>
       </item>
       <item row="1" column="3" colspan="2">
        <widget class="QLabel" name="output_weight">
         <property name="text">
          <string>x kg</string>
         </property>
        </widget>
       </item>
      </layout>
     </widget>
     <widget class="QWidget" name="layoutWidget">
      <property name="geometry">
       <rect>
        <x>257</x>
        <y>220</y>
        <width>191</width>
        <height>41</height>
       </rect>
      </property>
      <layout class="QHBoxLayout" name="horizontalLayout_2">
       <item>
        <widget class="QLabel" name="label_BMI">
         <property name="text">
          <string>BMI:</string>
         </property>
        </widget>
       </item>
       <item>
        <widget class="QLabel" name="output_BMI">
         <property name="text">
          <string/>
         </property>
        </widget>
       </item>
      </layout>
     </widget>
    </widget>
   </widget>
  </widget>
  <widget class="QStatusBar" name="statusbar"/>
 </widget>
 <resources/>
 <connections/>
</ui>
Gruß
Atalanttore
__deets__
User
Beiträge: 14529
Registriert: Mittwoch 14. Oktober 2015, 14:29

Und du benutzt auch die 64 Bit Version von Python, damit der Adressraum über die mit 32 Bit verfügbaren 4GB hinaus geht....?
Antworten