我如何重构这个工厂类型方法和数据库调用以使其可测试?
我正在尝试学习如何进行单元测试和模拟。 我了解 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 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(6)
您将 GetAgentFromDatabase() 移动到单独的类中是正确的。 下面是我重新定义 AgentRepository 的方法:
我定义了 IAgentDataProvider 接口,如下所示:
因此,AgentRepository 是被测试的类。 我们将模拟 IAgentDataProvider 并注入依赖项。 (我是用 Moq 完成的,但您可以使用不同的隔离框架轻松重做)。
(我省略了类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:
where I defined the IAgentDataProvider interface as follows:
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).
(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
这里的问题是决定什么是 SUT,什么是测试。 在您的示例中,您尝试测试
Select()
方法,因此希望将其与数据库隔离。 您有多种选择,虚拟化
GetAgentFromDatabase()
,以便您可以为派生类提供返回正确值的代码,在本例中创建一个提供IDataReaderFunctionaity
的对象> 无需与数据库对话As Gishu 建议不要使用 IsA 关系(继承),而是使用 HasA(对象组合),您再次拥有一个类来处理创建模拟
IDataReader
,但这次没有继承。然而,这两者都会产生大量代码,这些代码只是定义了我们在查询时返回的一组结果。 诚然,我们可以将此代码保留在测试代码中,而不是我们的主代码中,但这是一种努力。 您真正要做的就是为特定查询定义一个结果集,并且您知道什么真正擅长做到这一点...数据库
我不久前使用过 LinqToSQL 并发现
DataContext
对象有一些非常有用的方法,包括DeleteDatabase
和CreateDatabase
。考虑一下。 使用数据库进行单元测试的问题是数据会发生变化。 删除您的数据库并使用您的测试来发展可在未来测试中使用的数据。
有两点需要注意
确保您的测试以正确的顺序运行。 其 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,Virtualise the
GetAgentFromDatabase()
so you can provide a derived class with code to return the correct values, in this case creating an object that providesIDataReaderFunctionaity
without talking to the DB i.e.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
I used LinqToSQL a while back and discovered that the
DataContext
objects have some very useful methods, includingDeleteDatabase
andCreateDatabase
.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.
我将开始提出一些想法并一路更新:
I'll start putting up some ideas and will update along the way:
在我看来,您通常应该只担心使您的公共属性/方法可测试。 即,只要 Select(int agentId) 有效,您通常不关心它是如何通过 GetAgentFromDatabase(int agentId) 实现的。
你所拥有的似乎是合理的,因为我想它可以用类似下面的东西来测试(假设你的类称为 AgentRepository)
至于建议的增强功能。 我建议允许通过公共或内部访问来更改 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)
As for suggested enhancements. I would recommend allowing the AgentRepository's connection string to be changed, either by public or internal access.
假设您正在尝试测试类 [NoName] 的公共 Select 方法。
private Hashtable GetAgentFromDatabase(int agentId)
。
Assuming that you're trying to test the public Select method of class [NoName]..
private Hashtable GetAgentFromDatabase(int agentId)
.
我认为 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.