C# 我如何重构这个工厂类型的方法和数据库调用以使其可测试?

C# 我如何重构这个工厂类型的方法和数据库调用以使其可测试?,c#,unit-testing,mocking,isolation-frameworks,C#,Unit Testing,Mocking,Isolation Frameworks,我正在努力学习如何进行单元测试和模拟。我了解TDD和基本测试的一些原理。然而,我正在考虑重构下面的代码,这些代码是在没有测试的情况下编写的,我试图理解它需要如何更改才能使其可测试 public class AgentRepository { public Agent Select(int agentId) { Agent tmp = null; using (IDataReader agentInformation = GetAgentFromDatabase(agentId)

我正在努力学习如何进行单元测试和模拟。我了解TDD和基本测试的一些原理。然而,我正在考虑重构下面的代码,这些代码是在没有测试的情况下编写的,我试图理解它需要如何更改才能使其可测试

public class AgentRepository
{

public Agent Select(int agentId)
{
    Agent tmp = null;
    using (IDataReader agentInformation = GetAgentFromDatabase(agentId))
    {
        if (agentInformation.Read())
        {
            tmp = new Agent();
            tmp.AgentId = int.Parse(agentInformation["AgentId"].ToString());
            tmp.FirstName = agentInformation["FirstName"].ToString();
            tmp.LastName = agentInformation["LastName"].ToString();
            tmp.Address1 = agentInformation["Address1"].ToString();
            tmp.Address2 = agentInformation["Address2"].ToString();
            tmp.City = agentInformation["City"].ToString();
            tmp.State = agentInformation["State"].ToString();
            tmp.PostalCode = agentInformation["PostalCode"].ToString();
            tmp.PhoneNumber = agentInformation["PhoneNumber"].ToString();
        }
    }

    return tmp;
}

private IDataReader GetAgentFromDatabase(int agentId)
{
    SqlCommand cmd = new SqlCommand("SelectAgentById");
    cmd.CommandType = CommandType.StoredProcedure;

    SqlDatabase sqlDb = new SqlDatabase("MyConnectionString");
    sqlDb.AddInParameter(cmd, "AgentId", DbType.Int32, agentId);
    return sqlDb.ExecuteReader(cmd);
}

}
这两个方法在一个类中。GetAgentFromDatabase中与数据库相关的代码与企业库相关


我怎样才能让这件事成为可测试的呢?我是否应该将GetAgentFromDatabase方法抽象为另一个类?GetAgentFromDatabase是否应该返回IDataReader以外的内容?如果您有任何关于外部链接的建议或建议,我们将不胜感激。

我将开始提出一些想法,并将在此过程中不断更新:

  • SqlDatabase sqlDb=newsqldatabase(“MyConnectionString”);-您应该避免新运算符与逻辑混淆。您应该构造具有逻辑运算的xor;避免它们同时发生。使用依赖项注入将此数据库作为参数传递,以便可以模拟它。我的意思是,如果您想对它进行单元测试(而不是去数据库,在以后的某些情况下应该这样做)
  • IDataReader agentInformation=GetAgentFromDatabase(agentId)-也许您可以将读取器检索分离到其他类,以便在测试工厂代码时模拟此类

在我看来,您通常只需要担心使您的公共属性/方法可测试。也就是说,只要Select(int-agentId)有效,您通常不关心它如何通过GetAgentFromDatabase(int-agentId)进行操作

您所拥有的似乎是合理的,正如我所想象的,它可以通过以下内容进行测试(假设您的类被称为AgentRepository)


至于建议的增强。我建议允许通过公共访问或内部访问更改AgentRepository的连接字符串

假设您正在尝试测试类[NoName]的公共选择方法

  • 将GetAgentFromDatabase()方法移动到接口(如IDB_Access)中。让NoName具有一个接口成员,该成员可以设置为一个参数或属性。现在您有了一个seam,您可以在不修改方法中的代码的情况下更改行为
  • 我将更改上述方法的返回类型,以返回更一般的内容—您似乎将其用作哈希表。让IDB_Access的生产实现使用IDataReader在内部创建哈希表。这也使得它更少地依赖技术;我可以使用MySql或一些非MS/.net环境实现这个接口。
    私有哈希表GetAgentFromDatabase(int-agentId)
  • 接下来,对于单元测试,您可以使用存根(或者使用更高级的东西,比如模拟框架)

  • 至于我的意见,GetAgentFromDatabase()方法不能通过额外的测试来测试,因为它的代码完全被Select()方法的测试所覆盖。没有代码可以执行的分支,因此在这里创建额外的测试毫无意义。
    如果从多个方法调用GetAgentFromDatabase()方法,您应该自己测试它。

    这里的问题是确定什么是SUT,什么是测试。在您的示例中,您试图测试
    Select()
    方法,因此希望将其与数据库隔离。你有几个选择

  • 虚拟化
    GetAgentFromDatabase()
    ,这样您就可以为派生类提供返回正确值的代码,在这种情况下,创建一个提供
    IDataReaderFunction
    的对象,而无需与DB对话,即

    class MyDerivedExample : YourUnnamedClass
    {
        protected override IDataReader GetAgentFromDatabase()
        {
            return new MyDataReader({"AgentId", "1"}, {"FirstName", "Fred"},
              ...);
        }
    }
    
  • 与使用IsA关系(继承)不同,请使用HasA(对象组合),在HasA中,您再次拥有一个处理创建模拟
    IDataReader
    的类,但这次没有继承

    然而,这两种方法都会产生大量代码,这些代码只定义了一组查询时返回的结果。诚然,我们可以将此代码保留在测试代码中,而不是主代码中,但这是一项努力。您真正要做的就是为特定的查询定义一个结果集,您知道什么是真正擅长的。。。数据库

  • 不久前我使用了LinqToSQL,发现
    DataContext
    对象有一些非常有用的方法,包括
    DeleteDatabase
    CreateDatabase

    public const string UnitTestConnection = "Data Source=.;Initial Catalog=MyAppUnitTest;Integrated Security=True";
    
    
    [FixtureSetUp()]
    public void Setup()
    {
      OARsDataContext context = new MyAppDataContext(UnitTestConnection);
    
      if (context.DatabaseExists())
      {
        Console.WriteLine("Removing exisitng test database");
        context.DeleteDatabase();
      }
      Console.WriteLine("Creating new test database");
      context.CreateDatabase();
    
      context.SubmitChanges();
    }
    
  • 考虑一下。使用数据库进行单元测试的问题在于数据会发生变化。删除您的数据库并使用您的测试来改进您的数据,以便在将来的测试中使用

    有两件事需要注意 确保测试以正确的顺序运行。用于此的MbUnit语法是
    [DependsOn(“nameofPreiousTest”)]

    确保仅针对特定数据库运行一组测试。

    GetAgentFromDatabase()移动到单独的类中是正确的。下面是我如何重新定义AgentRepository的:

    public class AgentRepository {
        private IAgentDataProvider m_provider;
    
        public AgentRepository( IAgentDataProvider provider ) {
            m_provider = provider;
        }
    
        public Agent GetAgent( int agentId ) {
            Agent agent = null;
            using( IDataReader agentDataReader = m_provider.GetAgent( agentId ) ) {
                if( agentDataReader.Read() ) {
                    agent = new Agent();
                    // set agent properties later
                }
            }
            return agent;
        }
    }
    
    其中,我对IAgentDataProvider接口的定义如下:

    public interface IAgentDataProvider {
        IDataReader GetAgent( int agentId );
    }
    
    因此,AgentRepository是被测试的类。我们将模拟IAgentDataProvider并注入依赖项。(我是用Moq做的,但是你可以用不同的隔离框架轻松地重做)

    [TestFixture]
    公共类代理位置测试{
    私人代理存款回购;
    私人模拟m_mockProvider;
    [设置]
    公共无效案例设置(){
    m_mockProvider=新建Mock();
    m_repo=新代理存储(m_mockProvider.Object);
    }
    [撕裂]
    公共无效案例拆卸(){
    m_mockProvider.Verify();
    }
    [测试]
    public void AgentFactory\u OnEmptyDataReader\u ShouldReturnNull(){
    m_mockProvider
    .Setup(p=>p.GetAgent(It.IsA
    
    public class AgentRepository {
        private IAgentDataProvider m_provider;
    
        public AgentRepository( IAgentDataProvider provider ) {
            m_provider = provider;
        }
    
        public Agent GetAgent( int agentId ) {
            Agent agent = null;
            using( IDataReader agentDataReader = m_provider.GetAgent( agentId ) ) {
                if( agentDataReader.Read() ) {
                    agent = new Agent();
                    // set agent properties later
                }
            }
            return agent;
        }
    }
    
    public interface IAgentDataProvider {
        IDataReader GetAgent( int agentId );
    }
    
    [TestFixture]
    public class AgentRepositoryTest {
        private AgentRepository m_repo;
        private Mock<IAgentDataProvider> m_mockProvider;
    
        [SetUp]
        public void CaseSetup() {
            m_mockProvider = new Mock<IAgentDataProvider>();
            m_repo = new AgentRepository( m_mockProvider.Object );
        }
    
        [TearDown]
        public void CaseTeardown() {
            m_mockProvider.Verify();
        }
    
        [Test]
        public void AgentFactory_OnEmptyDataReader_ShouldReturnNull() {
            m_mockProvider
                .Setup( p => p.GetAgent( It.IsAny<int>() ) )
                .Returns<int>( id => GetEmptyAgentDataReader() );
            Agent agent = m_repo.GetAgent( 1 );
            Assert.IsNull( agent );
        }
    
        [Test]
        public void AgentFactory_OnNonemptyDataReader_ShouldReturnAgent_WithFieldsPopulated() {
            m_mockProvider
                .Setup( p => p.GetAgent( It.IsAny<int>() ) )
                .Returns<int>( id => GetSampleNonEmptyAgentDataReader() );
            Agent agent = m_repo.GetAgent( 1 );
            Assert.IsNotNull( agent );
                        // verify more agent properties later
        }
    
        private IDataReader GetEmptyAgentDataReader() {
            return new FakeAgentDataReader() { ... };
        }
    
        private IDataReader GetSampleNonEmptyAgentDataReader() {
            return new FakeAgentDataReader() { ... };
        }
    }