Auf dieser Seite
Benutzerdefinierte Selenium Webdrivers mit Decorators
Die Hauptfrage:
Wie erstellt man dynamisch benutzerdefinierte Selenium WebDriver-Klassen mit einem parametrisierten Browser-Binary-Pfad?
Die Motivation:
Die Durchführung von funktionalen Website-Tests mit dem selenium WebDriver führt dazu, dass man Tests mit verschiedenen Versionen der heute am häufigsten verwendeten Browser parallel auf derselben Maschine ausführen möchte.
Tests können parametrisiert werden, wobei der Parameter die Driver-Klasse selbst sein kann, und hier entsteht die Notwendigkeit, Treiberklassen dynamisch zu generieren und sie als Parameter an die Tests zu übergeben.
Die Idee:
Um mehr als eine Browser-Version verwenden zu können, muss man zunächst diese Versionen lokal installieren, dann einen benutzerdefinierten WebDriver-Klassengenerator mit einem benutzerdefinierten Binary-Pfad erstellen und diese generierten Klassen in den Tests verwenden, ohne sie zu initialisieren.
Die Vorgehensweise:
Ich werde die Schritte erklären, denen ich bei den heute beliebtesten Browsern gefolgt bin: Mozilla Firefox und Google Chrome.
-
Laden Sie die Firefox-Versionen, die Sie für Tests verwenden möchten, vom Mozilla-Releases-Archiv herunter.
-
Bei Chrome könnte es etwas Aufwand erfordern, aber die Anweisungen sind auf der Chromium-Projektseite klar. Ich werde sie hier auflisten, falls sie verloren gegangen sind:
- Suchen Sie in googlechromereleases.blogspot.com nach dem letzten Mal, als die gewünschte Version erwähnt wurde.
- Schlagen Sie diese Versionshistorie (z.B.: “44.0.2403.157”) in der Position Lookup nach
- In diesem Fall gibt es eine Basisposition von “330231” zurück. Dies ist der Commit, bei dem die 44-Version im Mai 2015 abgezweigt wurde.*
- Öffnen Sie das Archiv der Continuous Builds
- Klicken Sie auf Ihre Plattform (Linux/Mac/Win)
- Fügen Sie “330231” in das Filterfeld oben ein und warten Sie, bis alle Ergebnisse per XHR geladen sind.
- Schließlich erhalte ich einen perfekten Treffer: commondatastorage.googleapis.com/chromium-browser-snapshots
- Manchmal müssen Sie die Commit-Nummer decrementieren, bis Sie eine finden.
- Herunterladen und ausführen!
-
Jetzt haben Sie alle Ihre Versionen bereit und können ausgeführt werden. Es gibt viele Optionen, wo Sie sie speichern können, damit sie systemweit zugänglich sind. Eine Option wäre, sie in Umgebungsvariablen zu speichern.
-
Schreiben Sie einen benutzerdefinierten WebDriver-Klassengenerator, bei dem der Binary-Pfad ein Parameter ist und in Selenium-Tests dynamisch geändert werden kann. Aber das erste Problem, auf das ich hier stieß, war die Möglichkeit, die
__init__-Methode für unsere benutzerdefinierte Klasse zu ändern und den benutzerdefinierten Binary-Pfad zu übergeben. Ich stellte fest, dass dies durch Ableitung von der Klassetypemöglich ist:
binary = FirefoxBinary(bin_path)
def __init__(self, firefox_binary=binary):
webdriver.Firefox.__init__(self, firefox_binary=firefox_binary)
Firefox = type('Firefox', (webdriver.Firefox, ), {'__init__': __init__})
- Für Google Chrome wird der Binary-Pfad etwas anders geändert:
chrome_options = Options()
chrome_options.binary_location = bin_path
def __init__(self, chrome_options=chrome_options):
webdriver.Chrome.__init__(self, chrome_options=chrome_options)
Chrome = type('Chrome', (webdriver.Chrome, ), {'__init__': __init__})
- Es wäre praktischer, diese Funktionalität in Decorators unterzubringen. Zu diesem Zweck schrieb ich zwei Decorators, einen zur Generierung von Firefox WebDriver-Objekten:
class CustomFirefox:
def __init__(self, bin_path):
self.bin_path = bin_path
def __call__(self, cls):
binary = FirefoxBinary(self.bin_path)
def __init__(self, firefox_binary=binary):
webdriver.Firefox.__init__(self, firefox_binary=firefox_binary)
Firefox = type('Firefox', (webdriver.Firefox, ), {'__init__': __init__})
return Firefox
und einen anderen für Chrome WebDriver-Objekte:
class CustomChrome:
def __init__(self, bin_path):
self.bin_path = bin_path
def __call__(self, cls):
chrome_options = Options()
chrome_options.binary_location = self.bin_path
def __init__(self, chrome_options=chrome_options):
webdriver.Chrome.__init__(self, chrome_options=chrome_options)
Chrome = type('Chrome', (webdriver.Chrome, ), {'__init__': __init__})
return Chrome
- Jetzt muss man nur noch den entsprechenden Decorator auf eine Klasse anwenden und den Pfad zum benutzerdefinierten Binary übergeben. Zum Beispiel:
@CustomFirefox('<path_to_firefox_binary>')
class Driver: pass
print(Driver)
Die Ausgabe würde so aussehen:
<class '__main__.Firefox'>
Um den Browser zu öffnen, rufen Sie einfach das neu erstellte WebDriver-Objekt auf:
Driver()
- Dies funktioniert gut, außer dass die ursprünglichen Metadaten der WebDriver-Klasse verloren gehen. Um die Metadaten der Elternklasse
webdriver.Firefoxoderwebdriver.Chromebeizubehalten, müssen wir diese Daten zunächst speichern, bevor wir die__init__-Methode ändern, und sie danach wiederherstellen. Zum Beispiel für den Firefox-Decorator:
meta_data = dict(webdriver.Firefox.__dict__)
meta_data['__init__'] = __init__
Firefox = type('Firefox', (webdriver.Firefox, ), meta_data)
Wenn wir jetzt die zurückgegebene Klasse überprüfen, erhalten wir:
<class 'selenium.webdriver.firefox.webdriver.WebDriver'>
``¨
- Eine kleine Anpassung könnte hilfreich sein, um den Standardfall einzubeziehen, bei dem kein Pfad als Parameter übergeben wird, sodass der Decorator den Standard-WebDriver mit der im System installierten Standard-Browserversion generiert.
- Jetzt ist es möglich, eine Treiberfabrik zu erstellen, die benutzerdefinierte Webdrivers aus einer Liste von Binary-Pfaden generiert, wobei die Parameter für diese Fabrik der Decorator und die Liste der Binary-Pfade sind:
```python
def driver_factory(decorator, bin_paths):
drivers = []
for bin_path in bin_paths:
@decorator(bin_path)
class CustomDriver: pass
drivers.append(CustomDriver)
return drivers
-
Voilà! Der WebDriver ist bereit, in Tests verwendet zu werden!
-
Das vollständige Skript würde so aussehen:
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.firefox.firefox_binary import FirefoxBinary
class CustomFirefox:
'''
Decorator to generate Firefox webdriver
the parameter passed to this decorator is the path to
the executable firefox browser
'''
def __init__(self, bin_path):
self.bin_path = bin_path
def __call__(self, cls):
if self.bin_path:
binary = FirefoxBinary(self.bin_path)
def __init__(self, firefox_binary=binary):
webdriver.Firefox.__init__(self, firefox_binary=firefox_binary)
meta_data = dict(webdriver.Firefox.__dict__)
meta_data['__init__'] = __init__
Firefox = type('Firefox', (webdriver.Firefox, ), meta_data)
return Firefox
else:
return webdriver.Firefox
class CustomChrome:
'''
Decorator to generate Firefox webdriver
the parameter passed to this decorator is the path to
the executable chrome browser
'''
def __init__(self, bin_path):
self.bin_path = bin_path
def __call__(self, cls):
if self.bin_path:
chrome_options = Options()
chrome_options.binary_location = self.bin_path
def __init__(self, chrome_options=chrome_options):
webdriver.Chrome.__init__(self, chrome_options=chrome_options)
meta_data = dict(webdriver.Chrome.__dict__)
meta_data['__init__'] = __init__
Chrome = type('Chrome', (webdriver.Chrome, ), meta_data)
return Chrome
else:
return webdriver.Chrome
def driver_factory(decorator, bin_paths):
drivers = []
for bin_path in bin_paths:
@decorator(bin_path)
class CustomDriver: pass
drivers.append(CustomDriver)
return drivers
Es könnte möglich sein, dieselbe Funktionalität mit einer Metaklasse zu实现. Das werde ich später untersuchen. Wenn Sie Kommentare oder Vorschläge haben, teilen Sie sie mir bitte mit.