W drugiej części wpisu chciałbym dodać trochę szczegółów technicznych, które pominąłem w pierwszej części (patrz Potyczki z Selenium część 1) dla przejrzystości. Więc zaprezentuję jak wyglądają niektóre pliki konfiguracyjne i skrypty, których używamy do resetowania testowych node’ów. Forma jest bardzo skondensowana ale mam nadzieję, że w połączeniu z pierwszą częścią nie powinno być problemów ze zrozumieniem.
Jeszcze tytułem wyjaśnienia – testy automatyczne wykonują się podczas uruchomiania procesu build-a na TFS, a resetowanie node’ów następuje przed wykonaniem testów jak i po ich zakończeniu. To dlatego, że lokalnie podczas uruchamiani testów również czasami używamy node’ów. Po wykonaniu paczki testów automatycznych wykonujemy również podczas nightly build-a przywrócenie bazy danych do pierwotnego stanu, aby system na środowiskach testowych był wyczyszczony z danych testowych produkowanych przez testy automatyczne.
Konfiguracja NUnit
W pliku AssemblyInfo.cs:
//NUnit assembly settings [assembly: Parallelizable(ParallelScope.Fixtures)] [assembly: LevelOfParallelism(8)]
Musieliśmy dodać LevelOfParallelism na 8, bo na Hub-ie mamy tylko 2 procesory (VM) i mimo, że testy fizycznie wykonywane są na Node-ach nie chciało się wykonać więcej niż 2 na raz. A wynika to z wartości domyślnej tego przełącznika – 2 lub liczba procesorów (co jest większe), więc u nas zawsze było 2.
Selenium Hub
W konfiguracji hub-a nie ma nic specjalnego poza standardami. Do zainstalowania jako serwis użyty NSSM. Instalacja:
nssm install SeleniumHub java -jar c:\SeleniumHub\selenium-server-standalone-3.4.0.jar -role hub -hubConfig c:\SeleniumHub\hub_config.json net start SeleniumHub
Deinstalacja:
nssm remove SeleniumHub confirm
Plik konfiguracji Hub-a:
{"_comment" : "Configuration for Hub - hubConfig.json", "host": ip, "maxSessions": 10, "port": 4444, "cleanupCycle": 5000, "timeout": 300000, "newSessionWaitTimeout": -1, "servlets": [], "prioritizer": null, "capabilityMatcher": "org.openqa.grid.internal.utils.DefaultCapabilityMatcher", "throwOnCapabilityNotPresent": true, "nodePolling": 180000, "platform": "WINDOWS" }
Konfiguracja Node-a
Tu niestety nie obyło się bez trików. Z jakiegoś powodu nie działało nam kompletnie podawanie ścieżki do driver-a w pliku config, więc musieliśmy umieścić podmiankę drivera na headless w pliku restartującym Node-a. Jak widać Node obsługuje maksimum 3 instancje Firefox-a, Chrome-a i IE, ale nie więcej niż 3 sesje jednocześnie. Czyli mogą być 2 Chrom-y i IE lub 3 IE itp. Wynik eksperymentów z wydajnością hardware’u – tu akurat maszyna wirtualna z 2 procesorami.
Plik konfiguracji Node-a:
{ "capabilities": [ { "browserName": "firefox", "acceptSslCerts": true, "javascriptEnabled": true, "takesScreenshot": false, "firefox_profile": "", "browser-version": "27", "platform": "WINDOWS", "maxInstances": 3, "firefox_binary": "", "cleanSession": true, "marionette": true, "webdriver.gecko.driver": "geckodriver.exe" }, { "browserName": "chrome", "platform": "WINDOWS", "maxInstances": 3, "javascriptEnabled": true, "webdriver.chrome.driver": "chromedriver.exe" }, { "browserName": "internet explorer", "platform": "WINDOWS", "maxInstances": 3, "ignoreZoomSetting": true, "webdriver.ie.driver": "headless_ie_selenium.exe" } ], "_comment" : "Configuration for Node", "cleanUpCycle": 2000, "timeout": 30000, "proxy": "org.openqa.grid.selenium.proxy.DefaultRemoteProxy", "port": 5555, "register": true, "hub": "http://xxx.xxx.xxx.xxx:4444/grid/register", "hubPort": 4444, "hubHost": xxx.xxx.xxx.xxx, "maxSession": 3 }
Konfiguracja IE
No tutaj jest masa zabawy, żeby przekonać IE do działania równolegle z innymi przeglądarkami.
Konfiguracja IE jest opisana tutaj: Konfiguracja IE.
Wpis w rejestrze:
HKLM_CURRENT_USER\\Software\\Microsoft\\Internet Explorer\\Main -> Klucz DWORD TabProcGrowth z wartością 0
64-bitowy IEDriverServer – niezbędny
Headless driver jest niezbędny wraz z dodaniem zmiennej HEADLESS_UNIQUE do zmiennych środowiskowych.
Nie zaszkodzi dodać ścieżki do driver’ów do zmiennej PATH (miewaliśmy z tym problemy)
Wreszcie przekazanie capabilities do driver’a:
DesiredCapabilities capabilities = DesiredCapabilities.InternetExplorer(); capabilities.SetCapability("ie.forceCreateProcessApi", true); capabilities.SetCapability("ie.browserCommandLineSwitches", "-private"); Instance = new RemoteWebDriver(new Uri(Configuration.Instance.RemoteMachineUrl), capabilities, TimeSpan.FromMinutes(20));
Skrypt restartujący Node-a
Ponieważ wskazanie headless-a w pliku config nie działa, Node trzeba uruchamiać komendą:
java -Dwebdriver.ie.driver=headless_ie_selenium.exe -jar c:\SeleniumHub\selenium-server-standalone-3.4.0.jar -role node -nodeConfig c:\SeleniumHub\node_config.json
Tu znowu nie jest całkiem bezboleśnie. Skrypt PowerShell do restartu Node musi niestety wykonać sporą sztuczkę. Ponieważ normalnie wykonuje się w sesji 0 (uruchamiany zdalnie z build serwera), to uruchomiony proces również działałby w sesji 0. Czyli problemy. Więc skorzystaliśmy z możliwości Task Schedulera, żeby uruchomić skrypt z poziomu interaktywnie zalogowanego użytkownika. Reszta to już proste zakończenie procesów driver’ów, przeglądarek i samego node’a.
A cały skrypt do restartu Node-a wygląda następująco (uwzględnia użycie Headless-a):
function Start-Selenium-Node { Write-Output "Starting server ..." $UserID = "<YOUR USER ID - DOMAIN\USERNAME>" $Executable = "c:\Windows\System32\cmd.exe" $Argument = '/c "java -Dwebdriver.ie.driver=headless_ie_selenium.exe -jar c:\SeleniumHub\selenium-server-standalone-3.4.0.jar -role node -nodeConfig c:\SeleniumHub\node_config.json"' $WorkingDirectory = "c:\SeleniumHub\" $session = New-PSSession Invoke-Command -Session $Session -ArgumentList $Executable,$Argument,$WorkingDirectory,$UserID -ScriptBlock { param($Executable, $Argument, $WorkingDirectory, $UserID) $action = New-ScheduledTaskAction -Execute $Executable -Argument $Argument -WorkingDirectory $WorkingDirectory $principal = New-ScheduledTaskPrincipal -userid $UserID -LogonType Interactive $task = New-ScheduledTask -Action $action -Principal $principal $taskname = "_StartProcessActiveTask" try { $registeredTask = Get-ScheduledTask $taskname -ErrorAction SilentlyContinue } catch { $registeredTask = $null } if ($registeredTask) { Unregister-ScheduledTask -InputObject $registeredTask -Confirm:$false } $registeredTask = Register-ScheduledTask $taskname -InputObject $task -ErrorAction Continue Start-ScheduledTask -InputObject $registeredTask -ErrorAction Continue Unregister-ScheduledTask -InputObject $registeredTask -Confirm:$false -ErrorAction Continue } Remove-PSSession $session -ErrorAction Continue Write-Output "Server started" } function KillServer() { $procs = Get-WmiObject win32_process -filter "Name='java.exe' AND CommandLine LIKE '%selenium-server-standalone-3.4.0.jar%'" foreach($proc in $procs) { try { Write-Output "Terminating server" $proc.Terminate() } catch { Write-Output "Error terminating server" exit } } } function KillProcesses() { $browserList = 'chrome', 'MicrosoftEdge', 'iexplore', 'firefox' $driverList = 'chromedriver', 'geckodriver', 'MicrosoftWebDriver', 'IEDriverServer', 'headless_ie_selenium' try { Get-Process -Name $driverList -ErrorAction SilentlyContinue | Stop-Process -Force Write-Output "Drivers stopped" } Catch { Write-Output "Error stopping drivers" exit } try { Get-Process -Name $browserList -ErrorAction SilentlyContinue | Stop-Process -Force Write-Output "Browsers stopped" } Catch { Write-Output "Error stopping browser" exit } } function DeleteLocalTempFiles() { try { Get-ChildItem -Path $env:LOCALAPPDATA\Temp -Recurse | Remove-Item -Recurse -Force -ErrorAction SilentlyContinue } catch { Write-Output "Error while removing local temp files." } } DeleteLocalTempFiles KillServer KillProcesses Start-Selenium-Node
Podsumowanie
Droga była dość ciężka, ale efekt wart wykonanej pracy. Jesteśmy przygotowani do skalowania testów na większą liczbę instancji przeglądarek jeśli zajdzie taka potrzeba. Poza IE, który sprawił całą masę problemów z pozostałymi przeglądarkami Parallel Execution nie był dużym problemem.