使用 FakeItEasy 在 NUnit 测试中伪造 SqlConnection 和嵌入式方法

Faking SqlConnection and embedded methods in the NUnit Test, using FakeItEasy

我有以下 class 和方法,将连接到数据库,但为了测试,我不需要真正的连接,需要伪造它。我们为此使用 FakeItEasy。:

public abstract class HandlerBase
    {
        public string errorMessage;

        private MyActionsDataModel Action
        {
            get
            {
                if (_action == null)
                {
                    _action = new MyActionsDataModel();
                    using (var connection = new SqlConnection(Constants.Connections.MyDatabase))
                    {
                        connection.Open();
                        using (var transaction = connection.BeginTransaction(IsolationLevel.Serializable))
                        {
                            _action.Id = connection.QuerySingle<int>("UpdateAction", transaction: transaction, commandType: CommandType.StoredProcedure, param: Action);
                            transaction.Commit();
                        }
                    }
                }
                return _action;
            }
        }
        private MyActionsDataModel _action;

        public void RecordFailure(AggregateException ex)
        {
            Console.WriteLine("A failure happened:");
            Console.WriteLine(JsonConvert.SerializeObject(ex));
            errorMessage = "Inner Exception\r\n" + ex.Message;
            Action.ErrorOccurredOnUtc = DateTimeOffset.UtcNow;
            Action.ErrorType = ex.GetType().FullName;
            Action.ErrorMessage = errorMessage;
            SaveAction();
        }

        private void SaveAction()
        {
            using (var connection = new SqlConnection(Constants.Connections.MyDatabase))
            {
                connection.Open();
                using (var transaction = connection.BeginTransaction(IsolationLevel.Serializable))
                {
                    connection.Execute("UpdateAction", transaction: transaction,
                        commandType: CommandType.StoredProcedure, param: Action);
                    transaction.Commit();
                }
            }
        }
     }

我将在测试中调用的另一个 class:

public class MyHandlerType : HandlerBase
    {
        private readonly MyTracker _myTracker;

        public MyHandlerType(MyTracker myTracker) : base()
        {
            _myTracker = myTracker;
        }
    }

我想要的是伪造 Action 参数以及 SaveAction 方法。

这是我的测试,但不确定如何制作假部件。

public class HandlerTests
    {
        [TestCase]
        public void Test_RecordFailure()
        {
            var innerMessage = "Throw AppException for UnitTest.";
            var parentMessage = "Throw AggregationException for UnitTest.";

            var testHandler = new MyHandlerType(null);
            var innerException = new ApplicationException(innerMessage);
            var parentException = new AggregateException(parentMessage, innerException);
            testHandler.RecordFailure(parentException);
            var includeInnerMessage = testHandler.errorMessage.Contains(innerMessage);
            var includeParentMessage = testHandler.errorMessage.Contains(parentMessage);

            Assert.IsTrue(includeInnerMessage);
            Assert.IsTrue(includeParentMessage);
        }
    }

当前的 class 与实现问题紧密相关,这使得单独测试它变得困难。

考虑重构 class

public abstract class HandlerBase {
    private readonly Lazy<MyActionsDataModel> model;
    private readonly IDbConnectionFactory connectionFactory;

    protected HandlerBase(IDbConnectionFactory connectionFactory) {
        this.connectionFactory = connectionFactory;
        model = new Lazy<MyActionsDataModel>(() => {
            MyActionsDataModel action = new MyActionsDataModel();
            using (DbConnection connection = this.connectionFactory.Create()) {
                connection.Open();
                using (DbTransaction transaction = connection.BeginTransaction(IsolationLevel.Serializable)) {
                    action.Id = connection.QuerySingle<int>("UpdateAction",
                        transaction: transaction,
                        commandType: CommandType.StoredProcedure,
                        param: action);
                    transaction.Commit();
                }
            }
            return action;
        });           
    }

    public string ErrorMessage;

    public void RecordFailure(AggregateException ex) {
        Console.WriteLine("A failure happened:");
        Console.WriteLine(JsonConvert.SerializeObject(ex));
        ErrorMessage = "Inner Exception\r\n" + ex.Message;
        MyActionsDataModel action = model.Value;
        action.ErrorOccurredOnUtc = DateTimeOffset.UtcNow;
        action.ErrorType = ex.GetType().FullName;
        action.ErrorMessage = ErrorMessage;
        saveAction(action);
    }

    private void saveAction(MyActionsDataModel action) {
        using (DbConnection connection = connectionFactory.Create()) {
            connection.Open();
            using (DbTransaction transaction = connection.BeginTransaction(IsolationLevel.Serializable)) {
                connection.Execute("UpdateAction", transaction: transaction,
                    commandType: CommandType.StoredProcedure, param: action);
                transaction.Commit();
            }
        }
    }
}

注意显式依赖的引入

public interface IDbConnectionFactory {
    DbConnection Create();
}

可以实现

// Connection Factory method
public DbConnection Create() {
    DbConnection connection = new SqlConnection(Constants.Connections.MyDatabase);
    return connection;
}

在测试时,可以模拟工厂以在执行被测对象时按预期运行。

这是我的新代码(基于@Nkosi 的建议)和测试部分:

代码:

    public abstract class HandlerBase {
        private readonly Lazy<MyActionsDataModel> model;
        private readonly IDbConnectionFactory connectionFactory;
    
        protected HandlerBase(IDbConnectionFactory connectionFactory) {
            this.connectionFactory = connectionFactory;
            model = new Lazy<MyActionsDataModel>(() => {
                MyActionsDataModel action = new MyActionsDataModel();
                using (DbConnection connection = this.connectionFactory.Create()) {
                    connection.Open();
                    using (DbTransaction transaction = connection.BeginTransaction(IsolationLevel.Serializable)) {
                        action.Id = connection.QuerySingle<int>("UpdateAction",
                            transaction: transaction,
                            commandType: CommandType.StoredProcedure,
                            param: action);
                        transaction.Commit();
                    }
                }
                return action;
            });           
        }
    
        public string ErrorMessage;
    
        public void RecordFailure(AggregateException ex) {
            Console.WriteLine("A failure happened:");
            Console.WriteLine(JsonConvert.SerializeObject(ex));
            ErrorMessage = "Inner Exception\r\n" + ex.Message;
            MyActionsDataModel action = model.Value;
            action.ErrorOccurredOnUtc = DateTimeOffset.UtcNow;
            action.ErrorType = ex.GetType().FullName;
            action.ErrorMessage = ErrorMessage;
            saveAction(action);
        }
    
        private void saveAction(MyActionsDataModel action) {
            using (DbConnection connection = new SqlConnection(Constants.Connections.MyDatabase)) {
                connection.Open();
                using (DbTransaction transaction = connection.BeginTransaction(IsolationLevel.Serializable)) {
                    connection.Execute("UpdateAction", transaction: transaction,
                        commandType: CommandType.StoredProcedure, param: action);
                    transaction.Commit();
                }
            }
        }
    }
    
    public interface IDbConnectionFactory {
        DbConnection Create();
    }
    
    // Connection Factory method
    public DbConnection Create() {
        DbConnection connection = new SqlConnection(Constants.Connections.MyDatabase);
        return connection;
    }


public class MyHandlerType : HandlerBase
    {
        private readonly IDbConnectionFactory _connectionFactory;

        public MyHandlerType(IDbConnectionFactory connectionFactory) : base(connectionFactory)
        {
            _connectionFactory = connectionFactory;
        }
}

测试:

public class HandlerTests
    {
        protected MyHandlerType _subjectUnderTest;
        protected HandlerBase.IDbConnectionFactory _fakeConnectionFactory;

        [SetUp]
        public void Setup()
        {
            _fakeConnectionFactory = A.Fake<HandlerBase.IDbConnectionFactory>();
            A.CallTo(() => _fakeConnectionFactory.Create()).Returns<DbConnection>(new SqlConnection(Constants.Connections.MyDatabase));

            _subjectUnderTest = new MyHandlerType(_fakeConnectionFactory);
        }

        [TestCase]
        public void Test_RecordFailure()
        {
            var innerMessage = "Throw AppException for UnitTest.";
            var parentMessage = "Throw AggregationException for UnitTest.";
            var innerException = new ApplicationException(innerMessage);
            var parentException = new AggregateException(parentMessage, innerException);
            _subjectUnderTest.RecordFailure(parentException);
            var includeInnerMessage = testHandler.errorMessage.Contains(innerMessage);
            var includeParentMessage = testHandler.errorMessage.Contains(parentMessage);

            Assert.IsTrue(includeInnerMessage);
            Assert.IsTrue(includeParentMessage);
        }
    }