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

发布于 2024-07-29 17:24:24 字数 1575 浏览 9 评论 0原文

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

I'm trying to learn how to do Unit Testing and Mocking. I understand some of the principles of TDD and basic testing. However, I'm looking at refactoring the below code that was written without tests and am trying to understand how it needs to change in order to make it testable.

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);
}

}

These two methods are in a single class. The database-related code in the GetAgentFromDatabase is related to Enterprise Libraries.

How would I be able to go about making this testable? Should I abstract out the GetAgentFromDatabase method into a different class? Should GetAgentFromDatabase return something other than an IDataReader? Any suggestions or pointers to external links would be greatly appreciated.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(6

留蓝 2024-08-05 17:24:24

您将 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< /strong> 对象。 您可以扩展上面的测试夹具来测试更多有趣的案例。

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

You're correct about moving GetAgentFromDatabase() into a separate class. Here's how I redefined 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;
    }
}

where I defined the IAgentDataProvider interface as follows:

public interface IAgentDataProvider {
    IDataReader GetAgent( int agentId );
}

So, AgentRepository is the class under test. We'll mock IAgentDataProvider and inject the dependency. (I did it with Moq, but you can easily redo it with a different isolation framework).

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

(I left out the implementation of class FakeAgentDataReader, which implements IDataReader and is trivial -- you only need to implement Read() and Dispose() to make the tests work.)

The purpose of AgentRepository here is to take IDataReader objects and turn them into properly formed Agent objects. You can expand the above test fixture to test more interesting cases.

After unit-testing AgentRepository in isolation from the actual database, you will need unit tests for a concrete implementation of IAgentDataProvider, but that's a topic for a separate question. HTH

枯寂 2024-08-05 17:24:24

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

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

    class MyDerivedExample :YourUnnamedClass 
      { 
          受保护覆盖​​ IDataReader GetAgentFromDatabase() 
          { 
              返回新的 MyDataReader({"AgentId", "1"}, {"FirstName", "Fred"}, 
                ...); 
          } 
      } 
      
  2. As Gishu 建议不要使用 IsA 关系(继承),而是使用 HasA(对象组合),您再次拥有一个类来处理创建模拟 IDataReader,但这次没有继承。

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

  3. 我不久前使用过 LinqToSQL 并发现 DataContext 对象有一些非常有用的方法,包括 DeleteDatabaseCreateDatabase

    public const string UnitTestConnection = "数据源=.;初始目录=MyAppUnitTest;集成安全性=True"; 
    
    
      [夹具设置()] 
      公共无效设置() 
      { 
        OARsDataContext 上下文 = new MyAppDataContext(UnitTestConnection); 
    
        if (上下文.DatabaseExists()) 
        { 
          Console.WriteLine("正在删除现有的测试数据库"); 
          上下文.DeleteDatabase(); 
        } 
        Console.WriteLine("创建新的测试数据库"); 
        上下文.CreateDatabase(); 
    
        上下文.SubmitChanges(); 
      } 
      

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

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

The problem here is deciding what is SUT and what is Test. With your example you are trying to Test the Select() method and therefore want to isolate that from the database. You have several choices,

  1. Virtualise the GetAgentFromDatabase() so you can provide a derived class with code to return the correct values, in this case creating an object that provides IDataReaderFunctionaity without talking to the DB i.e.

    class MyDerivedExample : YourUnnamedClass
    {
        protected override IDataReader GetAgentFromDatabase()
        {
            return new MyDataReader({"AgentId", "1"}, {"FirstName", "Fred"},
              ...);
        }
    }
    
  2. As Gishu suggested instead of using IsA relationships (inheritance) use HasA (object composition) where you once again have a class that handles creating a mock IDataReader, but this time without inheriting.

    However both of these result in lots of code that simply defines a set of results that we be returned when queried. Admittedly we can keep this code in the Test code, instead of our main code, but its an effort. All you are really doing is define a result set for particular queries, and you know what’s really good at doing that... A database

  3. I used LinqToSQL a while back and discovered that the DataContext objects have some very useful methods, including DeleteDatabase and 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();
    }
    

Consider it for a while. The problem with using a database for unit tests is that the data will change. Delete your database and use your tests to evolve your data that can be used in future tests.

There are two things to be careful of
Make sure your tests run in the correct order. The MbUnit syntax for this is [DependsOn("NameOfPreviousTest")].
Make sure only one set of tests is running against a particular database.

殤城〤 2024-08-05 17:24:24

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

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

I'll start putting up some ideas and will update along the way:

  • SqlDatabase sqlDb = new SqlDatabase("MyConnectionString"); - You should avoid new operators mixed up with logic. You should construct xor have logic operations; avoid them happening at the same time. Use Dependency injection to pass this database as a parameter, so you can mock it. I mean this if you want to unit test it (not going to the database, which should be done in some case later)
  • IDataReader agentInformation = GetAgentFromDatabase(agentId) - Maybe you could separate the Reader retrieval to some other class, so you can mock this class while testing the factory code.
半岛未凉 2024-08-05 17:24:24

在我看来,您通常应该只担心使您的公共属性/方法可测试。 即,只要 Select(int agentId) 有效,您通常不关心它是如何通过 GetAgentFromDatabase(int agentId) 实现的。

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

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

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

IMO you should normally only worry about making your public properties/methods testable. I.e. as long as Select(int agentId) works you normally don't care how it does it via GetAgentFromDatabase(int agentId).

What you have seems reasonable, as I imagine it can be tested with something like the following (assuming your class is called AgentRepository)

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

As for suggested enhancements. I would recommend allowing the AgentRepository's connection string to be changed, either by public or internal access.

迷爱 2024-08-05 17:24:24

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

  1. 将 GetAgentFromDatabase() 方法移动到 IDB_Access 接口中。 让 NoName 有一个可以设置为构造函数参数或属性的接口成员。 所以现在你有了一个接缝,你可以改变行为而无需修改方法中的代码。
  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...

Assuming that you're trying to test the public Select method of class [NoName]..

  1. Move the GetAgentFromDatabase() method into an interface say IDB_Access. Let NoName have an interface member that can be set as a ctor parameter or a property. So now you have a seam, you can change the behavior without modifying the code in the method.
  2. I'd change the return type of the above method to return something more general - you seem to be using it like a hashtable. Let the production implementation of IDB_Access use the IDataReader to create the hashtable internally. It also makes it less technology dependent ; I can implement this interface using MySql or some non-MS/.net environment.
    private Hashtable GetAgentFromDatabase(int agentId)
  3. Next for your unit test, you could work with a stub (or use something more advanced like a mock framework)

.

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...
兲鉂ぱ嘚淚 2024-08-05 17:24:24

我认为 GetAgentFromDatabase() 方法不能通过额外的测试进行测试,因为它的代码完全被 Select() 方法的测试覆盖。 代码没有可以沿着的分支,因此在这里创建额外的测试没有意义。
如果从多个方法调用 GetAgentFromDatabase() 方法,您应该单独测试它。

As for my opinion the GetAgentFromDatabase() method must not be testet by an extra test, because its code is fully covered by the test of the Select() method. There are no branches the code could walk along, so no point in creating an extra test here.
If the GetAgentFromDatabase() method is called from multiple methods you should test it on its own though.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文