我怎么能重构这个工厂类型的方法和数据库调用是可测试的?

时间:2009-08-05 14:07:06

标签: c# unit-testing mocking isolation-frameworks

我正在努力学习如何进行单元测试和模拟。我理解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中与数据库相关的代码与Enterprise Libraries相关。

我怎样才能使这个可测试?我应该将GetAgentFromDatabase方法抽象为另一个类吗? GetAgentFromDatabase应该返回除IDataReader之外的其他内容吗?任何有关外部链接的建议或指示都将不胜感激。

6 个答案:

答案 0 :(得分:9)

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]
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() { ... };
    }
}

(我省略了 FakeAgentDataReader 类的实现,它实现了 IDataReader 并且很简单 - 你只需要实现 Read() Dispose()使测试工作。)

此处 AgentRepository 的目的是将 IDataReader 对象转换为正确形成的代理对象。您可以扩展上述测试夹具以测试更有趣的案例。

在将 AgentRepository 单元测试与实际数据库隔离后,您需要对 IAgentDataProvider 的具体实现进行单元测试,但这是一个单独问题的主题。 HTH

答案 1 :(得分:1)

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

  1. 虚拟化GetAgentFromDatabase(),以便您可以为派生类提供返回正确值的代码,在这种情况下创建一个提供IDataReaderFunctionaity的对象,而无需与DB通信,即

    class MyDerivedExample : YourUnnamedClass
    {
        protected override IDataReader GetAgentFromDatabase()
        {
            return new MyDataReader({"AgentId", "1"}, {"FirstName", "Fred"},
              ...);
        }
    }
    
  2. 作为Gishu suggested而不是使用IsA关系(继承)使用HasA(对象组合),您再次拥有一个处理创建模拟IDataReader的类,但这次没有继承。

    然而,这两个都会产生大量代码,这些代码只是定义了一组我们在查询时返回的结果。不可否认,我们可以将此代码保留在测试代码中,而不是我们的主代码中,但这是一种努力。你所做的只是为特定的查询定义一个结果集,你知道这样做的真正好处...数据库

  3. 我前一段时间使用过LinqToSQL,发现DataContext个对象有一些非常有用的方法,包括DeleteDatabaseCreateDatabase

    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();
    }
    
  4. 考虑一下。使用数据库进行单元测试的问题是数据会发生变化。删除数据库并使用测试来改进可用于将来测试的数据。

    有两件事需要注意 确保您的测试以正确的顺序运行。 MbUnit的语法是[DependsOn("NameOfPreviousTest")]。 确保只针对特定数据库运行一组测试。

答案 2 :(得分:0)

我会开始提出一些想法,并会一路更新:

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

答案 3 :(得分:0)

IMO您通常应该只担心让您的公共属性/方法可测试。即只要选择(int agentId),您通常无需通过 GetAgentFromDatabase(int agentId)来处理它。

你所拥有的似乎是合理的,因为我想它可以用类似下面的东西进行测试(假设你的类被称为AgentRepository)

AgentRepository aRepo = new AgentRepository();
int agentId = 1;
Agent a = aRepo.Select(agentId);
//Check a here

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

答案 4 :(得分:0)

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

  1. 将GetAgentFromDatabase()方法移动到IDB_Access接口中。设NoName具有可以设置为ctor参数或属性的接口成员。所以现在你有一个接缝,你可以改变行为,而无需修改方法中的代码。
  2. 我将更改上述方法的返回类型以返回更通用的内容 - 您似乎像哈希表一样使用它。让IDB_Access的生产实现使用IDataReader在内部创建哈希表。它也减少了技术依赖性;我可以使用MySql或某些非MS / .net环境实现此接口。 private Hashtable GetAgentFromDatabase(int agentId)
  3. 接下来进行单元测试,您可以使用存根(或使用更高级的东西,如模拟框架)
  4. public MockDB_Access : IDB_Access
    {
      public const string MY_NAME = "SomeName;
      public Hashtable GetAgentFromDatabase(int agentId)
      {  var hash = new Hashtable();
         hash["FirstName"] = MY_NAME; // fill other properties as well
         return hash;
      }
    }
    
    // in the unit test
    var testSubject = new NoName( new MockDB_Access() );
    var agent = testSubject.Select(1);
    Assert.AreEqual(MockDB_Access.MY_NAME, agent.FirstName); // and so on...
    

答案 5 :(得分:0)

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