Что здесь подразумеваю под true unit testing? То же что и всегда:
- В любом окружении результаты теста одинаковы. И в окружении любого из разработчиков, и на билдовом сервере.
- Тесты, исполняемые конкурентно, не мешают друг другу.
- Результат теста не зависит от параллельно исполняемых тестов.
- Повторное исполнение теста возвращает такой же результат.
Любой [Test], не попадающий в это хорошо известное определение, true unit тестом не является. Я с этом полностью согласен. Какие следствия напрашиваются применительно к тестированию доступа к данным и бизнес-логики с зависимостью от MS SQL Сервера:
- Каждый экземпляр теста исполняется в своей песочнице. В нашем случае у каждого экземпляра теста своя уникальная БД.
- Вне зависимости от успешности прохождения тестов ресурсы должны быть освобождены, а изменения должны быть откатаны обратно. В нашем случае каждый экземпляр теста по окончании работы уничтожает свою рабочую БД.
В итоге имеет две проблемы:
- Выбрать подходящий экземпляр SQL Server. И у разработчиков, и на билдовом сервере имеет место быть зоопарк версий и имен экземпляров SQL серверов. Теоретически можно принудить каждого внутри команды «поднять» экземпляр SQL-сервера с одинаковым именем для работы unit-тестов. Но за пределами команды, в тех же открытых проектах, принуждение маловероятное.
- Уничтожить рабочую БД. Drop database было бы элементарным решением проблемы освобождения ресурсов, если бы не дефолтовый пулинг в SQL-сервере. Пулинг оставляет открытые соединения с БД – из-за этого Drop database без дополнительных приседаний падает.
Другие проблемы, мешающие воплотить в unit-тестах слоя доступа к данным true-принципы, мне неизвестны.
Voilà
Хочу поделится своей несложным окружением для unit-тестирования доступа к данным и любого кода работы с БД SQL сервер:
- AnySqlServer – подходящий экземпляр SQL Server
- DB – уникальное имя БД-песочницы
- MasterConnectionString – строка подключения к экземпляру SQL-сервера
- DbConnectionString – строка подключения к БД-песочнице
- SetUp() – создать БД-песочницу
- TearDown() – учичтожить БД-песочницу, если остаются незакрытые соедния к БД-песочнице, например соединения в пуле, то подключения обрываются.
1: static class TestEnvironment2: {3: static TestEnvironment()
4: {5: var servers = SqlServiceInfo.Get(TimeSpan.FromSeconds(10));6: foreach (var sqlInstance in servers.Instances)7: if (sqlInstance.Status == ServiceControllerStatus.Running)
8: if (sqlInstance.Description != null)9: if (!sqlInstance.IsExpress)
10: if (SqlServerUtils.IsAdmin(sqlInstance.FullLocalName))
11: AnySqlServer = sqlInstance.FullLocalName;12: }13:14: public static readonly string AnySqlServer;15:16: public static readonly string DB =17: "UNITEST_" + Guid.NewGuid().ToString("N");18:19: public static string TracePath =20: Environment.SystemDirectory.Substring(0,2)21: + @"\\temp\\traces";
22:23: public static readonly string WorkingAppicationName =24: "SqlTrace unit-test";
25:26: public static string MasterConnectionString27: {28: get
29: {30: return
31: "Application Name=SQL Unit Testing framework;"
32: + "Integrated Security=SSPI;"
33: + "Data Source=" + AnySqlServer + ";"34: + "Pooling=false;";
35: }36: }37:38: public static string DbConnectionString39: {40: get
41: {42: return
43: "Application Name=" + WorkingAppicationName + ";" +44: "Integrated Security=SSPI;"
45: + "Data Source=" + AnySqlServer + ";"46: + "Pooling=true;"
47: + "Initial Catalog=" + DB + ";"48: + "Max Pool Size=300";
49: }50: }51:52: public static void SetUp()53: {54: Trace.WriteLine(55: "Working SQL Server instance is " + AnySqlServer);
56:57: using (var con = new SqlConnection(MasterConnectionString))58: {59: con.Open();60:61: var sql = "Create Database [" + DB + "]";62: using (var cmd = new SqlCommand(sql, con))63: {64: cmd.ExecuteNonQuery();65: }66: }67: }68:69: public static void TearDown()70: {71: try
72: {73: List<SqlServerUtils.ConnectionInfo> connections;74: using (var con = new SqlConnection(MasterConnectionString))75: {76: connections =77: SqlServerUtils.GetConnections(con)78: .FindAll(info => info.Database == DB && info.Spid > 50);79: }80:81: SqlServerUtils.KillConnections(MasterConnectionString, connections);82:83: using (var con = new SqlConnection(MasterConnectionString))84: using (var cmd = new SqlCommand("Drop Database " + DB, con))85: {86: con.Open();87: cmd.ExecuteNonQuery();88: }89: }90: catch(Exception ex)
91: {92: Trace.WriteLine(93: "Failed to teardown unit test" + Environment.NewLine + ex);
94: }95: }96: }
Подброр подходящего сервиса
Хочу отдельно, построчно, остановится на отборе подходящего экземпляра SQL-сервера:
- строка 7: Экземпляр windows-сервиса сервера должен быть в состоянии запущен :)
- строка 8: windows-аккаунт, под которым исполняется код (тест-кейс), должен иметь минимальные права на подключение к sql-серверу. Здесь Description - это значение функции @@version
- строка 9: MSDE 2000, SQL Express 2005/2008 не подходят - такой вот тест
- строка 10: windows-аккаунт, под которым исполняется код (тест-кейс), должен входить в группу sysadmin сервера
В вледующей серии - профилирование т.н. слоя доступа к данным в хранилище MS SQL Server.
Как работает SqlServiceInfo.Get(…)
- В реестре, в строгом соответствии с официальной документацией Books Online выискиваются все описания сервисов всех экземпляров SQL Server: File Locations for Default and Named Instances of SQL Server
- Далее тестируется каждый кандидат в своем рабочем потоке средствами пула потоков. Каждый кандидат проверяется на то что это зарегистрированный Win32-сервис Database Engine
- У каждого экземпляра Database Engine определяется версия исполнительного файла и статус win32-сервиса
- Каждый экземпляр тестируется на доступ с минимальными правами при подключении с помощью ADO.NET.