Alexander Beletsky's Development Blog: 2010-05

Web development: Интеграционное тестирование страниц

Интеграцинный тест, это тест, который осуществляет проверку работоспобности объекта или совокупности объектов. В отличии от юнит теста, где фокусом тестирования выспупает метод (методы), в интеграционном тестировании фокусом есть объект (объекты).

Подготовка



В солюшине уже есть проект под названием 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 могут и должны разрабатыватся.

Идеи не являются исчерпывающими, и естесвенно должны расширятся (или наоборот, сужатся) в зависимости от практической нужны и конкретных обстоятельств. Исходный код проекта можно скачать отсюда.

Web development: Реализация бизнес отображения

Теперь, когда бизнес объект и объект данных готов, можно переходить непосредсвенно к реализации интерфейса функции.

Страница регистрации



Страница регистрации выглядит следующим образом:



Разработка



Требуется ввести и-мейл, секретную фразу (будет использована для функции востановления пароля) и сам пароль. Код страницы:

<%@ Page Language="C#" MasterPageFile="~/Concept.Master" AutoEventWireup="true" CodeBehind="Registration.aspx.cs" Inherits="WebApplication.RegistrationView" Title="Registration" %>
<asp:Content ID="Content1" ContentPlaceHolderID="head" runat="server">
</asp:Content>
<asp:Content ID="Content2" ContentPlaceHolderID="ContentPlaceHolder1" runat="server">
  <h2>Please register on Web development site</h2><br />
  <div class="box">
  <label>Email: </label><asp:TextBox ID="EmailText" runat="server"></asp:TextBox>
  <label>Secret Phrase: </label> <asp:TextBox ID="SecretPhraseText" runat="server"></asp:TextBox>
  <label>Password: </label> <asp:TextBox ID="PasswordText" runat="server"></asp:TextBox>   
  <label>&nbsp</label> <asp:Button ID="Submit" runat="server" Text="Register now!" CssClass="button"/>     
  </div>
</asp:Content>

* This source code was highlighted with Source Code Highlighter.


Все очень просто, лейблы, ASP.net контролы ввода текста, и кнопка сабмита формы. Тепеь перейдем к code-behind классу страницы, основная работа будет сосредоточенна там. Пока что это "пустой", ничего не делающий класс, имеющий однин метод Page_Load. Страница регистрации должна реализовавать интерфейс отображения, который мы спроектировани в прошлой статье. Поэтому, страницу RegistrationView мы наследуем от IRegistrationView, и генерируем методы, реализующие методы интерфейса.

using System;
using System.Collections;
using System.Configuration;
using System.Data;
using System.Linq;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;
using System.Xml.Linq;
using Company.Projects.Views;
using Company.Product.BLL;
using Company.Product.DAL;

namespace WebApplication
{
  public partial class RegistrationView : System.Web.UI.Page, IRegistrationView
  {
    private Registration _registration;

    protected void Page_Load(object sender, EventArgs e)
    {
      _registration = new Registration(this, new RegistrationData());

      if (Page.IsPostBack)
      {
        
      }
    }

    #region IRegistrationView Members

    public void Success()
    {
      
    }

    public void Fail(string p)
    {
      
    }

    #endregion
  }
}

* This source code was highlighted with Source Code Highlighter.


Подумаем над тем, как будем сообщать пользателю об успехе или неудаче регистрации. При успехе, пользователь должен увидеть сообщениие об успешной регистрации с предложением вернутся на главную страницу, в случае неудачи сообщить об ошибке и предложить повторить операцию еще раз. Для реализации опционального показа того или иного сообщения, в зависимости от ситуации, мы воспользуемся MultiView контролом. Добавим 3 view, соотвенно для формы регистрации, формы сообщениия о успехе, формы о сообщении об ошибке.

<asp:Content ID="Content2" ContentPlaceHolderID="ContentPlaceHolder1" runat="server">
  <h2>Please register on Web development site</h2><br />
  <div class="box">
    <asp:MultiView ID="RegistrationMultiView" runat="server" ActiveViewIndex="0">
      <asp:View ID="RegistrationForm" runat="server">
        <label>Email: </label><asp:TextBox ID="EmailText" runat="server"></asp:TextBox>
        <label>Secret Phrase: </label> <asp:TextBox ID="SecretPhraseText" runat="server"></asp:TextBox>
        <label>Password: </label> <asp:TextBox ID="PasswordText" runat="server"></asp:TextBox>   
        <label>&nbsp</label> <asp:Button ID="Submit" runat="server" Text="Register now!" CssClass="button"/>        </asp:View>
      <asp:View ID="SuccessForm" runat="server" >
      </asp:View>
      <asp:View ID="FailedForm" runat="server" >
      </asp:View>           
    </asp:MultiView>
  </div>
</asp:Content>

* This source code was highlighted with Source Code Highlighter.


Code-behind страницы поменяем так, чтобы он реагировал на сигналы о статусе операции, соответсвующим переключением views в multiview.

#region IRegistrationView Members

public void Success()
{
  RegistrationMultiView.SetActiveView(SuccessForm);
}

public void Fail(string p)
{
  RegistrationMultiView.SetActiveView(FailedForm);
}

#endregion

* This source code was highlighted with Source Code Highlighter.


Запустим приложение под дебаггером и убедимся, что методы Success и Fail успешно вызываются, в зависимости от статуса операции. Осталось добавить только необходимый HTML на страницу, а также код управляющий содержанием сообщений.

Код страницы:
<asp:Content ID="Content2" ContentPlaceHolderID="ContentPlaceHolder1" runat="server">
  <h2>Please register on Web development site</h2><br />
  <div class="box">
    <asp:MultiView ID="RegistrationMultiView" runat="server" ActiveViewIndex="0">
      <asp:View ID="RegistrationForm" runat="server">
        <label>Email: </label><asp:TextBox ID="EmailText" runat="server"></asp:TextBox>
        <label>Secret Phrase: </label> <asp:TextBox ID="SecretPhraseText" runat="server"></asp:TextBox>
        <label>Password: </label> <asp:TextBox ID="PasswordText" runat="server" TextMode="Password"></asp:TextBox>   
        <label>&nbsp</label> <asp:Button ID="Submit" runat="server" Text="Register now!" CssClass="button"/>        </asp:View>
      <asp:View ID="SuccessForm" runat="server" >
        <label ID="SuccessMessage" class="green" runat="server"></label>
      </asp:View>
      <asp:View ID="FailedForm" runat="server" >
        <label ID="FailMessage" class="red" runat="server"></label>
      </asp:View>           
    </asp:MultiView>
  </div>
</asp:Content>

* This source code was highlighted with Source Code Highlighter.


Инициализация сообщений:
#region IRegistrationView Members

public void Success()
{
  SuccessMessage.InnerHtml = @"Congratulations! You've been successfully registered on Web Developement web site.";
  SuccessMessage.InnerHtml += @"<br /><br />Return to <a href=""Default.aspx"">Main page</a>.";
  RegistrationMultiView.SetActiveView(SuccessForm);
}

public void Fail(string p)
{
  FailMessage.InnerHtml = p;
  FailMessage.InnerHtml += "<br /><br />Please try register again.";  
  RegistrationMultiView.SetActiveView(FailedForm);
}

#endregion

* This source code was highlighted with Source Code Highlighter.


Тестирование



Когда весь код находится в сборе, самое время запустить приложение и посмотреть, как оно работает. Поехали:

Заполняем форму регистрации:



Нажимаем кнопку регистрации, и получаем сообщение об успешной операции:



Повторим регистрацию с тем же и-мейлом:



В результате чего, получим сообщение, что пользователь с данным и-мейлом уже существует.



Проверим кейс, когда существую какие либо проблемы с базой. Для тестирования я взял и перенес содержимое папки App_Data во временный фолдер, таким образом приложение не сможет успешно соеденится с базой данных.



Заключение



В этой статье мы завершили реализацию функции регистрации на сайте, с помощью средств ASP.net Web Forms, используя парадигму MVC, и TDD как практику разработки. Следующих я покажу примеры интерграционного тестирования страницы, а также мы разработаем валидацию и функцию восстановления пароля с помощь AJAX.

Web development: Реализация объекта данных

В прошлой статье мы начали расматривать разработку бизнес функции сайта, руководствуюсь правилами DDD (Domain Driven Development) для подхода к решению, и TDD (Test Driven Development) как средства реализации. В итоге мы получили протестированный бизнес объект и определили два интерфейса, View (отображения) и Data (данных).

В этой статье мы реализуем Data объект.

Итак, требуемый интерфейс:

namespace Company.Product.DAL
{
  public interface IRegistrationData
  {
    bool IsUserExists(string email);
    void RegisterUser(string email, string secret, string password);
  }
}

* This source code was highlighted with Source Code Highlighter.


Начнем с первого, основого теста, предпологающего, что объект реализующий интерефейс уже существуют:

[Test]
public void Smoke()
{
  var t = new RegistrationData();
}

* This source code was highlighted with Source Code Highlighter.


Сгенирируем новый класс. И добавим первый тест:

[Test]
public void RegisterUser()
{
  //INIT
  var register = new RegistrationData();

  //ACT
  register.RegisterUser("email", "sec", "pass");

  //POST
  Assert.That(register.IsUserExists("email"), Is.True);
}

* This source code was highlighted with Source Code Highlighter.


Тесты не будут проходить, потому как оба метода выбрасывают исключения.


Мы вплотную подошли к вопросы реализации DAL. Прежде чем мы начнем, рассмотрим возможные фреймворки для реализации:

- ADO.net

- Linq to SQL

- Entities

- other ORM


ADO.net



Классический подход к базам данных в .Net приложениях. Использование ADO.net можно назвать низкоуровневым подходом к базам данных, которые предполагает ручное создание соединения, составление и выполение SQL иструкций. Как правило поверх ADO.net API создается ряд оберток, облегчающий эти задачи. На сеголняший день использование чистого ADO.net является крайне тяжеловесным, и по возможности его надо изберать.


Linq to Sql




Сам по себе Linq появился в .Net начная с версии 3.5. Это акромним к Language-Integrated Query, и предстявляет собой встроенные средства в .Net языки (C#, VB, F#) по доступу и манипуляции данных. Linq позволяет прозрачно и одинаково работать с тами источниками данных как ADO.net, .Net коллекции, XML документы. Будучи поразительно простым по своиму синтаксису, язык запросов Linq предоствляет действительно широкие возможности. Инструкциями select, from, where напоминающий SQL код, производятся как выборки и трансформации данных. Linq to SQL включает в себя ORM (object relation management) с удобным дизайнером встроеным в IDE Visual Studio.


Entities framework




Появился в .Net 3.5 SP1 и впринципе является эволюционным витком ADO.net. Более сложный нежели Linq to Sql, в свою очередь обладает большими возможностями, в частности, в отличии от того же Linq to Sql, в Entities существует поддержка связей M-M. Лично я не сталкивался с ним слишком близко, поэтому не могу сказать многое, но судя по записям в MSDN и блогам, именно Entities будет более поддержан в следующих версиях .Net, и позиционируется как основное средство работы с данными в .Net приложениях.


Other ORM




Из возможных вариантов может быть, NHibernate и SubSonic. NHibernate это порт фреймворка Hibernate Java. SubSonic разработка Роба Конери работающего в Microsoft. При всей взрослости Hibernate для Java, NHibernate не смог достич его уровня. Лично мой опыт опыт показывает, что по возможности от NHibernate надо отказыватся (если несколько лет назад его использование могло быть оправданно, то с перешним наличием ORM фреймворков нет). SubSonic выпустил уже 3ю версию, и объединяет вокруг себя довольно большой комьюнити и функциональных возможностей. Оба проекта используют open source модель. Если бы нужно было выбирать между NHibernate и SubSonic, я бы более склонялся ко второму.


Если опиратся на технологии Microsoft, то выбор ложится между Linq to Sql и Entities Framework. Сравнение возможностей можно посмотреть в этой статье. Правило примерно следующее: для малых и средних приложений, с не очень сложной структурой данных выбираем Linq to Sql, в противном случае Entities Framework.


Мой выбор Linq to Sql.


База данных




Добавим новую базу данных в проект. В WebApplication, в App_Data добавляем websitedb.mdf. Это пока что пустая база не сожержащая ни одной таблицы.





Добавим первую таблицу, Users. Это простая таблица сожержащая Id, Email, SecretPhase, Password.





Модель данных




Теперь, когда таблица уже готова, мы можем сгенерировать классы доступа. В проекте Company.Product.DAL добавим новый элемент Linq To Sql Classes





Пустой Linq to Sql Classes файл будет добавлен в проект (WebSite.dbml). Теперь добавим класс, который будет замаплен на таблицу Users. Для это мы просто перетягиваем таблицу Users из Solution Explorer в дизайн панель dbml.





Теперь сделаем небольшие настройки. Правой кнопкой щекнем по полю Id, и устновим следующие свойства:


Access: Private
Auto Generate Values: True

* This source code was highlighted with Source Code Highlighter.






Тем самым мы запретим непосредственный доступ к полю Id, а также сделаем его автогенерируемым.


Теперь посмотрим на код, который мы получили в результате.


namespace Company.Product.DAL
{
  // .
   
  [System.Data.Linq.Mapping.DatabaseAttribute(Name="websitedb")]
  public partial class WebSiteModelDataContext : System.Data.Linq.DataContext
  {
    //...
  }
}

* This source code was highlighted with Source Code Highlighter.


WebSiteModelDataContext - класс представляющий entry point Linq to Sql classes. Через DataContext осуществляется доступ ко всем классам даныых (mapped classes). Инстанциирование этого класса это дешевая операция, поэтому объект DataContext обычно создается в скоупе методов, или коротко живущих классов инкапсулирующих операции с базами данных.


namespace Company.Product.DAL
{
  public class RegistrationData : IRegistrationData
  {
    WebSiteModelDataContext _model = new WebSiteModelDataContext();
   
    //...

* This source code was highlighted with Source Code Highlighter.


Классы записей в базе данных:


[Table(Name="dbo.Users")]
public partial class User

  private int _Id; 
  private string _Email;
  private string _SecretPhrase;
  private string _Password;
 
  public User()
  {
  }
 
  //...
 
  [Column(Storage="_Id", DbType="Int NOT NULL IDENTITY", IsDbGenerated=true)]
  private int Id
  {
    get
    {
      return this._Id;
    }
    set
    {
      if ((this._Id != value))
      {
        this._Id = value;
      }
    }
  }
   
  //...
}

* This source code was highlighted with Source Code Highlighter.


User - класс представляющий кортеж в таблице Users. Все свойства аттрибутированны согласно определениям в базе данных. Очень просто читается, и с ним без труда можно разобратся. Кстати, даже если таблица называется Users то класс данных все равно имеет название User, что вполне логично и корректно.


Одним из замечательных свойств DataContext класса, является то, что он полностью инкапсулируют в себе конфигурацию, поэтому не нужно вручную возится с web.config, и вообще с какими либо настройками.


Реализация RegistrationData класса




Ну вот, все готово, чтобы вернутся к тестам и реализации методов RegistrationData. Метод RegisterUser должен создать новую запись в базе данных, а IsUserExists проверить сущесвует ли запись с заданным электронным адресом. Эти два очень простых метода:


public bool IsUserExists(string email)
{
  var user = _model.Users.Single(p => p.Email == email);
  return user != null;
}

public void RegisterUser(string email, string secret, string password)
{
  var user = new User()
  {
    Email = email,
    SecretPhrase = secret,
    Password = password
  };

  _model.Users.InsertOnSubmit(user);
  _model.SubmitChanges();
}

* This source code was highlighted with Source Code Highlighter.


RegisterUser создает новый объект User, инициалирует поля и передает его в модель. Метод SubmitChanges осуществялет непосредвенную запись данных в базу.


IsUserExists обращается к таблице Users и пытается излечь единственную запись, имейл которой совпадает с заданным.


Если мы попробуем запустить тест сейчас, то получим исключение, о том что в таблице отсутвует primary key. Перейдем в дизайнер и назначим поле Email - Primary Key.





Теперь, при запуске тестов порядок.


Хотя в бизнес объекте мы всегда проверяем, есть ли пользователь с таким и-мейлом перед тем как провести регистрацию, мы должны протестировать такой случай и убедится, что это не возможно.


[Test]
public void RegisterUserTwice()
{
  //INIT
  var register = new RegistrationData();

  try
  {
    //ACT
    register.RegisterUser("email", "sec", "pass");
    register.RegisterUser("email", "sec", "pass");
  }
  catch (DuplicateKeyException e)
  {
    //OK
    return;
  }

  //POST
  Assert.Fail();
}

* This source code was highlighted with Source Code Highlighter.



Запусим тест, и получим зеленый результат.


Улучшения тестов




Даже если тесты прошли успешно, уже на втором запуске они покраснеют. Все дело в том, что запись email-sec-pass уже существует, повторная запись не возможна. Я уже писал в одной их статей, что основной задачей тестирования в DAL является корректная подготовка и очистка базы. Если подготовка в данном случае не нужна, то очистка это как раз то, что мешает тестам быть повторяемым. К счастью в Linq to Sql есть хорошо продуманная система транзакций, частным видом которой есть неявные трансакции. В неявных трансакциях во время вызова SubmitChanges происходит, и если вызов происходит в скоупе трансакции то фактическая запись данных не будет осществлятся пока транзакция не будет завершена. Для использования неявных трансакций существует класс помошник TransactionScope.


Поменяем код тестов таким образом,


[TestFixture]
public class RegistrationDataTests
{
  TransactionScope _transaction;

  [SetUp]
  public void Setup()
  {
    _transaction = new TransactionScope();
  }

  [TearDown]
  public void TearDown()
  {
    if (_transaction != null)
    {
      _transaction.Dispose();
      _transaction = null;
    }
  }

* This source code was highlighted with Source Code Highlighter.



SetUp и TearDown методы соответвенно вызываются перед и после каждого теста, т.е. для каждого теста мы создаем новую трансакцию, после выполнения разрушаем ее без коммита изменений. Таким образом данные созданные в тесте в базу данных не попадают.


Заключение




Ну вот, второй важный шаг завершен.. и теперь у нас есть, как бизнес объект, так и объект данных. Нам осталось добавить код отображения, интерировать все воедино, что мы сделаем в следующих статьях.