12

我正在尝试学习如何进行单元测试和模拟。我了解 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 以外的内容?任何建议或指向外部链接的指针将不胜感激。

4

6 回答 6

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对象并将它们转换为正确格式的Agent对象。您可以扩展上述测试夹具以测试更多有趣的案例。

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

于 2009-08-05T17:48:07.380 回答
1

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

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

    class MyDerivedExample : YourUnnamedClass
    {
        protected override IDataReader GetAgentFromDatabase()
        {
            return new MyDataReader({"AgentId", "1"}, {"FirstName", "Fred"},
              ...);
        }
    }
    
  2. 正如Gishu 建议的那样,不要使用 IsA 关系(继承),而是使用 HasA(对象组合),在这里您再次拥有一个处理创建 mock 的类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();
    }
    

考虑一下。使用数据库进行单元测试的问题在于数据会发生变化。删除您的数据库并使用您的测试来改进您的数据,以便在未来的测试中使用。

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

于 2009-08-05T15:15:01.517 回答
0

我将开始提出一些想法并在此过程中进行更新:

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

IMO 您通常应该只担心使您的公共属性/方法可测试。即只要Select(int agentId)正常工作,您通常不会关心它是如何通过GetAgentFromDatabase(int agentId) 完成的。

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

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

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

于 2009-08-05T14:21:43.117 回答
0

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

  1. 将 GetAgentFromDatabase() 方法移动到 IDB_Access 接口中。让 NoName 有一个可以设置为 ctor 参数或属性的接口成员。所以现在你有了一个接缝,你可以在不修改方法中的代码的情况下改变行为。
  2. 我会更改上述方法的返回类型以返回更通用的内容 - 您似乎像使用哈希表一样使用它。让 IDB_Access 的生产实现使用 IDataReader 在内部创建哈希表。它还减少了对技术的依赖;我可以使用 MySql 或一些非 MS/.net 环境来实现这个接口。 private Hashtable GetAgentFromDatabase(int agentId)
  3. 接下来对于您的单元测试,您可以使用存根(或使用更高级的东西,例如模拟框架)

.

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...
于 2009-08-05T14:22:19.137 回答
0

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

于 2009-08-05T14:34:00.417 回答