我可以模拟SqlConnection.BeginTransaction c#吗?

时间:2017-04-11 16:02:53

标签: c# unit-testing

我必须处理一些我无法改变的遗留代码。我必须编写实现名为" ITask"的接口的类。有一个名为" RunTask"接受称为Schedule例如

的具体类型
 public void RunTask(Schedule thisSchedule)
 {
     //I have to do stuff with thisSchedule, no I can't fix that name...
 }

虽然遗留代码不使用单元测试,但我非常希望将它用于我的工作,但问题是" thisSchedule"。我已经制作了一个伪造的Schedule版本,它源自它并试图控制其中所有方法的运作方式(这可能是一个傻瓜的差事)。到目前为止,我已经成功地利用了惊人数量的虚拟方法或使用反射,但我已经击中了我的第一个节目塞子

我无法连接到真正的数据库,我有一个经常调用的方法;

public void BeginTransaction()
{
    MyTransaction = MyConnection.BeginTransaction(IsolationLevel.RepeatableRead);
}

internal SqlConnection MyConnection = new System.Data.SqlClient.SqlConnection();

这会引发异常,因为连接已关闭。理想情况下,我希望能够设置一个标志,说明该方法已被调用,然后只是吞下异常,但我很乐意忽略该异常。

任何事情,无论多么令人讨厌,这将让我通过这个方法调用将是一个可接受的答案。这是或失败。

我不允许在visual studio enterprise中使用像typeMock或shims这样的付费服务。

修改

using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Data;
using System.Data.SqlClient;

namespace PredictionServicesTests
{
    [TestClass]
    public class UnitTest1
    {
        [TestMethod]
        public void BeginTransaction_WhenCalled_SetsTransactionStartedToTrue()
        {
            Schedule schedule = new FakeSchedule();

            schedule.BeginTransaction(); //It would be enough to simply get past this line to the Assert

            Assert.IsTrue(((FakeSchedule)schedule).TransactionStarted); //This would be nice, but isn't vital
        }

        class Schedule
        {
            private SqlTransaction MyTransaction;
            internal SqlConnection MyConnection = new System.Data.SqlClient.SqlConnection();
            public void BeginTransaction()
            {
                MyTransaction = MyConnection.BeginTransaction(IsolationLevel.RepeatableRead);
            }
        }

        class FakeSchedule : Schedule
        {
            public bool TransactionStarted { get; set; }
        }
    }
}

请记住我不能改变Schedule类,只要事情可以那么简单!

ITask界面;

    interface ITask
    {
        void RunTask(Schedule thisSchedule);
    }

3 个答案:

答案 0 :(得分:1)

你是" newing"在类中的SQL连接使得很难反转控件并使其更加单元测试友好。

另一个选择是连接到虚拟数据库并使用该连接。虚拟数据库就足以允许将测试作为集成测试来运行。

应将遗留代码视为您无法控制的第三方代码。而且你不应该浪费时间测试你无法控制的第三部分代码。通常的做法是将第三方代码封装在您控制并使用它的抽象之后。遗留代码演示technical debt由于设计不佳而正在兑现当前遇到的困难。

鉴于目前的限制,其他任何事情都无法完成。

答案 1 :(得分:1)

在单元测试方面,任何外部依赖都需要隔离。不同的框架有不同的隔离外部依赖关系的方式,以及它们可以隔离的限制。

许多框架允许您创建接口的模拟,以便您可以为受控状态的成员提供实现。很多这些框架也适用于抽象类的抽象成员。但是,很少有框架支持在具体类(非抽象成员)上提供实现的能力。我知道的唯一两个可用的是:

这两个框架都使用.NET Profiler API来拦截成员调用,以使用您提供的代码替换它们。我对TypeMock Isolator不太熟悉,但我对Microsoft Fakes非常熟悉。 Microsoft Fakes框架支持生成程序集(即System.Fakes.dll),该程序集包含允许您在成员上提供自己的实现的类。它支持创建针对接口和抽象类的Stub(相当于“mocks”),以及针对具体和静态类的Shims。

如果您选择使用Microsoft Fakes,则首先需要针对System {Data.dll生成假装配,这是SqlConnection所在的位置。这将生成ShimSqlConnection类,此类将包含允许您提供SqlConnection类'成员的替代实现的成员。以下是如何做到这一点的示例:

[TestMethod]
public void SampleTest()
{
    using (ShimsContext.Create())
    {
        // Arrange
        SqlConnection.AllInstances.BeginTransactionIsolationLevel =
            (instance, iso) => { ... };

        // Act

        // Assert
    }
}

当然,无论是模拟,填充,存根还是其他......你应该提供模拟你想要的行为的替代实现。由于您要替换SqlConnection.BeginTransaction(IsolationLevel)成员,您仍必须遵守合同和预期行为;您不应该返回值或抛出实际实现不会执行的异常。在我的公司,我们利用Microsoft Fakes来模拟以下场景:

  • 如果我们从中读取Stream时会发生什么?
  • 如果HttpWebRequest的用户/通行证有误,会怎样?
  • 如果我们希望我们的数据库返回两行(即控制SqlDataReader.Read()成员)会怎样?

在所有这些场景中,我们的目标是隔离这些成员的实现行为,并将其替换为在受控实验中可能实际发生的实现。

更新

我刚刚注意到原始海报上说“我不允许在视觉工作室企业中使用像typeMock或垫片这样的付费服务。”。如果这是一个限制,那么你将不得不找到另一个我不知道利用.NET Profiler API或自己使用框架的工具。 Profiling (Unmanaged API Reference)

答案 2 :(得分:0)

我最接近的是创建一个名为IFake的接口,我将其应用于FakeSchedule;

public interface IFake
{
    Action GetFakeAction(Action action);
}

public class FakeSchedule : Schedule, IFake
{
    public bool TransactionStarted { get; set; }

    public Action GetFakeAction(Action action)
    {
        return () => TransactionStarted = true;
    }
}

然后我创建了以下扩展方法;

    public static void Testable<T>(this T obj, Action action)
    {
        if(obj is IFake)
        {
            action = ((IFake)obj).GetFakeAction(action);
        }
        action();
    }

这允许我像这样调用BeginTransaction方法;

    [TestMethod]
    public void BeginTransaction_WhenCalled_SetsTransactionStartedToTrue()
    {
        Schedule schedule = new FakeSchedule();
        var connection = new SqlConnection();

        schedule.Testable(schedule.BeginTransaction);

        Assert.IsTrue(((FakeSchedule)schedule).TransactionStarted); 
    }

显然这个解决方案远非理想,但它可行,如果我正在运行单元测试,那么开始交易的调用就会转移到真正的实现之外。

我实现的版本需要一些工作(我的GetFakeAction实现太愚蠢)但它是我认为最接近的,我能看到的唯一其他解决方案是使用{{ 3}}或跟随Prig并连接到虚拟数据库。