我如何重构这个工厂类型方法和数据库调用以使其可测试?
-
22-07-2019 - |
题
我正在尝试学习如何进行单元测试和模拟。我了解 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 以外的其他内容?任何建议或指向外部链接的指示将不胜感激。
解决方案
您正要移动正确的 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;
}
}
其中I所定义的 IAgentDataProvider 强>接口如下:
public interface IAgentDataProvider {
IDataReader GetAgent( int agentId );
}
所以,的 AgentRepository 强>是测试下的类。我们会嘲笑的 IAgentDataProvider 并注入的依赖。 (I与做了它的起订量,但可以很容易地用不同的隔离框架重做它)。
[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 并是微不足道的 - 你只需要实现的阅读()和 Dispose()方法以使测试工作。)
<强> AgentRepository 强>这里是采取的的IDataReader 强>对象的目的,把它们变成适当地形成的代理强>对象。可以扩大上述测试夹具来测试更有趣的情况。
在单元测试的 AgentRepository 从实际的数据库隔离,则需要单元测试的具体实施 IAgentDataProvider ,不过这是另外一个问题的话题。 HTH
其他提示
这里的问题是决定什么是 SUT,什么是测试。通过您的示例,您正在尝试测试 Select()
方法,因此希望将其与数据库隔离。你有多种选择,
虚拟化
GetAgentFromDatabase()
因此您可以为派生类提供返回正确值的代码,在本例中创建一个提供IDataReaderFunctionaity
无需与数据库交谈,即class MyDerivedExample : YourUnnamedClass { protected override IDataReader GetAgentFromDatabase() { return new MyDataReader({"AgentId", "1"}, {"FirstName", "Fred"}, ...); } }
作为 纪州建议 使用 HasA(对象组合)而不是使用 IsA 关系(继承),您再次拥有一个处理创建模拟的类
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("NameOfPreviousTest")]
。确保仅针对特定数据库运行一组测试。
我将开始提出一些想法并不断更新:
- SqlDatabase sqlDb = new SqlDatabase("MyConnectionString");- 你应该避免 新的 运算符与逻辑混合在一起。您应该构造异或具有逻辑运算;避免它们同时发生。使用依赖注入将此数据库作为参数传递,以便您可以模拟它。我的意思是如果你想对其进行单元测试(不进入数据库,这应该在稍后的某些情况下完成)
- IDataReader agentInformation = GetAgentFromDatabase(agentId) - 也许您可以将 Reader 检索分离到其他类,这样您就可以在测试工厂代码时模拟此类。
IMO通常应该只担心使你的公共属性/方法测试。即只要选择(INT的agentId)工作你通常不关心它是如何通过做它的 GetAgentFromDatabase(INT的agentId)
您有什么似乎是合理的,因为我想它可以通过类似如下(假设你的类被称为AgentRepository)
测试AgentRepository aRepo = new AgentRepository();
int agentId = 1;
Agent a = aRepo.Select(agentId);
//Check a here
作为用于建议的增强。我会建议让AgentRepository的连接字符串被改变,或者通过公开或内部的访问。
假设您正在尝试测试类 [NoName] 的公共 Select 方法..
- 将 GetAgentFromDatabase() 方法移至 IDB_Access 接口中。让 NoName 有一个可以设置为构造函数参数或属性的接口成员。所以现在你有了一个接缝,你可以改变行为而无需修改方法中的代码。
- 我会更改上述方法的返回类型以返回更通用的内容 - 您似乎像使用哈希表一样使用它。让 IDB_Access 的生产实现使用 IDataReader 在内部创建哈希表。它还减少了对技术的依赖;我可以使用 MySql 或一些非 MS/.net 环境来实现此接口。
private Hashtable GetAgentFromDatabase(int agentId)
- 接下来,对于单元测试,您可以使用存根(或使用更高级的东西,例如模拟框架)
.
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...
至于我认为该GetAgentFromDatabase()方法不能由一个额外的测试testet,因为它的代码是完全由选择()方法的测试覆盖。有没有分支机构的代码可以一起走,所以在这里创建一个额外的测试是没有意义的。 如果GetAgentFromDatabase()方法是从多个方法叫你应该测试它自身虽然。