Основная идея юнит (или модульного, как его еще называют) тестирования – тестирование отдельных компонентов программы, т.е. классов и их методов. Разрабатывать код, покрытый тестами, весьма полезно, потому что при их правильном использовании практически исключается возможность регресии в истории развитии программы – «что-то новое добавили, половина старого слегла». Также сейчас весьма модна методология разработки “TDD” — Test Driven Development. Согласно ей, программист вначале разрабатывает набор тестов для будущей функциональности, просчитывает все варианты выполнения, и лишь потом начинает писать непосредственно рабочий код, подходящий под уже написанные тесты.
Так как существование тестов в программе является не только подтверждением квалификации разработчика, но и зачастую требованием заказчика, я решил заняться этим вопросом и «пощупать» тесты вблизи.
Работаю я в основном в Visual Studio, пишу на шарпе, а значит выбор был почти ограничен двумя продуктами – Nunit и Unit Testing Framework.
Unit Testing Framework — это встроенная в Visual Studio система тестирования, разрабатываемая Майкрософт, постоянно развивающаяся( в числе последних обновлений – возможность тестирования UI, о чем уже писали на хабре), и что немаловажно, она почти наверняка будет существовать все время, пока есть Visual Studio, чего не скажешь о стронних разработках. Отличная интеграция в IDE и функция подсчета процента покрытия кода в программе окончательно склонили чашу весов – выбор был сделан.
В сети присутствует немаленькое количество разнообразных туториалов по тестированию, но все они обычно сводятся к тестированию самописного калькулятора или сравнению строк. Это вещи, конечно, тоже необходимые и важные, но на серьезные примеры они тянут плохо, если сказать откровенно – совсем не тянут. Такие задачи я и сам могу протетстировать даже в уме.
Вот список более серьезных задач
• проверка корректности создания БД
• проверка корректности работы бизнес-логики
• получение пользы от всего этого(в моем случае польза была получена))
Итак, приступим!
Согласно принципам тестирования и здравой логике, у нас будет отдельная база под тестирование. Потому создаем базу TestDB, добавляем в нее таблицу Users
и вносим в нее данные
Добавим еще хранимую процедуру, которую и будем тестировать. Она будет заниматься довольно привычным делом – по заданному логину и хешу пароля возвращать идентификатор пользователя.
Unit Testing Framework позволяет тестировать разнообразные аспекты работы БД – проверка схемы, количества записей в таблицах, хранимых процедур, времени выполнения запросов, их результатов и многое другое.
Создаем новое решение типа TestProject. Назовем его LinkCatalog.Tests. И добавляем в него новый тест Database Unit Test
Появляется окно настройки соединения с БД. Настраиваем соединение с нашей БД и жмем ОК. В этом же окне можно указать параметры автогенерации данных для тестов. Эта функция использует Data Generation Plan и позволяет заполнить таблицу базы тестовыми значениями, используя шаблоны и даже регулярные выражения.
Нажимаем ОК и попадаем на окно тестирования БД.
Первый тест – самый простой. Проверим количество записей в таблице:
Теперь устанавливаем условие корректности теста. Как я говорил, существует большой список всевозможных критериев, но нас интересует один из самых простых – ScalarValue.
Все опции условия настраивается в окошке Properties.
Все! На этом первый тест закончен. Запускаем и смотрим
Что и требовалось доказать – строки успешно хранятся в базе.
Теперь настало время заняться уже более реальной проверкой, чем количества записей. Речь идет о хранимой процедуре. А вдруг разработчики базы допустили в ней критическую ошибку? А ведь это важнейшая часть работы подсистемы аутентификации пользователей!
Создаем новый тест БД, нажав на зеленый плюсик
Вот код теста:
Здесь уже используется другое условие – ожидается, что в выборке будет ровно 1 строка.
Этот тест также завершается успешно, что не может не радовать.
По результатам тестирования базы данных можно сказать, что она работает стабильно и ожидаемо. Сейчас, покрытие процедур БД достигает 100% — предела, которого непросто добиться в более сложном приложении или базе с бОльшим количеством таблиц/процедур/связей.
Теперь приступим к тестированию кода.
У меня было небольшое ASP.NET приложение с реализацией шаблона MVC. Модель представляла собой отдельную сборку под гордым названием DAL и включала в себя враппер доступа к БД. Одним из требований было использование именно DataReader в ADO.NET. Настройки, как это принято, хранятся в web.config.
Я решил начать тестирование именно модели: вот такой код предстояло оттестировать
Добавляем в проект новый юнит-тест
и получаем файл такого содержания:
Атрибут [TestClass] означает, что этот класс содержит тестовые методы, а [TestMethod] – что такой метод представляет собой конкретный метод.
Добавляем в проект ссылку на сборку DAL и импортируем пространство имен LinkCatalog.DAL. Подготовительные работы закончены, настало время писать тесты.
У нас всегда есть администратор с таким логином и потому его идентификатор не может быть -1.
Запускаем тест – и ошибка:
Выскочило исключение:
Как видно, ошибка заключается в конструкторе класса DB – он не может найти файл конфигурации и, вследствие этого, тест завершается не только неудачно, но и с ошибкой.
Решение проблемы конфигурационных файлов довольно простое:
Нет, среда тестирования не подключит web.config автоматически. Вместо этого каждый тест-проект создает свой файл конфигурации app.config, и все, что требуется – дописать в него необходимые настройки.
Теперь тест проходит успешно:
Немного изменим тест
В базе гарантированно нет такого логина(логин должен быть без пробелов, за этим следят валидаторы в контроллере при регистрации), и это вторая и последняя проверка метода.
Тест опять не проходится – смотрим детализацию:
Теперь все ОК!
Вот так вот тестирование помогает выявить некоторые ошибки в программе, пускай они возникают от забывчивости/лени/не знания, но так их исправить легче, чем на живом сервере. Этот топик, естественно, не покрывает всех аспектов тестирования программ и лишь приоткрыл эту область. За кадром осталась возможность условных тестов, зависящих от других тестов, тестов пользовательских интерфейсов и другие вещи.
Тестируйте свои программы и пускай багов у Вас будет мало, а фич много-много!
Так как существование тестов в программе является не только подтверждением квалификации разработчика, но и зачастую требованием заказчика, я решил заняться этим вопросом и «пощупать» тесты вблизи.
Работаю я в основном в Visual Studio, пишу на шарпе, а значит выбор был почти ограничен двумя продуктами – Nunit и Unit Testing Framework.
Unit Testing Framework — это встроенная в Visual Studio система тестирования, разрабатываемая Майкрософт, постоянно развивающаяся( в числе последних обновлений – возможность тестирования UI, о чем уже писали на хабре), и что немаловажно, она почти наверняка будет существовать все время, пока есть Visual Studio, чего не скажешь о стронних разработках. Отличная интеграция в IDE и функция подсчета процента покрытия кода в программе окончательно склонили чашу весов – выбор был сделан.
В сети присутствует немаленькое количество разнообразных туториалов по тестированию, но все они обычно сводятся к тестированию самописного калькулятора или сравнению строк. Это вещи, конечно, тоже необходимые и важные, но на серьезные примеры они тянут плохо, если сказать откровенно – совсем не тянут. Такие задачи я и сам могу протетстировать даже в уме.
Вот список более серьезных задач
• проверка корректности создания БД
• проверка корректности работы бизнес-логики
• получение пользы от всего этого(в моем случае польза была получена))
Итак, приступим!
Тестирование БД
Согласно принципам тестирования и здравой логике, у нас будет отдельная база под тестирование. Потому создаем базу TestDB, добавляем в нее таблицу Users
CREATE TABLE [dbo].[Users](
[id] [int] IDENTITY(1,1) NOT NULL,
[login] [nchar](100) COLLATE Ukrainian_CI_AS NOT NULL,
[hash] [nchar](50) COLLATE Ukrainian_CI_AS NOT NULL,
[level] [int] NULL,
CONSTRAINT [PK_Users] PRIMARY KEY CLUSTERED
([id] ASC)
WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON))
CREATE UNIQUE NONCLUSTERED INDEX [IX_UserName] ON [dbo].[Users]
([Login] ASC)
WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, IGNORE_DUP_KEY = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON)
и вносим в нее данные
INSERT [dbo].[Users] ([Login], [Hash], [Level]) VALUES (N'Sirix', N'827ccb0eea8a706c4c34a16891f84e7b', 1)
INSERT [dbo].[Users] ([Login], [Hash], [Level]) VALUES (N'Tester', N'3095C3E4F1465133E5E6BE134EB2EBE2', 1)
INSERT [dbo].[Users] ([Login], [Hash], [Level]) VALUES (N'Admin', N'E3AFED0047B08059D0FADA10F400C1E5', 2)
INSERT [dbo].[Users] ([Login], [Hash], [Level]) VALUES (N'U1', N'827CCB0EEA8A706C4C34A16891F84E7B', 1)
Добавим еще хранимую процедуру, которую и будем тестировать. Она будет заниматься довольно привычным делом – по заданному логину и хешу пароля возвращать идентификатор пользователя.
CREATE PROCEDURE [dbo].[GetUserId]
(
@login nchar(100),
@hash nchar(50),
@id int OUTPUT
)
AS
SELECT @id = ID FROM Users WHERE Login = @login AND hash = @hash
RETURN
* This source code was highlighted with Source Code Highlighter.
Unit Testing Framework позволяет тестировать разнообразные аспекты работы БД – проверка схемы, количества записей в таблицах, хранимых процедур, времени выполнения запросов, их результатов и многое другое.
Начинаем тесты
Создаем новое решение типа TestProject. Назовем его LinkCatalog.Tests. И добавляем в него новый тест Database Unit Test
Появляется окно настройки соединения с БД. Настраиваем соединение с нашей БД и жмем ОК. В этом же окне можно указать параметры автогенерации данных для тестов. Эта функция использует Data Generation Plan и позволяет заполнить таблицу базы тестовыми значениями, используя шаблоны и даже регулярные выражения.
Нажимаем ОК и попадаем на окно тестирования БД.
Тест №1
Первый тест – самый простой. Проверим количество записей в таблице:
Select count(*) FROM Users
Теперь устанавливаем условие корректности теста. Как я говорил, существует большой список всевозможных критериев, но нас интересует один из самых простых – ScalarValue.
Все опции условия настраивается в окошке Properties.
Все! На этом первый тест закончен. Запускаем и смотрим
Что и требовалось доказать – строки успешно хранятся в базе.
Тест №2
Теперь настало время заняться уже более реальной проверкой, чем количества записей. Речь идет о хранимой процедуре. А вдруг разработчики базы допустили в ней критическую ошибку? А ведь это важнейшая часть работы подсистемы аутентификации пользователей!
Создаем новый тест БД, нажав на зеленый плюсик
Вот код теста:
/* Выходное значение */
DECLARE @id INT;
SET @id = -1
/* Должно установить id = 1 */
EXEC GetUserId N'Sirix', N'827ccb0eea8a706c4c34a16891f84e7b', @id OUTPUT;
SELECT * FROM Users WHERE id = @id
* This source code was highlighted with Source Code Highlighter.
Здесь уже используется другое условие – ожидается, что в выборке будет ровно 1 строка.
Этот тест также завершается успешно, что не может не радовать.
По результатам тестирования базы данных можно сказать, что она работает стабильно и ожидаемо. Сейчас, покрытие процедур БД достигает 100% — предела, которого непросто добиться в более сложном приложении или базе с бОльшим количеством таблиц/процедур/связей.
Юнит-тесты кода
Теперь приступим к тестированию кода.
У меня было небольшое ASP.NET приложение с реализацией шаблона MVC. Модель представляла собой отдельную сборку под гордым названием DAL и включала в себя враппер доступа к БД. Одним из требований было использование именно DataReader в ADO.NET. Настройки, как это принято, хранятся в web.config.
Я решил начать тестирование именно модели: вот такой код предстояло оттестировать
namespace LinkCatalog.DAL
{
public class UserModel
{
...........
public static int GetUserIdByName(string username)
{
string query = "SELECT ID FROM Users WHERE Login = @login;";
DB.get().CommandParameters.Add(new SqlParameter("@login", username));
int id = -1;
int.TryParse(DB.get().GetOneCell(query).ToString(), out id);
return id;
}
}
public class DB
{
...........
private static DB instance;
public static DB get()
{
if (instance == null)
instance = new DB();
return instance;
}
private SqlConnection connection;
private SqlDataReader reader;
public List CommandParameters;
private DB()
{
this.connection = new SqlConnection(WebConfigurationManager.ConnectionStrings["DBConnectionString"].ConnectionString);
this.CommandParameters = new List();
}
public object GetOneCell(string query)
{
SqlCommand sc = new SqlCommand(query, this.connection);
if (this.CommandParameters.Count != 0)
sc.Parameters.AddRange(this.CommandParameters.ToArray());
this.connection.Open();
object res = sc.ExecuteScalar();
this.CommandParameters.Clear();
this.Close();
return res;
}
}
}
* This source code was highlighted with Source Code Highlighter.
Добавляем в проект новый юнит-тест
и получаем файл такого содержания:
using System;
using System.Text;
using System.Collections.Generic;
using System.Linq;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace LinkCatalog.Tests
{
[TestClass]
public class UserModel_Tests
{
[TestMethod]
public void TestMethod1()
{
}
}
}
* This source code was highlighted with Source Code Highlighter.
Атрибут [TestClass] означает, что этот класс содержит тестовые методы, а [TestMethod] – что такой метод представляет собой конкретный метод.
Добавляем в проект ссылку на сборку DAL и импортируем пространство имен LinkCatalog.DAL. Подготовительные работы закончены, настало время писать тесты.
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace LinkCatalog.Tests
{
using LinkCatalog.DAL;
[TestClass]
public class UserModel_Tests
{
[TestMethod]
public void GetUserById_Test()
{
Assert.AreNotEqual(UserModel.GetUserIdByName("Admin"), -1);
}
}
}
* This source code was highlighted with Source Code Highlighter.
У нас всегда есть администратор с таким логином и потому его идентификатор не может быть -1.
Запускаем тест – и ошибка:
Выскочило исключение:
Test method LinkCatalog.Tests.UserModel_Tests.GetUserById_Test threw exception:
System.NullReferenceException: Object reference not set to an instance of an object.
LinkCatalog.DAL.DB..ctor() in C:\Users\Ванек\Documents\Visual Studio 2010\Projects\Practice\DAL\Database.cs: line 35
LinkCatalog.DAL.DB.get() in C:\Users\Ванек\Documents\Visual Studio 2010\Projects\Practice\DAL\Database.cs: line 17
LinkCatalog.DAL.UserModel.GetUserIdByName(String username) in C:\Users\Ванек\Documents\Visual Studio 2010\Projects\Practice\DAL\Models\UserModel.cs: line 63
LinkCatalog.Tests.UserModel_Tests.GetUserById_Test() in C:\Users\Ванек\Documents\Visual Studio 2010\Projects\Practice\LinkCatalog.Tests\UserModel_Tests.cs: line 12
* This source code was highlighted with Source Code Highlighter.
Как видно, ошибка заключается в конструкторе класса DB – он не может найти файл конфигурации и, вследствие этого, тест завершается не только неудачно, но и с ошибкой.
Решение проблемы конфигурационных файлов довольно простое:
Нет, среда тестирования не подключит web.config автоматически. Вместо этого каждый тест-проект создает свой файл конфигурации app.config, и все, что требуется – дописать в него необходимые настройки.
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<configSections>
<section name="DatabaseUnitTesting" type="Microsoft.Data.Schema.UnitTesting.Configuration.DatabaseUnitTestingSection, Microsoft.Data.Schema.UnitTesting, Version=10.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" />
</configSections>
<DatabaseUnitTesting>
<DataGeneration ClearDatabase="true" />
<ExecutionContext Provider="System.Data.SqlClient" ConnectionString="Data Source=SIRIXPC\sqlexpress;Initial Catalog=TestDB;Integrated Security=True;Pooling=False"
CommandTimeout="30" />
<PrivilegedContext Provider="System.Data.SqlClient" ConnectionString="Data Source=SIRIXPC\sqlexpress;Initial Catalog=TestDB;Integrated Security=True;Pooling=False"
CommandTimeout="30" />
</DatabaseUnitTesting>
<connectionStrings>
<add name="DBConnectionString" connectionString="Data Source=SIRIXPC\SQLEXPRESS;Initial Catalog=tbd;Integrated Security=True"
providerName="System.Data.SqlClient" />
</connectionStrings>
</configuration>
* This source code was highlighted with Source Code Highlighter.
Теперь тест проходит успешно:
Немного изменим тест
[TestMethod]
public void GetUserById_Test()
{
Assert.AreNotEqual(UserModel.GetUserIdByName("Admin"), -1);
Assert.AreEqual(UserModel.GetUserIdByName("0-934723 ### 12sdf s"), -1);
}
* This source code was highlighted with Source Code Highlighter.
В базе гарантированно нет такого логина(логин должен быть без пробелов, за этим следят валидаторы в контроллере при регистрации), и это вторая и последняя проверка метода.
Тест опять не проходится – смотрим детализацию:
Test method LinkCatalog.Tests.UserModel_Tests.GetUserById_Test threw exception:
System.NullReferenceException: Object reference not set to an instance of an object.
На этот раз ошибка заключается в UserModel.GetUserIdByName, а именно вот здесь не хватает проверки на null:
int.TryParse(DB.get().GetOneCell(query).ToString(), out id);
Добавляем:
var res = DB.get().GetOneCell(query);
if (res != null)
int.TryParse(res.ToString(), out id);
* This source code was highlighted with Source Code Highlighter.
Теперь все ОК!
Вот так вот тестирование помогает выявить некоторые ошибки в программе, пускай они возникают от забывчивости/лени/не знания, но так их исправить легче, чем на живом сервере. Этот топик, естественно, не покрывает всех аспектов тестирования программ и лишь приоткрыл эту область. За кадром осталась возможность условных тестов, зависящих от других тестов, тестов пользовательских интерфейсов и другие вещи.
Тестируйте свои программы и пускай багов у Вас будет мало, а фич много-много!