В прошлой статье я начал рассмотрение общих вопросов разработки под веб с ASP.net (C#) 3.5. Сейчас рассмотрим более детальный пример, в котором я покажу одной из бизнес функций для сайта.
Веб сайт, на который надо добавить страницу регистрации. Лайаут страницы регистрации выглядит следующим образом (шаблон сайта взят http://www.oswd.org/):

Регистрация, это функция данного сайта, поэтому, как и все другие функции она будет реализована в библиотеке BLL (busines logic layer). Начнем, как обычно, с тестов. В проект Company.Product.BLL.Test добаляем новую фикстуру и первый, самый простой тест.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using NUnit.Framework;
namespace Company.Product.BLL.Tests
{
[TestFixture]
public class RegistrationTests
{
[Test]
public void Smoke()
{
var t = new Registration();
}
}
}
* This source code was highlighted with Source Code Highlighter.
Данный тест лишь проверяет, что существет определение Registration и объект класса может успешно создан. Может показатся, что такой тест лишний, но я предпочитаю всегда начинать именно с него.
Добавим Registration.cs класс в проект Company.Product.BLL, сделаем тестовый проект собирабельным и запустим тест. Тест будет зеленым, а это значит, что пора начинать.
Бизнес требования
Какие же конкретные действия мы ожидаем от страницы регистрации? Запишем их:
- если регистрация прошла успешно, показать приветсвие и перевести на главную страницу
- если пользователь с таким имейлом уже есть в системе, вывести предупреждение
- если во время регистрации произошла ошибка, показать сведения об ошибке пользователю
По моему это самое необходимое.. Если что-то будет нужно дополнительно, добавим в процессе реализации.
Подготовительные действия
Пока что Registration имеет только конструктор по умолчанию. Этого явно не достаточно, ведь бизнес объекту необходимо откуда-то получать данные.. а также передавать информацию в отображение. Поменяем конструкор следующи образом:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Company.Projects.Views;
using Company.Product.DAL;
namespace Company.Product.BLL
{
public class Registration
{
private IRegistrationView _view;
private IRegistrationData _data;
public Registration(IRegistrationView view, IRegistrationData data)
{
_view = view;
_data = data;
}
}
}
* This source code was highlighted with Source Code Highlighter.
Соответсвенно в проекты View и DAL добавим декларации интерфейсов IRegistrationView, IRegistrationData, которые пока что не содержат ни одного метода. Это позволит собрать проект BLL, но не тестовый проект.
В тестовый проект добавим фолдер Mocks, а в него два класса, RegistrationViewMock и RegistrationDataMock, которые реализовывают интерфейсы отображения и данных. Также нужно добавить референсы на View и DAL проекты.

Смоук тест после модификаций выглядит следующим образом
[Test]
public void Smoke()
{
var t = new Registration(new RegistrationViewMock(), new RegistrationDataMock());
}
* This source code was highlighted with Source Code Highlighter.
Забегая вперед отмечу, что почти все бизнес объекты имеют схожий конструктор.. принимающий 2 аргумента - отображение и данные (хотя не исключены и частные случаи, когда объекту нужны только отображение или, наоборот, только данные).
Тестовый проект запускается и собирается, поехали дальше.
Регистрация нового пользователя
Объект обладает методом регистрации, в который передаем и-мейл, секретную фразу и пароль, если все ОК, то у отображения будет вызван метод о успешной регистрации.
Реализуем данное требование в виде теста.
[Test]
public void RegisterNewUser()
{
//INIT
var view = new RegistrationViewMock();
var data = new RegistrationDataMock();
var register = new Registration(view, data);
//ACT
register.RegisterUser("email", "secret phrase", "password");
//POST
Assert.That(view.RegisterSuccess, Is.True);
}
* This source code was highlighted with Source Code Highlighter.
Добавим реализацию метода RegisterUser в бизнес объект (просто пустой метод), мок-объект отображения будет таким,
class RegistrationViewMock : IRegistrationView
{
private bool _success = false;
public bool RegisterSuccess
{
get { return _success; }
}
}
* This source code was highlighted with Source Code Highlighter.
Тест валится с сообщением:
TestCase 'Company.Product.BLL.Tests.RegistrationTests.RegisterNewUser' failed:
Expected: True
But was: False
* This source code was highlighted with Source Code Highlighter.
(это правильно, вообще тесты надо создавать так, чтобы первый запуск всегда был неудачным).
Вернемся к реализации, и не будем очень долго думать над ней.. делаем лишь то, что необходимо для прохождения теста, а именно
public void RegisterUser(string email, string secret, string password)
{
_view.Success();
}
* This source code was highlighted with Source Code Highlighter.
Используя средства студии генерируем новый метод интерфейса View, и добавляем реализацию метода в мок-объект.
#region IRegistrationView Members
public void Success()
{
_success = true;
}
#endregion
* This source code was highlighted with Source Code Highlighter.
Перезапустим тесты, и получим зеленый результат.
Регистрация пользователя с одинаковым имейлом
Исходя из наших требований имейл выступает ключем, и он не должен повторятся. Поэтому если происходит попытка регистрации пользователя с таким же имейлом, это дожно проводить к неудаче.
В коде это предлождение выглядит так,
[Test]
public void RegisterUserWithSameEmail()
{
//INIT
var view = new RegistrationViewMock();
var data = new RegistrationDataMock();
var register = new Registration(view, data);
//ACT
register.RegisterUser("email", "secret phrase", "password");
register.RegisterUser("email", "secret phrase", "password");
//POST
Assert.That(view.RegisterSuccess, Is.False);
Assert.That(view.FailureMessage, Is.EqualTo("Sorry, but user with same e-mail is already registered on site"));
}
* This source code was highlighted with Source Code Highlighter.
В мок для отображения добавим новое свойство типа string, FailureMessage. Запустим тест, и получим красный результат.
TestCase 'Company.Product.BLL.Tests.RegistrationTests.RegisterUserWithSameEmail'
failed:
Expected: False
But was: True
* This source code was highlighted with Source Code Highlighter.
Добавим самую простую реализацию, которая приходит в голову:
public void RegisterUser(string email, string secret, string password)
{
if (_email == email)
{
_view.Fail("Sorry, but user with same e-mail is already registered on site");
return;
}
_email = email;
_view.Success();
}
* This source code was highlighted with Source Code Highlighter.
Для отображения сгенеруем новый метод Fail, и реализуем его в моке.
public void Fail(string message)
{
_failureMessage = message;
_success = false;
}
* This source code was highlighted with Source Code Highlighter.
ОК, запустим тест.. и получим зеленый результат!
Теперь на секунду остановимся и задумаемся над корректностью теста - ведь, объект бизнес логики Registration это не персистеный объект, он живет на протяжении жизни страницы, и при открытии новой страницы (или пост-беке) объект будет создан заново. Наш тест это совершенно не учитывает.
Корректный сценарий использования выглядит так,
//ACT
register.RegisterUser("email", "secret phrase", "password");
register = new Registration(view, data);
register.RegisterUser("email", "secret phrase", "password");
* This source code was highlighted with Source Code Highlighter.
Естесвенно, что тест не проходит, так как наша текущая реализация не является корректной. Нам нужен персистентый объект, который сможет хранить информацию вне объекта бизнес логики. Такие функции должен предоставлять DAL приложения. Итак, начнем задействовать объект данных.
Изменим реализацию метода регистрации
public void RegisterUser(string email, string secret, string password)
{
if (_data.IsUserExists(email))
{
_view.Fail("Sorry, but user with same e-mail is already registered on site");
return;
}
_data.RegisterUser(email, secret, password);
_view.Success();
}
* This source code was highlighted with Source Code Highlighter.
И сгенирируем 2 новых метода у интерфейса данных.
#region IRegistrationData Members
public bool IsUserExists(string email)
{
throw new NotImplementedException();
}
public void RegisterUser(string email, string secret, string password)
{
throw new NotImplementedException();
}
#endregion
* This source code was highlighted with Source Code Highlighter.
Тесты не проходят, потому как оба метода выбрасывают исключения. Добавим реализацию,
#region IRegistrationData Members
public bool IsUserExists(string email)
{
return false;
}
public void RegisterUser(string email, string secret, string password)
{
}
#endregion
* This source code was highlighted with Source Code Highlighter.
Перезапустим тест, он все еще красный, но уже по другой причине:
TestCase 'Company.Product.BLL.Tests.RegistrationTests.RegisterUserWithSameEmail'
failed:
Expected: False
But was: True
* This source code was highlighted with Source Code Highlighter.
Проблема в том, что регистрация проходит успешно, а мы ожидаем, что должен быть фейл. Просто мок объекта данных ведет себя не совсем так, как должен вести себя в действительности. Добавим нужное поведение.
class RegistrationDataMock : IRegistrationData
{
private IList<string> _userEmails = new List<string>();
#region IRegistrationData Members
public bool IsUserExists(string email)
{
return (_userEmails.Contains(email));
}
public void RegisterUser(string email, string secret, string password)
{
//register user
_userEmails.Add(email);
}
#endregion
}
* This source code was highlighted with Source Code Highlighter.
После сборки и запуска, тест зеленый.
Неожиданная ошибка во время регистрации
Мы должны учитывать тот факт, что DAL будет обращатся к реальному источнику данных, а это значит, что мы должны быть готовы к тому, что методы DAL могут генерировать исключения.
[Test]
public void RegisterFailed()
{
//INIT
bool failOnRegister = true;
var view = new RegistrationViewMock();
var data = new RegistrationDataMock(failOnRegister);
var register = new Registration(view, data);
//ACT
register.RegisterUser("email", "secret phrase", "password");
//POST
Assert.That(view.RegisterSuccess, Is.False);
Assert.That(view.FailureMessage, Is.EqualTo("Sorry, but unexcpected exception happend during operation. Please try to register later"));
}
* This source code was highlighted with Source Code Highlighter.
Констуруктор дата объекта модифицируем так, чтобы он принимал флаг об успешном или не успешном прохождении регистрации. А метод регистрации поменяем так, чтобы он учитывал данный флаг.
public void RegisterUser(string email, string secret, string password)
{
if (_fail)
{
throw new Exception("RegisterUser method failed!");
}
//register user
_userEmails.Add(email);
}
* This source code was highlighted with Source Code Highlighter.
Результат запуска теста будет следующим,
TestCase 'Company.Product.BLL.Tests.RegistrationTests.RegisterFailed'
failed: System.Exception : RegisterUser method failed!
* This source code was highlighted with Source Code Highlighter.
Проблема в том, что бизнес объект не производит обработку исключений. Этого быть не должно, исключения дожны быть обработаны бизнес объектом и информация о проблемах должна передоваться в отображение.
Заключим все обращения к DAL в try/catch блок. И если исключение произошло, сообщим об этом в отображение.
public void RegisterUser(string email, string secret, string password)
{
try
{
if (_data.IsUserExists(email))
{
_view.Fail("Sorry, but user with same e-mail is already registered on site");
return;
}
_data.RegisterUser(email, secret, password);
_view.Success();
}
catch (Exception)
{
_view.Fail("Sorry, but unexcpected exception happend during operation. Please try to register later");
}
}
* This source code was highlighted with Source Code Highlighter.
Самое время перезапустить все тесты, дабы убедится что ничего не сломано предыдущимим изменениями.
------ Test started: Assembly: Company.Product.BLL.Tests.dll ------
4 passed, 0 failed, 0 skipped, took 1,58 seconds (NUnit 2.4).
* This source code was highlighted with Source Code Highlighter.Заключение
Посмотрим назад и оценим проделанную работу.
Интерфейсы отображения и данных:
namespace Company.Product.DAL
{
public interface IRegistrationData
{
bool IsUserExists(string email);
void RegisterUser(string email, string secret, string password);
}
}
namespace Company.Projects.Views
{
public interface IRegistrationView
{
void Success();
void Fail(string p);
}
}
* This source code was highlighted with Source Code Highlighter.
Код бизнес объекта:
namespace Company.Product.BLL
{
public class Registration
{
private IRegistrationView _view;
private IRegistrationData _data;
public Registration(IRegistrationView view, IRegistrationData data)
{
_view = view;
_data = data;
}
public void RegisterUser(string email, string secret, string password)
{
try
{
if (_data.IsUserExists(email))
{
_view.Fail("Sorry, but user with same e-mail is already registered on site");
return;
}
_data.RegisterUser(email, secret, password);
_view.Success();
}
catch (Exception)
{
_view.Fail("Sorry, but unexcpected exception happend during operation. Please try to register later");
}
}
}
}
* This source code was highlighted with Source Code Highlighter.
Мы прошли путь от бизнес требований, к реализации бизнес объекта, через тестирование. Именно бизнес объект стоял в центре разработки, и его нужды определяют конкретные интерфейсы IView и IData. Мы с состоянии протестировать бизнес объект даже не задумываясь как конкретно будет реализован DAL или View, но теперь мы точно знаем что именно необходимо получить от этих объектов.
В следующей статье мы продолжим реализацию, и создадим реальный DAL класс, реализующий интрефейс, требуемый бизнес объектом.

