Возьмем для примера кусок кода, который отсылает клиенту счет за какие то товары с учетом доставки:
public class OrderProcessingModule
{
public void SendInvoice(Guid orderId)
{
// get order from database
Order order = GetOrder(orderId);
// calculate total price
double qtyPrice = 0;
if (order.Qty < 10)
{
qtyPrice += order.Qty * order.UnitPrice;
}
else
{
// apply discount for large quantities
qtyPrice += 0.9 * order.Qty * order.UnitPrice;
}
double shippingPrice = order.Shipping.IsInternational ? 10 : 5;
if (order.Shipping.IsExpress)
shippingPrice *= 2;
var totalPrice = qtyPrice + shippingPrice;
// prepare and send html-formatted message to customer
var message = CreateMessageForOrder(order, totalPrice);
var messagingGateway = new MessagingGateway();
messagingGateway.Send(message);
}
}
Вроде бы неплохой код, структурирован, часть функциональности вынесена в отдельные методы:
private Order GetOrder(Guid orderId)
{
string connectionString = ConfigurationManager.AppSettings["ConnectionString"];
using (var connection = new SqlConnection(connectionString))
{
// 'SELECT * FROM Order' lives here
var order = new Order();
return order;
}
}
private MailMessage CreateMessageForOrder(Order order, double totalPrice)
{
const string format = @"Define HTML template here";
var sb = new StringBuilder();
sb.AppendLine("" + order.UnitPrice + "");
sb.AppendLine("" + order.Qty + "");
sb.AppendLine("" + (order.Shipping.IsInternational ? "y/" : "n/") + (order.Shipping.IsExpress ? "y" : "n") + "");
sb.AppendLine("" + totalPrice + "");
var html = string.Format(format, order.OrderId, sb);
var mailMessage = new MailMessage();
mailMessage.Body = html;
// ...
return mailMessage;
}
Но как это тестировать?! Как проверить что в зависимости от заказа правильно расчитывается его стоимость, что клиенту отправляется правильно отформатированное письмо и что оно отправляется правильному клиенту? Никак!!! Вариант с тем, чтобы написать тест, который будет ходить по SMTP или IMAP к почтовику, забирать письмо и проверять что оно существует и правильно сформированно я, по определенным причинам, не рассматриваю.
И что делать, если требования поменялись и теперь в логике расчета общей стоимости нужно учитывать персональную скидку клиента. Однако для этого придется внести изменения в код, который кроме расчетов стоимости еще и занят выборкой данных из БД, форматированием письма и его отправкой. В примере всё достаточно очевидно и просто, но жизнь сложнее - слишком много шансов что-нибудь случайно сломать.
Single Responsibility Principle - A class should have one, and only one, reason to change.
Звучит красиво, но что это на самом деле? На самом деле это означает, что код, который отвечает за расчет стоимости должен быть выделен в отдельный класс. И не только этот код...
Попробуем выделить в отдельные классы код, ответственный за чтение из базы, расчет стоимости и форматирование:
class DataProvider
{
private readonly string _connectionString;
public DataProvider(string connectionString)
{
_connectionString = connectionString;
}
public Order GetOrder(Guid orderId)
{
using (var connection = new SqlConnection(_connectionString))
{
// 'SELECT * FROM Order' lives here
var order = new Order();
return order;
}
}
}
class CostCalculator
{
public double CalculateOrderCost(Order order)
{
double qtyPrice = 0;
if (order.Qty < 10)
{
qtyPrice += order.Qty * order.UnitPrice;
}
else
{
// apply discount for large quantities
qtyPrice += 0.9 * order.Qty * order.UnitPrice;
}
double shippingPrice = order.Shipping.IsInternational ? 10 : 5;
if (order.Shipping.IsExpress)
shippingPrice *= 2;
var totalPrice = qtyPrice + shippingPrice;
return totalPrice;
}
}
class MailFormatter
{
public MailMessage CreateMessageForOrder(Order order, double totalPrice)
{
const string format = @"Define HTML template here" ;
var sb = new StringBuilder();
sb.AppendLine("" + order.UnitPrice + "");
sb.AppendLine("" + order.Qty + "");
sb.AppendLine("" + (order.Shipping.IsInternational ? "y/" : "n/") + (order.Shipping.IsExpress ? "y" : "n") + "");
sb.AppendLine("" + totalPrice + "");
var html = string.Format(format, order.OrderId, sb);
var mailMessage = new MailMessage();
mailMessage.Body = html;
// ...
return mailMessage;
}
}
В результате наш OrderProcessingModule заметно упрощается:
public class OrderProcessingModule
{
public void SendInvoice(Guid orderId)
{
// get order from database
string connectionString = ConfigurationManager.AppSettings["ConnectionString"];
Order order = new DataProvider(connectionString).GetOrder(orderId);
// calculate total price
var totalPrice = new CostCalculator().CalculateOrderCost(order);
// prepare and send html-formatted message to customer
var message = new MailFormatter().CreateMessageForOrder(order, totalPrice);
var messagingGateway = new MessagingGateway();
messagingGateway.Send(message);
}
}
В принципе мы добились чего хотели - мы можем написать простые юнит-тесты на каждый кусок функциональности, изменить только один класс с правилами расчета стоимости, адаптировать тесты, еще раз убедиться что всё работает правильно и идти спать спокойно :-)
Однако неплохо бы иметь возможность оттестировать и сам контроллер в лице OrderProcessingModule, но сейчас это невозможно, так как для этого нам потребуются конфигурационный файл с параметрами соединения, развернутая БД и сервис для отправки сообщений. Что ж, попробуем решить и эту проблему:
public class OrderProcessingModule
{
private readonly IDataProvider _dataProvider;
private readonly ICostCalculator _costCalculator;
private readonly IMailFormatter _mailFormatter;
private readonly IMessagingGateway _messagingGateway;
public OrderProcessingModule(IDataProvider dataProvider, ICostCalculator costCalculator, IMailFormatter mailFormatter, IMessagingGateway messagingGateway)
{
if (dataProvider == null)
throw new ArgumentNullException("dataProvider");
// TODO: check all arguments
_dataProvider = dataProvider;
_costCalculator = costCalculator;
_mailFormatter = mailFormatter;
_messagingGateway = messagingGateway;
}
public void SendInvoice(Guid orderId)
{
// get order from database
Order order = _dataProvider.GetOrder(orderId);
// calculate total price
var totalPrice = _costCalculator.CalculateOrderCost(order);
// prepare and send html-formatted message to customer
var message = _mailFormatter.CreateMessageForOrder(order, totalPrice);
_messagingGateway.Send(message);
}
}
Теперь, вооружившись каким-нибудь Mock Framework'ом, можно приступать к написанию простых, понятных, быстрых юнит-тестов. Happy coding! :-)
Комментариев нет:
Отправить комментарий