Всем привет!
Хочу вам рассказать, как мне удалось решить проблему запуска автотестов в следующем сценарии:
- у меня есть набор тестовых AD-учетных записей;
- этим учетным записям сопоставлены пользователи в тестируемой системе 1-к-1;
- при входе в систему проверяется токен, который браузер получает от винды, и по которому система аутентифицирует пользователя;
- в соответствии с учетной записью AD мы авторизуемся в системе с нужными правами.
В чем проблема?
Запуск браузера руками от имени другого пользователя осуществляется легко:
с зажатой клавишей Shift правой кнопкой мыши жмем по ярлыку, например, хрома, там запуск от имени другого пользователя, домен-логин-пароль, всё, браузер запущен.
В автотестах все не так просто. Вспомним схему запуска браузера и работы с ним:
Код ↔ Драйвер браузера ↔ Браузер
_driver = new ChromeDriver(path_to_chromedriver, options);
В таком сценарии наша задача просто определить опции запуска браузера, проинициализировать драйвер, передав в конструктор опции, а драйвер себе уже запустит браузер, который найдет в %PATH% или передан в опциях.
Подобный вопрос уже подымался на этом форуме. Моё решение основано вот на этом совете stack overflow.
Итак, что и как происходит:
using (new ActiveDirectory.User(CurrentBankUser, domainName, password))
{
Browser = new RemoteWebDriver(new Uri($"http://localhost:{BrowserPort}", UriKind.Absolute), options);
}
Браузер стартуем как удаленный вебдрайвер на локалхосте, к коротому цепляемся по определенному порту, лежащему в переменной BrowserPort
. Директивой Using мы указываем, в каком потоке должен стартовать драйвер, к которому мы цепляемся:
public class User : IDisposable
{
public User(string userName, string domainName, string password)
{
BrowserPort = (9000 + AppDomain.GetCurrentThreadId()).ToString();
ProcessStartInfo processStartInfo = new ProcessStartInfo(ChromeDriverPath, $"--port={BrowserPort}");
processStartInfo.UserName = userName;
System.Security.SecureString securePassword = new System.Security.SecureString();
foreach (char c in password)
{
securePassword.AppendChar(c);
}
processStartInfo.Password = securePassword;
processStartInfo.Domain = domainName;
processStartInfo.UseShellExecute = false;
processStartInfo.LoadUserProfile = true;
processStartInfo.WorkingDirectory = ProjectEnvironment.ChromeDriverDir;
processStartInfo.WindowStyle = ProcessWindowStyle.Hidden;
Thread startThread = new Thread(() =>
{
BaseSteps.driverProcess = Process.Start(processStartInfo);
BaseSteps.driverProcess.WaitForExit();
})
{ IsBackground = true };
startThread.Start();
}
public void Dispose()
{}
}
Вот здесь BrowserPort = (9000 + AppDomain.GetCurrentThreadId()).ToString();
как раз таки объявляется тот порт, на котором стартанёт драйвер, и по которому мы будем к нему цепляться.
Здесь ProcessStartInfo processStartInfo = new ProcessStartInfo(ChromeDriverPath, $"--port={BrowserPort}");
мы передаем процесс, который будем запускать (хромдрайвер), и указываем опциональный флаг --port
, если его не указать, то больше одного теста в параллели вы запустить не сможете, потому что хромдрайвер по дефолту стартует на порту 9515:
Дальше по коду мы передаём данные доменной учетки (домен/логин и пароль), указываем всякие данные и стартуем поток, в котором будет запущен драйвер от имени указанной AD-учетной записи.
Почему метод Dispose()
пустой? Потому что этот метод будет вызван автоматически, как только мы выйдем отсюда:
using (new ActiveDirectory.User(CurrentBankUser, domainName, password))
{
Browser = new RemoteWebDriver(new Uri($"http://localhost:{BrowserPort}", UriKind.Absolute), options);
// Тут должен быть выполнен код тестов
}
// Вот здесь будет выполнен Dispose()
Так как у меня Specflow, а драйвер я храню в ThreadStatic переменной, то я просто запускаю драйвер с браузером, в методе Dispose()
не происходит ничего, что мне и нужно. Главное по окончании теста не забыть правильно очистить ресурсы.
Для этого у меня есть глобальный метод Dispose()
public void Dispose()
{
Browser.Quit();
driverProcess = GetChromeDriverProcess();
driverProcess.Kill();
}
Здесь мы выключаем браузер, ищем нужный нам процесс хромдрайвера и убиваем его.
Поиск процесса осуществляется так:
public static Process GetChromeDriverProcess()
{
var processes = Process.GetProcesses().ToList();
var ADprocesses = new List<Process>();
ADprocesses.AddRange(processes.FindAll(process => process.ProcessName.Equals("chromedriver")));
var cmdlines = new List<string>();
foreach (var process in ADprocesses)
{
cmdlines.Add(GetCommandLine(process));
}
return ADprocesses[cmdlines.FindIndex(cmd => cmd.Contains("--port=" + BrowserPort))];
}
private static string GetCommandLine(Process process)
{
using (ManagementObjectSearcher searcher = new ManagementObjectSearcher("SELECT CommandLine FROM Win32_Process WHERE ProcessId = " + process.Id))
using (ManagementObjectCollection objects = searcher.Get())
{
return objects.Cast<ManagementBaseObject>().SingleOrDefault()?["CommandLine"]?.ToString();
}
}
Здесь мы просто ищем все процессы, среди них хромдрайвер, потом получаем CommandLine
свойства процессов хромдрайверов, и ищем тот, что содержит изначально заданный нами порт. Чтобы ManagementObjectSearcher
не ругался, надо добавить в зависимости System.Management.Instrumentation
Выводы:
- Это работает, параллельное выполнение работает тоже;
- Если вам кажется, что это какие-то костыли - вам не кажется
Ну и лично у меня немного шок вызывает осознание того, что браузер в таком контексте становится тестовыми данными, потому что надо менеджить, под кем запущен браузер, и устраивает ли это нас. Мне пришлось написать парсер feature-файлов, который ищет нужный нам feature-файл в каталоге тестов, потом в нем ищет выполняемый тест и дальше по регулярке вытаскивает логин AD, лезет в Vault за доменом и паролем, и только затем запускает браузер.
Надеюсь, вам не придётся столкнуться с подобным, а если и придётся, то эта статья поможет решить проблему.
Спасибо за внимание!