Как макет 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

https://effort.codeplex.com/

Попробуйте это поможет вам эффективно высмеивать ваши данные.

Другие вопросы по тегам