Pull to refresh

Unit-тестирование средствами .NET

Reading time 8 min
Views 57K
Основная идея юнит (или модульного, как его еще называют) тестирования – тестирование отдельных компонентов программы, т.е. классов и их методов. Разрабатывать код, покрытый тестами, весьма полезно, потому что при их правильном использовании практически исключается возможность регресии в истории развитии программы – «что-то новое добавили, половина старого слегла». Также сейчас весьма модна методология разработки “TDD” — Test Driven Development. Согласно ей, программист вначале разрабатывает набор тестов для будущей функциональности, просчитывает все варианты выполнения, и лишь потом начинает писать непосредственно рабочий код, подходящий под уже написанные тесты.

Так как существование тестов в программе является не только подтверждением квалификации разработчика, но и зачастую требованием заказчика, я решил заняться этим вопросом и «пощупать» тесты вблизи.

Работаю я в основном в 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.


Теперь все ОК!

Вот так вот тестирование помогает выявить некоторые ошибки в программе, пускай они возникают от забывчивости/лени/не знания, но так их исправить легче, чем на живом сервере. Этот топик, естественно, не покрывает всех аспектов тестирования программ и лишь приоткрыл эту область. За кадром осталась возможность условных тестов, зависящих от других тестов, тестов пользовательских интерфейсов и другие вещи.

Тестируйте свои программы и пускай багов у Вас будет мало, а фич много-много!
Tags:
Hubs:
+7
Comments 23
Comments Comments 23

Articles