Tho*_*mas 11 c# sql unit-testing transactions
由于一些限制,我无法使用实体框架,因此需要手动使用SQL连接,命令和事务.
在为调用这些数据层操作的方法编写单元测试时,我偶然发现了一些问题.
对于单元测试,我需要在事务中执行它们,因为大多数操作都是按其性质更改数据,因此在事务之外执行它们会产生问题,因为这会改变整个基础数据.因此,我需要围绕这些进行事务处理(最后没有提交任何提交).
现在我有两种不同的变体,这些BL方法的工作方式.一些交易本身就在其中,而其他交易本身则没有交易.这两种变体都会引起问题.
分层事务:在这里我得到DTC由于超时而取消分布式事务的错误(尽管超时设置为15分钟且运行仅2分钟).
只有1个事务:当我来到"new SQLCommand"
被调用方法的行时,我得到一个关于事务状态的错误.
我的问题是如何解决这个问题并通过手动普通和分层交易进行单元测试?
单元测试方法示例:
using (SqlConnection connection = new SqlConnection(Properties.Settings.Default.ConnectionString))
{
connection.Open();
using (SqlTransaction transaction = connection.BeginTransaction())
{
MyBLMethod();
}
}
Run Code Online (Sandbox Code Playgroud)
使用方法的事务示例(非常简化)
using (SqlConnection connection = new SqlConnection(Properties.Settings.Default.ConnectionString))
{
connection.Open();
using (SqlTransaction transaction = connection.BeginTransaction())
{
SqlCommand command = new SqlCommand();
command.Connection = connection;
command.Transaction = transaction;
command.CommandTimeout = 900; // Wait 15 minutes before a timeout
command.CommandText = "INSERT ......";
command.ExecuteNonQuery();
// Following commands
....
Transaction.Commit();
}
}
Run Code Online (Sandbox Code Playgroud)
非Transaction使用方法的示例
using (SqlConnection connection = new SqlConnection(Properties.Settings.Default.ConnectionString))
{
connection.Open();
SqlCommand command = new SqlCommand();
command.Connection = connection;
command.CommandTimeout = 900; // Wait 15 minutes before a timeout
command.CommandText = "INSERT ......";
command.ExecuteNonQuery();
}
Run Code Online (Sandbox Code Playgroud)
从表面上看,您有几个选择,具体取决于您要测试的内容以及您花钱/更改代码库的能力.
目前,您正在有效地编写集成测试.如果数据库不可用,那么您的测试将失败.这意味着测试可能会很慢,但如果它们通过,那么你可以非常确信代码可以正确地访问数据库.
如果您不介意访问数据库,那么更改代码/支出金额的最小影响将是允许您完成事务并在数据库中验证它们.您可以通过获取数据库快照并在每次测试运行时重置数据库,或者通过使用专用测试数据库并以可以安全地反复访问数据库然后验证的方式编写测试来执行此操作.因此,例如,您可以插入具有递增ID的记录,更新记录,然后验证是否可以读取它.如果有错误,你可能有更多的解散,但如果你没有修改数据访问代码或数据库结构,那么这通常不会是一个太大的问题.
如果你能够花一些钱并且你想要将你的测试变成单元测试,以便他们不会访问数据库,那么你应该考虑调查TypeMock.它是一个非常强大的模拟框架,可以做一些非常可怕的东西.我相信它使用分析API来拦截调用,而不是使用像Moq这样的框架使用的方法.有使用Typemock嘲笑的SQLConnection的例子在这里.
如果您没有钱花钱/您可以更改代码并且不介意继续依赖数据库,那么您需要查看一些方法来共享您的测试代码和数据访问方法之间的数据库连接.我想到的两种方法是将连接信息注入到类中,或者通过注入可以访问连接信息的工厂使其可用(在这种情况下,您可以在测试期间注入工厂的模拟,返回连接你要).
如果您采用上述方法,而不是直接注入SqlConnection
,请考虑注入一个也负责事务的包装类.就像是:
public class MySqlWrapper : IDisposable {
public SqlConnection Connection { get; set; }
public SqlTransaction Transaction { get; set; }
int _transactionCount = 0;
public void BeginTransaction() {
_transactionCount++;
if (_transactionCount == 1) {
Transaction = Connection.BeginTransaction();
}
}
public void CommitTransaction() {
_transactionCount--;
if (_transactionCount == 0) {
Transaction.Commit();
Transaction = null;
}
if (_transactionCount < 0) {
throw new InvalidOperationException("Commit without Begin");
}
}
public void Rollback() {
_transactionCount = 0;
Transaction.Rollback();
Transaction = null;
}
public void Dispose() {
if (null != Transaction) {
Transaction.Dispose();
Transaction = null;
}
Connection.Dispose();
}
}
Run Code Online (Sandbox Code Playgroud)
这将阻止嵌套事务的创建和提交.
如果您更愿意重构代码,那么您可能希望以更可模仿的方式包装数据访问代码.因此,例如,您可以将核心数据库访问功能推送到另一个类.根据您正在做的事情,您需要对其进行扩展,但最终可能会出现以下情况:
public interface IMyQuery {
string GetCommand();
}
public class MyInsert : IMyQuery{
public string GetCommand() {
return "INSERT ...";
}
}
class DBNonQueryRunner {
public void RunQuery(IMyQuery query) {
using (SqlConnection connection = new SqlConnection(Properties.Settings.Default.ConnectionString)) {
connection.Open();
using (SqlTransaction transaction = connection.BeginTransaction()) {
SqlCommand command = new SqlCommand();
command.Connection = connection;
command.Transaction = transaction;
command.CommandTimeout = 900; // Wait 15 minutes before a timeout
command.CommandText = query.GetCommand();
command.ExecuteNonQuery();
transaction.Commit();
}
}
}
}
Run Code Online (Sandbox Code Playgroud)
这允许您对命令生成代码中的更多逻辑进行单元测试,而无需实际担心命中数据库,并且可以针对数据库测试核心数据访问代码(Runner)一次,而不是针对您想要的每个命令对数据库运行.我仍然会为所有数据访问代码编写集成测试,但我只倾向于在实际处理代码段时运行它们(以确保正确指定了列名等).