Подготовка
В солюшине уже есть проект под названием WebApplication.Tests, именно он будет содержать тесты страниц и контролов веб приложения. Пока что в он пустой, и перед тем как добавить первый тест, необходимы некоторые подготовительные действия. Во первых, необходимо включить в проект HttpSimulator (о нем я писал в прошлой статье), как крайне удобное средство тестирования веб страниц без веб сервера (Serverless testing). С HtppSimulator можно ознакомиться на странице автора, Фила Хаака (одного из разработчиков ASP.net MVC). Кстати линк, по которому были доступны исходники уже не валидный, поэтому HtppSimulator можно скачать здесь. Забегая вперед, отмечу, что в случае тестирования страницы регистрации, использование HtppSimulator не является необходим, однако лучше подготовить необходимый фреймворк сразу.
Интеграционный тест на то и интеграционный, что в нем задействованы все компоненты "в живую". Использование моков и стабов в интеграцинном тестировании противоречит его смыслу. Так как приложение обращается к базе данных, мы должны уметь правильно подготовить и удалить после себя те данные, которые нужны для теста. Поэтому мы должны имееть класс DbScript, который будет готовить и очищать тестовые данные.
Начиная работать с тестами в С++, мне очень нравился поход, который опирался на гарантированный вызов конструктора/деструктора объекта, поэтому можно было проектировать базовые классы, выполняющие инициализвацию и очистку ресурсов, а каждый тест инкапусулировать в класс, наследующий его. Таким образом, перед запуском мы выполняли подготовку, и чтобы не случилось во время выполнения теста, будет вызван деструктор, завершающий тестовую сессию. В .NET ситуация иная, просто потому что в нем нет явных деструкторов.. а тесты в NUnit это не классы, а методы класса TestFixture. Конечно, в NUnit есть методы SetUp/TearDown, но они обощенный на уровне класса, и не подходят в тех случаях, когда, напрмер каждому тесту необходима уникальная инициализация. Тем не менее, мы может симулировать подобное поведение. Для этого мы добавим новый класс FixtureInit, реализующий интрефейс IDisposable, который будет инициализирован в using блоке, внутри каждого теста.
Наконец сам код страницы необходимо немного изменить, дабы привести ее к виду пригодному для тестирования.
Http Simulator integration
Сам проект состоит из 4-х файлов, включая тесты, поэтому просто довим их в WebApplication.Test в фолдер HttpSimulator.
DbScript class
Пока что, это пустой класс с методами Init и Clean.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace WebApplication.Tests.Framework
{
public class DbScript : IDisposable
{
public DbScript()
{
Init();
}
private void Init()
{
}
#region IDisposable Members
public void Dispose()
{
Clean();
}
private void Clean()
{
}
#endregion
}
}
* This source code was highlighted with Source Code Highlighter.
FixtureInit class
Класс FixtureInit в конструкторе инициализиует HtppSimulator, а в методе Dispose закрывает его.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using R1UnitTest.HtttpSimulator;
namespace WebApplication.Tests.Framework
{
public class FixtutureInit : IDisposable
{
private HttpSimulator _simulator;
private DbScript _script;
public FixtutureInit(string uri)
{
_simulator = new HttpSimulator().SimulateRequest(new Uri(uri));
_script = new DbScript();
}
public HttpSimulator Simulator
{
get { return _simulator; }
}
#region IDisposable Members
public void Dispose()
{
_simulator.Dispose();
_script.Dispose();
}
#endregion
}
}
* This source code was highlighted with Source Code Highlighter.
RegistrationPageTests class
Добавим класс тестов страницы:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using NUnit.Framework;
using WebApplication.Tests.Framework;
namespace WebApplication.Tests.Tests
{
[TestFixture]
public class RegistrationPageTests
{
[Test]
public void Smoke()
{
using (var fixture = new FixtutureInit("http://localhost/registration.aspx"))
{
var page = new RegistrationView();
}
}
}
}
* This source code was highlighted with Source Code Highlighter.
Registration.aspx.cs changes
Добавим новый метод _Page_Load, и изменим существующий, для поддержки работы страницы в тестовом и продакшн моде.
protected void Page_Load(object sender, EventArgs e)
{
var controls = new Dictionary<string, object> {
{ "EmailText", EmailText },
{ "SecretPhraseText", SecretPhraseText },
{ "PasswordText", PasswordText }
};
var postback = Page.IsPostBack;
_Page_Load(HttpContext.Current, controls, sender, e, postback);
}
public void _Page_Load(HttpContext context, IDictionary<string, object> controls, object sender, EventArgs e, bool postback)
{
_registration = new Registration(this, new RegistrationData());
if (postback)
{
_registration.RegisterUser(EmailText.Text, SecretPhraseText.Text, PasswordText.Text);
}
}
* This source code was highlighted with Source Code Highlighter.
Реализация
Загрузка страницы
Все готово к запуску первого теста, тест PageLoad выполняет проверку, что загрузка страницы происходит без исключений.
[Test]
public void PageLoad()
{
using (var fixture = new FixtutureInit("http://localhost/registration.aspx"))
{
//INIT
var page = new RegistrationView();
var controls = CreateControls();
//ACT/POST
page._Page_Load(HttpContext.Current, controls, new object(), new EventArgs(), false);
}
}
* This source code was highlighted with Source Code Highlighter.
Метод CreateControls создает словарь используемых на странице контролов:
#region common test code
private IDictionary<string, object> CreateControls()
{
var controls = new Dictionary<string, object> {
{ "EmailText", new TextBox() },
{ "SecretPhraseText", new TextBox() },
{ "PasswordText", new TextBox() }
};
return controls;
}
#endregion
* This source code was highlighted with Source Code Highlighter.
Запустим тест и убедимся, что все ОК.
Проверка регистрации
Теперь сделаем более полезный тест, который сможет проверить функциональность регистрации нового пользователя. Код теста следующий.
[Test]
public void RegisterUser()
{
using (var fixture = new FixtutureInit("http://localhost/registration.aspx"))
{
//INIT
var page = new RegistrationView();
var controls = CreateControls();
(controls["EmailText"] as TextBox).Text = "abe@test.com";
(controls["SecretPhraseText"] as TextBox).Text = "bla-bla";
(controls["PasswordText"] as TextBox).Text = "test_pass1";
//ACT (post back)
page._Page_Load(HttpContext.Current, controls, new object(), new EventArgs(), true);
//POST
var expected = @"Congratulations! You've been successfully registered on Web Developement web site.<br /><br />Return to <a href=""Default.aspx"">Main page</a>.";
Assert.That((controls["SuccessMessage"] as HtmlGenericControl).InnerHtml, Is.EqualTo(expected));
Assert.That((controls["RegistrationMultiView"] as MultiView).GetActiveView(), Is.EqualTo((controls["SuccessForm"] as View)));
}
}
* This source code was highlighted with Source Code Highlighter.Мы выполняем проверку на то, что контрол содержит корректный HTML код и малтивью находится в правильном состоянии. Для того, чтобы этот тест мог быть собран и начал проходить, необходимо внести ряд изменений.
- Добавить все необходимые контролы, как в тесте так и на самой странице, в контейнер контролов
- Код страниц изменить таким образом, чтобы доступ был к контролам осуществляся только через этот контейнер
public partial class RegistrationView : System.Web.UI.Page, IRegistrationView
{
private Registration _registration;
private IDictionary<string, object> _controls;
protected void Page_Load(object sender, EventArgs e)
{
var controls = new Dictionary<string, object> {
{ "RegistrationMultiView", RegistrationMultiView },
{ "RegistrationForm", RegistrationForm },
{ "EmailText", EmailText },
{ "SecretPhraseText", SecretPhraseText },
{ "PasswordText", PasswordText },
{ "SuccessForm", SuccessForm },
{ "SuccessMessage", SuccessMessage },
{ "FailedForm", FailedForm },
{ "FailMessage", FailMessage }
};
var postback = Page.IsPostBack;
_Page_Load(HttpContext.Current, controls, sender, e, postback);
}
public void _Page_Load(HttpContext context, IDictionary<string, object> controls, object sender, EventArgs e, bool postback)
{
_registration = new Registration(this, new RegistrationData());
_controls = controls;
if (postback)
{
var email = (_controls["EmailText"] as TextBox).Text;
var password = (_controls["PasswordText"] as TextBox).Text;
var secret = (_controls["SecretPhraseText"] as TextBox).Text;
_registration.RegisterUser(email, secret, password);
}
}
#region IRegistrationView Members
public void Success()
{
var successMessageControl = _controls["SuccessMessage"] as HtmlGenericControl;
var multiViewControl = _controls["RegistrationMultiView"] as MultiView;
successMessageControl.InnerHtml = @"Congratulations! You've been successfully registered on Web Developement web site.";
successMessageControl.InnerHtml += @"<br /><br />Return to <a href=""Default.aspx"">Main page</a>.";
multiViewControl.SetActiveView(_controls["SuccessForm"] as View);
}
public void Fail(string p)
{
var failMessageControl = _controls["FailMessage"] as HtmlGenericControl;
var multiViewControl = _controls["RegistrationMultiView"] as MultiView;
failMessageControl.InnerHtml = p;
failMessageControl.InnerHtml += "<br /><br />Please try register again.";
multiViewControl.SetActiveView(_controls["FailedForm"] as View);
}
#endregion
}
* This source code was highlighted with Source Code Highlighter.Хочу отметить, что в реалной разработке, руководствуясь TDD, я сразу пишу код страницы таким образом, чтобы ее можно было тестировать. Т.е. сразу добавляю метод _Page_Load и контейнер контролов. Доступ к контролам, только через объект из контейнера.
Есть еще один тонкий момент. Если запустить тест сейчас, то он упадет с исключением, что невозможно установить View в MultiView, потому как данный View не есть Sub View, данного MultiView. Это вполне логично, и для того чтобы это исправить, необходимо подкорректировать метод инициализации контролов в тесте:
namespace WebApplication.Tests.Tests
{
[TestFixture]
public class RegistrationPageTests
{
#region common test code
private IDictionary<string, object> CreateControls()
{
var controls = new Dictionary<string, object> {
{ "RegistrationMultiView", new MultiView() },
{ "RegistrationForm", new View() },
{ "EmailText", new TextBox() },
{ "SecretPhraseText", new TextBox() },
{ "PasswordText", new TextBox() },
{ "SuccessForm", new View() },
{ "SuccessMessage", new HtmlGenericControl() },
{ "FailedForm", new View() },
{ "FailMessage", new HtmlGenericControl() }
};
(controls["RegistrationMultiView"] as MultiView).Views.Add(controls["RegistrationForm"] as View);
(controls["RegistrationMultiView"] as MultiView).Views.Add(controls["SuccessForm"] as View);
(controls["RegistrationMultiView"] as MultiView).Views.Add(controls["FailedForm"] as View);
return controls;
}
#endregion
* This source code was highlighted with Source Code Highlighter.
DbScript again
Наконец, тест может быть запущен, и он успешно пройдет.. но только один раз, на следующем запуске мы получим фейл. Все просто, мы зарегистрировали пользователя и теперь пытаемся зарегестрировать его снова, что противоречит требованиям. Тут как раз нам пригодиться класс DbScript, а именно: мы добавим в него код, который будет удалять созданную тестами запись. DbScript будет работать через туже модель, которую мы создали ранее. CleanUp удаляет все записи, в которых пароль начинается на "test".
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Company.Product.DAL;
namespace WebApplication.Tests.Framework
{
public class DbScript : IDisposable
{
private WebSiteModelDataContext _model = new WebSiteModelDataContext();
public DbScript()
{
Init();
}
private void Init()
{
}
#region IDisposable Members
public void Dispose()
{
Clean();
}
private void Clean()
{
DeleteTestUsers();
}
private void DeleteTestUsers()
{
var users = from u in _model.Users where u.Password.StartsWith(@"test") select u;
_model.Users.DeleteAllOnSubmit<User>(users);
_model.SubmitChanges();
}
#endregion
}
}
* This source code was highlighted with Source Code Highlighter.
Tests, test, test
Следующий тест, который уже явно проверяет фейл процесса регистрации.
[Test]
public void FailToRegsiterUser()
{
using (var fixture = new FixtutureInit("http://localhost/registration.aspx"))
{
//INIT
var page = new RegistrationView();
var controls = CreateControls();
(controls["EmailText"] as TextBox).Text = "exists@test.com";
(controls["SecretPhraseText"] as TextBox).Text = "bla-bla";
(controls["PasswordText"] as TextBox).Text = "test_pass2";
//ACT (post back)
page._Page_Load(HttpContext.Current, controls, new object(), new EventArgs(), true);
//POST
var expected = @"Sorry, but user with same e-mail is already registered on site.<br /><br />Please try register again.";
Assert.That((controls["FailMessage"] as HtmlGenericControl).InnerHtml, Is.EqualTo(expected));
Assert.That((controls["RegistrationMultiView"] as MultiView).GetActiveView(), Is.EqualTo((controls["FailedForm"] as View)));
}
}
* This source code was highlighted with Source Code Highlighter.
Для реализации тестового сценария необходимо уже иметь такую учетную запись. Тут мы снова обращаемя к DbScript. На этот раз добавим код создания нового объекта, в методе Init.
namespace WebApplication.Tests.Framework
{
public class DbScript : IDisposable
{
private WebSiteModelDataContext _model = new WebSiteModelDataContext();
public DbScript()
{
Init();
}
private void Init()
{
AddTestUser();
}
private void AddTestUser()
{
var user = new User()
{
Email = "exists@test.com",
SecretPhrase = "bla-bla",
Password = "test_pass2"
};
_model.Users.InsertOnSubmit(user);
_model.SubmitChanges();
}
* This source code was highlighted with Source Code Highlighter.
Заключение
Вопреки многим упрекам, что ASP.net Web Forms не пригоден (или крайне труден) для юнит тестирования, я с этим категорически не согласен. Для ASP.net Web Forms, при правльном подходе к делу, и соблюдении простых принципов, очень гладко ложится на методику разработки с тестами. Поэтому тесты, для ASP.net Web Forms могут и должны разрабатыватся.
Идеи не являются исчерпывающими, и естесвенно должны расширятся (или наоборот, сужатся) в зависимости от практической нужны и конкретных обстоятельств. Исходный код проекта можно скачать отсюда.












