Вымывание транзакций базы данных?
У меня есть пара таблиц с родительскими/дочерними отношениями - инцидент и инцидент. У меня есть viewmodel, который содержит информацию из обеих этих таблиц. И у меня есть метод бизнес-уровня, которому передается экземпляр viewmodel, который должен обновлять обе таблицы.
Итак, в методе я использую новый механизм транзакций EF6:
using (var transaction = this.db.Database.BeginTransaction())
{
try
{
// various database stuff
this.db.SaveChanges();
// more database stuff
this.db.SaveChanges();
// yet more database stuff
this.db.SaveChanges();
transaction.Commit();
}
catch (Exception ex)
{
transaction.Rollback();
this.logger.logException(ex, "Exception caught in transaction, rolling back");
throw;
}
}
Итак, моя проблема. Как проверить это?
Я использую платформу тестирования модулей Microsoft с помощью Moq, и у меня не было проблем с издевательством DBContexts и DbSet < > s, но я не могу понять, как обойти транзакционные вещи.
Если я не пытаюсь издеваться над транзакцией, я получаю InvalidOperationException:
"В приложении не может быть найдена соединительная строка с именем xxx config."
Что имеет смысл - нет файла конфигурации приложения, и нет никакой базы данных.
Но если я попытаюсь высмеять BeginTransaction(), я получу ошибки инициализации: NotSupportedException:
"Неверная настройка для не виртуального элемента: m = > m.Database.BeginTransaction".
И это заставило меня преследовать сорняки, глядя на декомпилирующие методы .NET, пытаясь определить какой-то класс, который может быть получен из удобного интерфейса или что-то еще, где я мог бы каким-то образом ввести насмешливый объект.
Я не пытаюсь выполнить блок-транзакцию MS-транзакции - я просто хочу убедиться, что соответствующие изменения внесены в соответствующие записи в каждой из таблиц. Но по мере того как он сидит, похоже, что это не проверяемое, и что любой метод, который использует транзакции, не поддается тестированию. И это просто боль.
У меня Googled вокруг, и не нашел ничего полезного. Кто-нибудь сталкивался с этой проблемой? У кого-нибудь есть идеи о том, как действовать?
Ответы
Ответ 1
Тестирование такого рода вещей всегда сложно, но прежде всего вы должны спросить себя, хотите ли вы unit test свою бизнес-логику или хотите интегрировать тестирование своего приложения.
Если вы хотите unit test свою логику, вы в принципе не должны даже пытаться фальсифицировать инфраструктуру сущности, потому что вы не хотите тестировать EF, вы просто хотите проверить свой код, правильно?
Для этого высмеивайте любой объект доступа к данным и только unit test свою бизнес-логику.
Но если вы хотите проверить, работает ли ваш уровень доступа к данным, например, если ваш код может обрабатывать все выполненные вами CRUD-операции, вы должны выполнить тесты интеграции с реальной базой данных. Не пытайтесь издеваться над объектами доступа к данным (EF) в этом случае, просто выполните тесты с тестовой базой данных или sql-express localDB, например.
Ответ 2
Вы можете обернуть контекст и транзакцию в интерфейсе, а затем реализовать интерфейс с помощью некоторого класса поставщика:
public interface IDbContextProvider
{
YourContext Context { get; set; }
DbContextTransaction DbTransaction { get; set; }
void Commit();
void Rollback();
void BeginTransaction();
void SaveChanges();
}
а затем выполните его:
public class EfContextProvider : IDbContextProvider
{
public EfContextProvider(YourContext context)
{
Context = context;
}
public YourContext Context { set; get; }
public DbContextTransaction DbTransaction { set; get; }
public void Commit()
{
DbTransaction.Commit();
}
public void Rollback()
{
DbTransaction.Rollback();
}
public void BeginTransaction()
{
DbTransaction=Context.Database.BeginTransaction();
}
public void SaveChanges()
{
Context.SaveChanges();
}
}
теперь давайте вашему классу зависимость IDbContextProvider и работаем с ним (у него также есть контекст внутри). Возможно, замените используемый блок на _contextProvider.BeginTransaction(); а затем также _contextProvider.Commit(); или _contextProvider.Rollback();
Ответ 3
Я потратил несколько часов, пытаясь понять это, я полагал, что это может быть сделано MS Fakes напрямую без оболочки или нового класса.
Вам нужно выполнить три шага:
- Создайте объект shim для DbContextTransaction и объедините его методы Commit и Rollback, чтобы ничего не делать.
- Создайте объект shim для базы данных. И объедините его метод BeginTransaction, чтобы вернуть объект Shim объекта DbContextTransaction, созданный на шаге 1.
- Свойство Detour DbContext.Database для всех экземпляров, чтобы вернуть объект Shim базы данных, созданный на шаге 2.
И все.
static void SetupDBTransaction()
{
System.Data.Entity.Fakes.ShimDbContextTransaction transaction = new System.Data.Entity.Fakes.ShimDbContextTransaction();
transaction.Commit = () => { };
transaction.Rollback = () => { };
System.Data.Entity.Fakes.ShimDatabase database = new System.Data.Entity.Fakes.ShimDatabase();
database.BeginTransactionIsolationLevel = (isolationLevel) =>{return transaction.Instance;};
System.Data.Entity.Fakes.ShimDbContext.AllInstances.DatabaseGet = (@this) => { return database.Instance; };
}
Ответ 4
Вы можете представлять классы EF как классы POCO и изолировать все взаимодействия с базами данных в классах адаптеров базы данных. У этих классов адаптеров был бы интерфейс, который вы могли бы высмеять при тестировании бизнес-логики.
Операции с базой данных в классах адаптеров могут быть протестированы с использованием реального соединения с базой данных, но с выделенной базой данных и строкой соединения для модульных тестов.
Итак, как насчет тестирования бизнес-кода, заключенного в транзакции?
Чтобы изолировать бизнес-код от адаптеров базы данных, вам нужно будет создать интерфейс для области транзакций EF, который вы можете высмеять.
Я ранее работал с таким дизайном, хотя не с EF, но с аналогичной упаковкой POCO (в псевдо С#, а не с синтаксисом или проверкой здравого смысла):
interface IDatabaseAdapter
{
ITransactionScope CreateTransactionScope();
}
interface ITransactionScope : IDisposable
{
void Commit();
void Rollback();
}
class EntityFrameworkTransactionScope : ITransactionScope
{
private DbContextTransaction entityTransaction;
EntityFrameworkTransactionScope(DbContextTransaction entityTransaction)
{
this.entityTransaction = entityTransaction;
}
public Commit() { entityTransaction.Commit(); }
public Rollback() { entityTransaction.Rollback(); }
public Dispose() { entityTransaction.Dispose(); }
}
class EntityFrameworkAdapterBase : IDatabaseAdapter
{
private Database database;
protected EntityFrameworkAdapterBase(Database database)
{
this.database = database;
}
public ITransactionScope CreateTransactionScope()
{
return new EntityFrameworkTransactionScope(database.BeginTransaction());
}
}
interface IIncidentDatabaseAdapter : IDatabaseAdapter
{
SaveIncident(Incident incident);
}
public EntityIncidentDatabaseAdapter : EntityFrameworkAdapterBase, IIncidentDatabaseAdapter
{
EntityIncidentDatabaseAdapter(Database database) : base(database) {}
SaveIncident(Incident incident)
{
// code for saving the incident
}
}
Вышеупомянутая конструкция должна позволять вам создавать unit test для операций с инфраструктурой сущностей, не беспокоясь о бизнес-логике или транзакции, а также создавать модульные тесты для бизнес-логики, где вы можете издеваться над сбоями базы данных и использовать MOQ или аналогичные для проверки того, что откат находится в факт вызвал ваш ITransactionScope макет.
Что-то вроде выше, вы должны быть способны покрыть практически любую транзакционную неудачу на любом этапе бизнес-логики, о котором вы можете думать.
Конечно, вы должны дополнить свои модульные тесты хорошими интеграционными тестами, поскольку транзакции могут быть сложными, особенно сложные взаимоблокировки могут возникать при одновременном использовании, и их трудно поймать в издеваемом тесте.
Ответ 5
Что вам нужно - это то, что вы можете называть Commit() и Rollback() и имеет форму System.Data.Entity.DbContextTransaction, правильно? Таким образом, оказывается, что вы можете использовать реальное DbContextTransaction в ЛЮБОЙ реальной базе данных. Затем, пока ни один из ваших тестовых кодов не внесет никаких реальных изменений в базу данных, используемую для транзакции, Commit() или Rollback() будут успешными и ничего не сделают.
В моем приложении уровень веб-api должен выполнять несколько операций бизнес-логики внутри транзакции db, так что, если вторая операция получает ошибку, первая операция так и не была выполнена. Я добавил метод для моего интерфейса бизнес-логики, чтобы вернуть транзакцию, которую может использовать веб-слой api. В моем тестировании этого кода я высмеиваю метод, чтобы вернуть DbContextTransaction в пустой тестовой базе данных. Вот код настройки, который я использовал:
var scope = (new PConn.DataAccess.PressConnEntities()).Database.BeginTransaction();
var bizl = new Mock<IOrderMgr>();
bizl.Setup(m => m.CreateNewOrder(7, It.IsAny<string>(), It.IsAny<string>())).Returns(_testOrder1);
// .GetOrdersQuery(channel, beginUTC, endUTC);
bizl.Setup(m => m.GetOrdersQuery(7, It.IsAny<DateTime>(), It.IsAny<DateTime>())).Returns(matchedOrdersList.AsQueryable());
bizl.Setup(m => m.BeginTransaction()).Returns(scope);
Для проблемы, которую вы пытаетесь решить, важна только первая строка фрагмента кода и последняя строка.
Подводя итог:
- Измените свой код, чтобы получить транзакцию из метода, который вы можете высмеять в тестах.
- Возвратите реальную транзакцию в тестовую базу данных, когда вы издеваетесь над методом, который возвращает транзакцию.
- Убедитесь, что ваш тестовый файл App.Config имеет законную конфигурацию для подключения к тестовой базе данных.
- Commit() и откат() для вашего сердечного контента. Никакие данные не изменяются, потому что вы издевались над всеми операциями DbSet и DbContext.
Вот пример тестируемого кода, в котором я использую (не очень) фальшивую транзакцию:
using (var scope = this.OrderManager.BeginTransaction())
{
PrintOrder pconnOrder = this.OrderManager.CreateNewOrder(channel, payload, claimsIdentity.Name);
bool parseResult = this.OrderManager.ParseNewOrder(pconnOrder, claimsIdentity.Name, out parseErrorMessage);
if (!parseResult)
{
// return a fault to the caller
HttpResponseMessage respMsg = new HttpResponseMessage(HttpStatusCode.BadRequest);
respMsg.Content = new StringContent(parseErrorMessage);
throw (new HttpResponseException(respMsg));
}
scope.Commit();
return (pconnOrder.PrintOrderID);
}