Methoden zusammenfassen, Code kürzen

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.
Antworten
Moses4python
User
Beiträge: 13
Registriert: Donnerstag 16. Mai 2019, 20:43

Hallo Zusammen,

ich hoffe, die Fragen die nun folgen, sind noch nicht gestellt worden, sonst wäre es super, wenn ihr mir einfach einen Link teilen könnt. Ich kann mir gut vorstellen, dass das bereits ein Thema gewesen ist aber mit den von mir verwendeten Keywords, bin ich nicht so richtig fündig geworden und wäre sehr glücklich über eine Hilfe, wo ich ggf. schauen muss oder suchen kann...

Hintergrund:
Ich 'versuche' eine Testsuite für Requests Abfragen zu schreiben. Der Test läuft durch aber mir gefällt der Code noch nicht.
Nur falls die Frage aufkommt, ich möchte die Ergebnisse die ich mit Selenium erhalte, mit den Ergebnissen von Requests vergleichen.

Nun habe ich also für mein Prove of Concept einen 'dummy Code' entwickelt und möchte diesen nun optimieren.

Problem/ Optimierungsbedarf:
(Da der Code zu lange wäre, um diesen hier zu teilen, ein Beispiel)
Nun sieht die Logik meines Codes wie folgt aus:

Code: Alles auswählen

class testSuite():
    
    def test1():
            print("something")
    def test2():
            print("something")
    def test3():
            print("something")

ff = testSuite()
ff.test1()
ff.test2()
ff.test3()
...
der Code öffnet eine csv Datei
schreibt jede aufgerufene URL über die append funktion dann ein,
öffnet eine txt datei und schreibt dort die Seitenspezifischen Daten bspw. Url oder response Status rein.

Soweit so gut. Das ganze klappt dann auch mit py.test oder unittest usw.

aber auf dieser Art und Weise bin ich schnell bei mehreren hunderten von Zeilen Code, was ja ganz bestimmt nicht the 'state of the art' ist oder wie es so schön heißt 'pythonic'.
Also was wäre ein eleganterer weg?

Natürlich könnte ich die URLs direkt aus einer CSV Datei abrufen, gemäß der Logik:

sites = ['alpha', 'beta', 'gamma']

for site in sites:
print(sites[0])
break

aber dann müsste a) immer auch die Position angeben werden und würde die Länge des Codes nicht wesentlich verringern. Zudem machen die Methoden für jeden einzelnen Schritt in meinem jetzigen Stadium durchaus Sinn, da ich dann schneller weiß, wo der Fehler ist und es ist leichter zu debuggen. Oder habe ich hier einen Denkfehler? Deswegen war meine Überlegung, ob es nicht eine andere Möglichkeit gibt, bspw. die ganzen Methoden als Liste zusammenzufassen.

Kann man etwas schreiben wie main = [def test01, def test02, def test03]? Ich habe das noch nie gesehen, und es sieht irgendwie "verboten" aus.

Über Tips oder Ideen würde ich mich sehr freuen.
Vielen Dank.

Informationen:
Python Version: 3.7
OS: Windows10/ Linux Ubuntu 16.04
IDE: PyCharm Education Edition 2019


PS: Ich bin neu indem Forum, sollte ich irgendeine Regel übersehen haben oder etwas besser machen können, gebt bitte Bescheid, damit es beim nächsten mal besser machen kann.
Benutzeravatar
__blackjack__
User
Beiträge: 13077
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Moses4python: Ich verstehe das Problem nicht. Beim ersten Beispiel ist das Erstellen von `testSuite` (was nicht mit einem Kleinbuchstaben anfangen sollte) und das aufrufen der Methoden ja nicht Deine Aufgabe. Das macht der Testrunner für Dich:

Code: Alles auswählen

class TestSuite:
    
    def test1(self):
        print("something")
    
    def test2(self):
        print("something")
    
    def test3(self):
        print("something")
Tests laufen lassen:

Code: Alles auswählen

$ pytest -v forum15.py
============================= test session starts ==============================
platform linux -- Python 3.6.7, pytest-3.8.0, py-1.6.0, pluggy-0.7.1 -- /usr/bin/python3
cachedir: .pytest_cache
rootdir: /home/bj, inifile:
collected 3 items                                                              

forum15.py::TestSuite::test1 PASSED                                      [ 33%]
forum15.py::TestSuite::test2 PASSED                                      [ 66%]
forum15.py::TestSuite::test3 PASSED                                      [100%]

=========================== 3 passed in 0.02 seconds ===========================
Was Du mit ``main = […`` meinst, verstehe ich nicht wirklich. Man kann Tests zu Testsuiten zusammenstellen wenn man Gruppen von Tests einzeln ausschliessen oder laufen lassen will.

Was die Datei mit URLs angeht: So etwas ist natürlich auch möglich. Das mit der Position habe ich allerdings nicht verstanden‽

So ganz allgemein würden mir bei `pytest` zum Kürzen vom Code die Stichpunkte „Fixtures“ und „parametrisierte Tests“ einfallen.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Moses4python
User
Beiträge: 13
Registriert: Donnerstag 16. Mai 2019, 20:43

Hi @__blackjack__;

vielen dank für deine Antwort. Das hat mir schon einmal sehr geholfen.

1.) Zur Schreibweise, ich gebe zu wenn man der pep folgt, dann hast du recht. Die Schreibweise habe ich von einem Dozenten von der Plattform udemy übernommen. Da es funktioniet habe ich mir nicht soviele Gedanken gemacht. Ich glaube, er ist eigentlich Java Programmierer, kann es daran liegen, dass er diese Schreibweise so verwendet hat? Aber danke für die Anregung, dass kann ich ja schnell ändern.

2.) Mir kommt der test Code umständlich und lang vor, mache ich dass denn richtig?

Code: Alles auswählen

import requests
import time
import pytest
import unittest


@pytest.yield_fixture()
def setUp():
    print("Running response tests setUp")
    yield
    print("Running response tests tearDown")

@pytest.mark.usefixtures("setUp")
class responseFetcher(unittest.TestCase):

    @pytest.mark.run(order=1)
    def test01(setUp):
        url = 'http://www.example.com'
        r = requests.get(url)
        time.sleep(5)
        print("URL: ", r.url)
        print("Status Code: ",  r.status_code, r.history)
        print("#############################################")
        print("Header: ", r.headers)
        print("#############################################")
        print("Links: ", r.links)
        print("#############################################")
        if r.status_code == '200':
            assert r.status_code == True
        else:
            pass
        print("############## END ---> NEXT ################")
Der Code läuft durch.
Wenn ich aber nun 100 Webseiten so überprüfen möchte, ist es dann noch 'cleaner code'?
Oder würde man das aus professioneller Sicht anders machen?
Ich habe das nun für 20 URLs gemacht und bin bei knapp 400 Zeilen Code.

Würdest Du hier alle Seiten als einen Test verbauen oder würdest du bei einer Anzahl von 100 Webseiten, dass in beispielsweise 10er "Päckchen" aufteilen?

Gut, ich habe mir da einige Print Befehle eingebaut, da ich dann beim Aufruf von allen Tests einen besseren Überblick (noch) habe, wenn ich den Test über die Konsole laufen lasse. Was den Code vielleicht auch unnötig lang macht. Ich habe überlegt, ob ich stattdessen besser das logging package verwenden sollte, um mir autologs ausgeben zulassen. Sollte dann ein Problem auftreten, würde ich es auch darüber sehen und könnte im Umkehrschluss auf einige print Befehle verzichten. Das würde den Code hier verringern.

Denn die Frage und das Problem war ja, wie kann ich den Code schlanker machen, reduzieren und optimieren.
Meine Überlegung und Frage, die ich hatte bezüglich Methoden in arrays bzw. Listen zusammenfassen hat sich nun erst einmal erledigt, durch deine Antwort mit pytest.

3. Zur Position:

Code: Alles auswählen

sites = ['alpha', 'beta', 'gamma']

for site in sites:
    print(sites[1])
    break
    Output>> beta 
mit Postion meinte ich hier = 'print(sites[1]) -> 'beta' = print(sites[2]) - >'gamma' ... würde ich also auf diese Art und Weise mit Webseiten umgehen, müsste ich doch meinem Verständnis nach immer [1] oder [2] etc. angeben, damit mein Programm weiß, welche URL es abrufen soll oder habe ich da etwas falsch verstanden?
Wenn ich es richtig verstanden habe, müsste ich also auch hier für jede Webseite den Befehl schreiben und kann dann auch gleich url = ' xy' jedesmal schreiben...

Wenn man schreibt:

Code: Alles auswählen

sites = ['alpha', 'beta', 'gamma']

for site in sites:
    print(sites)
erhält man:

Code: Alles auswählen

['alpha', 'beta', 'gamma']
und ich habe es auch versucht, es hat nicht funktioniert. Ich verstehe allerdings auch nicht so recht warum es nicht funktioniert, denn unter Linux und Python 2.7 funktioniert es einwandfrei... wie auch immer... das meinte ich eben mit Position (Was wäre denn der richtige Begriff im deutschen?).
Vielen Dank erst mal fürs durchlesen und deine Hilfe.
__deets__
User
Beiträge: 14528
Registriert: Mittwoch 14. Oktober 2015, 14:29

BlackJack hat bereits parametrisierte tests erwähnt. Aber selbst wenn du die nicht nutzen kannst/willst, kannst du den gemeinsamen Code inneren Funktion/Methode stopfen, und dann entweder

Code: Alles auswählen

for url in METRIC_SHIT_TON_OF_URLS:
    tuwas(url)
Benutzen oder einzelne test_a, test_b ... Methoden schreiben die dann tuwas aufrufen. Das aber nur wenn du die zb geziehlt aufrufen willst durch den Test Runner.
Moses4python
User
Beiträge: 13
Registriert: Donnerstag 16. Mai 2019, 20:43

Hallo Zusammen,

vielen Dank nochmals.
Von
'parametisierten Tests'
habe ich noch nichts gehört, die gehören zum unittestpackage, richtig?
Dann schau ich hier noch einmal in die Dokumentation. Sind parametisierte tests, dasselbe wie im englischen Subtests? Oder nach welchen Keywords, suche ich am besten in diesem Zusammenhang?
Bin ich dann hier richtig: https://docs.python.org/3/library/unitt ... g-subtests

Vielen, vielen Dank nochmals und ein schönes Wochenende allerseits.
__deets__
User
Beiträge: 14528
Registriert: Mittwoch 14. Oktober 2015, 14:29

Afaik kann unittest keine paramterisierten Tests. Aber pyTest.

http://doc.pytest.org/en/latest/parametrize.html
Benutzeravatar
__blackjack__
User
Beiträge: 13077
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Moses4python: Das scheint so etwas in der Richtung zu sein, ich benutze das `unittest`-Modul aber überhaupt nicht. Das ist mir zu javaesque und zu umständlich. Gerade wenn man Code kürzen möchte, sollte man da IMHO die Finger von lassen. Und das was Du da geschrieben hast sieht auch etwas ”fett” aus.

Bin mir auch nicht so ganz sicher ob man eine `setUp()`-Funktion als `yield_fixture` nehmen sollte. Bei Tests läuft ja viel über Namenskonventionen und `setUp()` wäre etwas was, nun ja, eine `setUp()`-Funktion wäre, also der erste Teil von so einem `yield_fixture`, das nach dem ``yield`` wäre das was in einer `tearDown()`-Funktion stehen würde. Ich wäre jedenfalls nicht überrascht wenn das Probleme bereiten würde dass das zu oft ausgeführt wird. Oder auch keine Probleme, weil es ja keinen Effekt hat, solange man kein `next()` auf dem Ergebnis aufruft.

Klassennamen klein anzufangen ist auch keine Java-Konvention. Bei Klassen hat Java die gleiche Konvention wie Python: MixedCase. Manchmal auch PascalCase genannt. Klassennamen in camelCase sind mir ehrlich gesagt noch *nie* untergekommen, bei keiner Sprache und keiner Richtlinie. Bei Unittests ist das bei Methodennamen ja auch nur weil diese API ursprünglich von der SUnit-Bibliothek von Smalltalk kommt und von da nach Java, C++, C# und weitere Sprachen portiert wurde, und von Java kam das dann auch nach Python. Das war am Anfang im Grunde eine 1:1-Kopie von JUnit. Was neben den unkonventionellen Namen halt auch mit sich bringt das es sich nicht so anfühlt als wäre es für eine Sprache wie Java und nicht für Python.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Benutzeravatar
__blackjack__
User
Beiträge: 13077
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Moses4python: Dein Test ist inhaltlich auch kaputt weil der niemals fehlschlagen kann, weil `status_code` niemals '200' sein wird. Das kann 200 sein – also eine Zahl und keine Zeichenkette. Wenn Du darauf prüfst ist der Test aber immer noch kaputt, weil er dann immer fehlschlägt wenn der Status 200, also eigentlich Ok ist, denn wenn `status_code` 200 ist, kann er ja nicht *gleichzeitig* auch `True` sein. Möchtest Du hier auf 200 testen? Oder generell ob die Anfrage positiv beantwortet wurde, also ein `status_code` aus der 100er, 200er, oder 300er Gruppe kam?

`pytest.yield_fixture` ist veraltet und durch `pytest.fixture` ersetzt.

Das das erste Argument Deiner Methode nicht `self` heisst ist falsch. Wie bist Du Darauf gekommen das `setUp` zu nennen?

Warum hast Du dieses sinnfreie Fixture dort überhaupt im Code wenn es doch möglichst schlank sein soll?

Was da steht kann ohne `unittest` und ohne Klasse auch einfach so schreiben:

Code: Alles auswählen

import requests
import pytest


@pytest.mark.run(order=1)
def test01():
    url = 'http://www.example.com'
    response = requests.get(url)
    time.sleep(0.001)
    print('URL: ', response.url)
    print('Status Code:', response.status_code, response.history)
    print('#############################################')
    print('Header: ', response.headers)
    print('#############################################')
    print('Links: ', response.links)
    print('#############################################')
    assert response.ok
    print('############## END ---> NEXT ################')
Und wenn Du das für mehr als eine URL in jeweils einem eigenen Test machen willst, dann lagerst Du alles bis auf die URL halt in eine eigene Funktion aus (wir stellen uns in `test02()` jetzt einfach mal eine andere URL vor :-)):

Code: Alles auswählen

import time

import requests
import pytest


def _check_response(url):
    response = requests.get(url)
    time.sleep(0.001)
    print('URL: ', response.url)
    print('Status Code: ', response.status_code, response.history)
    print('#############################################')
    print('Header: ', response.headers)
    print('#############################################')
    print('Links: ', response.links)
    print('#############################################')
    assert response.ok
    print('############## END ---> NEXT ################')


@pytest.mark.run(order=1)
def test01():
    _check_response('http://www.example.com')


@pytest.mark.run(order=1)
def test02():
    _check_response('http://www.example.com')
Oder mit `pytest.mark.parametrize`:

Code: Alles auswählen

import time

import requests
import pytest


@pytest.mark.run(order=1)
@pytest.mark.parametrize(
    'url', ['http://www.example.com', 'http://www.example.com']
)
def test_response(url):
    response = requests.get(url)
    time.sleep(0.001)
    print('URL: ', response.url)
    print('Status Code: ', response.status_code, response.history)
    print('#############################################')
    print('Header: ', response.headers)
    print('#############################################')
    print('Links: ', response.links)
    print('#############################################')
    assert response.ok
    print('############## END ---> NEXT ################')
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Moses4python
User
Beiträge: 13
Registriert: Donnerstag 16. Mai 2019, 20:43

Hallo _blackjack__, hey _deeds_,

vielen Dank für eure Lösungsvorschläge.
Die haben mir ganz gut weitergeholfen.

Ich wünsche euch eine angenehme Woche,
bis dahin.
Antworten