Как макет Entity Framework в N-слойной архитектуре
У меня есть приложение N-Layer с Entity Framework (подход Code-First). Теперь я хочу автоматизировать некоторые тесты. Я использую Moq Framework. Я нахожу некоторые проблемы с написанием тестов. Возможно, моя архитектура не так? Под неправильным я подразумеваю, что я написал компоненты, которые плохо изолированы и поэтому не поддаются тестированию. Мне не очень нравится это... Или, возможно, я просто не могу правильно использовать moq framework.
Я позволю вам увидеть мою архитектуру:
На каждом уровне я делаю инъекцию context
в конструкторе класса.
Фасад:
public class PublicAreaFacade : IPublicAreaFacade, IDisposable
{
private UnitOfWork _unitOfWork;
public PublicAreaFacade(IDataContext context)
{
_unitOfWork = new UnitOfWork(context);
}
}
BLL:
public abstract class BaseManager
{
protected IDataContext Context;
public BaseManager(IDataContext context)
{
this.Context = context;
}
}
Репозиторий:
public class Repository<TEntity>
where TEntity : class
{
internal PublicAreaContext _context;
internal DbSet<TEntity> _dbSet;
public Repository(IDataContext context)
{
this._context = context as PublicAreaContext;
}
}
IDataContext
интерфейс, который реализован моим DbContext:
public partial class PublicAreaContext : DbContext, IDataContext
Теперь, как я издеваюсь EF
и как я пишу тесты:
[TestInitialize]
public void Init()
{
this._mockContext = ContextHelper.CreateCompleteContext();
}
куда ContextHelper.CreateCompleteContext()
является:
public static PublicAreaContext CreateCompleteContext()
{
//Here I mock my context
var mockContext = new Mock<PublicAreaContext>();
//Here I mock my entities
List<Customer> customers = new List<Customer>()
{
new Customer() { Code = "123455" }, //Customer with no invoice
new Customer() { Code = "123456" }
};
var mockSetCustomer = ContextHelper.SetList(customers);
mockContext.Setup(m => m.Set<Customer>()).Returns(mockSetCustomer);
...
return mockContext.Object;
}
И вот как я пишу свой тест:
[TestMethod]
public void Success()
{
#region Arrange
PrepareEasyPayPaymentRequest request = new PrepareEasyPayPaymentRequest();
request.CodiceEasyPay = "128855248542874445877";
request.Servizio = "MyService";
#endregion
#region Act
PublicAreaFacade facade = new PublicAreaFacade(this._mockContext);
PrepareEasyPayPaymentResponse response = facade.PrepareEasyPayPayment(request);
#endregion
#region Assert
Assert.IsTrue(response.Result == it.MC.WebApi.Models.ResponseDTO.ResponseResult.Success);
#endregion
}
Здесь вроде все работает правильно!!! И похоже, что моя архитектура верна. Но что, если я хочу вставить / обновить сущность? Ничего больше не работает! Я объясняю почему:
Как видите, я передаю *Request
объект (это DTO) к фасаду, затем в моем TOA я генерирую свою сущность из правильности DTO:
private PaymentAttemptTrace CreatePaymentAttemptTraceEntity(string customerCode, int idInvoice, DateTime paymentDate)
{
PaymentAttemptTrace trace = new PaymentAttemptTrace();
trace.customerCode = customerCode;
trace.InvoiceId = idInvoice;
trace.PaymentDate = paymentDate;
return trace;
}
PaymentAttemptTrace
это сущность, которую я вставлю в Entity Framework. Она не является поддельной, и я не могу ее внедрить. Таким образом, даже если я пропущу свой смоделированный контекст (IDataContext), при попытке вставить объект, который не смоделирован, мой тест не пройден!
Вот это сомнение в том, что у меня неправильная архитектура, поднялось!
Так что не так? Архитектура или как я использую moq?
Спасибо за помощь
ОБНОВИТЬ
Вот как я проверяю свой код.. Например, я хочу проверить след платежа..
Вот тест:
[TestMethod]
public void NoPaymentDate()
{
TracePaymentAttemptRequest request = new TracePaymentAttemptRequest();
request.AliasTerminale = "MyTerminal";
//...
//I create my request object
//You can see how I create _mockContext above
PublicAreaFacade facade = new PublicAreaFacade(this._mockContext);
TracePaymentAttemptResponse response = facade.TracePaymentAttempt(request);
//My asserts
}
Здесь фасад:
public TracePaymentAttemptResponse TracePaymentAttempt(TracePaymentAttemptRequest request)
{
TracePaymentAttemptResponse response = new TracePaymentAttemptResponse();
try
{
...
_unitOfWork.PaymentsManager.SavePaymentAttemptResult(
easyPay.CustomerCode,
request.CodiceTransazione,
request.EsitoPagamento + " - " + request.DescrizioneEsitoPagamento,
request.Email,
request.AliasTerminale,
request.NumeroContratto,
easyPay.IdInvoice,
request.TotalePagamento,
paymentDate);
_unitOfWork.Commit();
response.Result = ResponseResult.Success;
}
catch (Exception ex)
{
response.Result = ResponseResult.Fail;
response.ResultMessage = ex.Message;
}
return response;
}
Вот как я разработал PaymentsManager
:
public PaymentAttemptTrace SavePaymentAttemptResult(string customerCode, string transactionCode, ...)
{
//here the problem... PaymentAttemptTrace is the entity of entity framework.. Here i do the NEW of the object.. It should be injected, but I think it would be a wrong solution
PaymentAttemptTrace trace = new PaymentAttemptTrace();
trace.customerCode = customerCode;
trace.InvoiceId = idInvoice;
trace.PaymentDate = paymentDate;
trace.Result = result;
trace.Email = email;
trace.Terminal = terminal;
trace.EasypayCode = transactionCode;
trace.Amount = amount;
trace.creditCardId = idCreditCard;
trace.PaymentMethod = paymentMethod;
Repository<PaymentAttemptTrace> repository = new Repository<PaymentAttemptTrace>(base.Context);
repository.Insert(trace);
return trace;
}
В конце концов, как я написал хранилище:
public class Repository<TEntity>
where TEntity : class
{
internal PublicAreaContext _context;
internal DbSet<TEntity> _dbSet;
public Repository(IDataContext context)
{
//the context is mocked.. Its type is {Castle.Proxies.PublicAreaContextProxy}
this._context = context as PublicAreaContext;
//the entity is not mocked. Its type is {PaymentAttemptTrace} but should be {Castle.Proxies.PaymentAttemptTraceProxy}... so _dbSet result NULL
this._dbSet = this._context.Set<TEntity>();
}
public virtual void Insert(TEntity entity)
{
//_dbSet is NULL so "Object reference not set to an instance of an object" exception is raised
this._dbSet.Add(entity);
}
}
4 ответа
Ваша архитектура выглядит хорошо, но реализация имеет недостатки. Это утечка абстракции.
На вашей диаграмме слой Фасад зависит только от BLL, но когда вы смотрите на PublicAreaFacade
В конструкторе вы увидите, что в действительности он имеет прямую зависимость от интерфейса из уровня Repository:
public PublicAreaFacade(IDataContext context)
{
_unitOfWork = new UnitOfWork(context);
}
Этого не должно быть. Он должен принимать только свою прямую зависимость в качестве входных данных - PaymentsManager
или - еще лучше - интерфейс этого:
public PublicAreaFacade(IPaymentsManager paymentsManager)
{
...
}
Следовательно, ваш код становится более тестируемым. Когда вы смотрите на свои тесты, теперь вы видите, что вам нужно высмеивать самый внутренний слой вашей системы (т.е. IDataContext
и даже его объекты доступа Set<TEntity>
) хотя вы тестируете один из самых внешних слоев вашей системы (PublicAreaFacade
учебный класс).
Вот как юнит тест для TracePaymentAttempt
метод будет выглядеть, если PublicAreaFacade
зависит только от IPaymentsManager
:
[TestMethod]
public void CallsPaymentManagerWithRequestDataWhenTracingPaymentAttempts()
{
// Arrange
var pm = new Mock<IPaymentsManager>();
var pa = new PulicAreaFacade(pm.Object);
var payment = new TracePaymentAttemptRequest
{
...
}
// Act
pa.TracePaymentAttempt(payment);
// Assert that we call the correct method of the PaymentsManager with the data from
// the request.
pm.Verify(pm => pm.SavePaymentAttemptResult(
It.IsAny<string>(),
payment.CodiceTransazione,
payment.EsitoPagamento + " - " + payment.DescrizioneEsitoPagamento,
payment.Email,
payment.AliasTerminale,
payment.NumeroContratto,
It.IsAny<int>(),
payment.TotalePagamento,
It.IsAny<DateTime>()))
}
Похоже, вам нужно немного изменить код. Новые вещи вводят жестко запрограммированные зависимости и делают их непроверяемыми, поэтому постарайтесь абстрагировать их. Может быть, вы можете спрятать все, что связано с EF, за другим слоем, тогда все, что вам нужно сделать, это смоделировать этот слой и никогда не трогать EF.
Проходить IUnitOfWork
в конструктор слоя Facade или BLL, в зависимости от того, какой из них непосредственно вызывается для единицы работы. Затем вы можете настроить то, что Mock<IUnitOfWork>
возвращается в ваших тестах. Вам не нужно проходить IDataContext
ко всему, кроме, возможно, конструкторов репо и единицы работы.
Например, если у Фасада есть метод PrepareEasyPayPayment
это делает репо через UnitOfWork
вызов, настройте макет, как это:
// Arrange
var unitOfWork = new Mock<IUnitOfWork>();
unitOfWork.Setup(x => x.PrepareEasyPayPaymentRepoCall(request)).Returns(true);
var paymentFacade = new PaymentFacade(unitOfWork.Object);
// Act
var result = paymentFacade.PrepareEasyPayPayment(request);
Затем вы смоделировали вызов данных и можете легко протестировать свой код на Фасаде.
Для тестирования вставки у вас должен быть метод Facade, такой как CreatePayment
который занимает PrepareEasyPayPaymentRequest
, Внутри этого CreatePayment
метод, он должен ссылаться на репо, вероятно, через единицу работы, как
var result = _unitOfWork.CreatePaymentRepoCall(request);
if (result == true)
{
// yes!
}
else
{
// oh no!
}
То, что вы хотите подделать для модульного тестирования, это то, что этот вызов репо create / insert возвращает true или false, чтобы вы могли проверить ветви кода после того, как вызов репо завершен.
Вы также можете проверить, что вызов вставки был выполнен как ожидалось, но это обычно не так ценно, если параметры для этого вызова не имеют большой логики, связанной с их построением.
Вы можете использовать этот фреймворк с открытым исходным кодом для модульного тестирования, который хорош для макета сущностного фреймворка dbcontext
Попробуйте это поможет вам эффективно высмеивать ваши данные.