我正在努力学习如何进行单元测试和模拟。我理解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之外的其他内容吗?任何有关外部链接的建议或指示都将不胜感激。
答案 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()
方法,因此希望将其与数据库隔离。你有几个选择,
虚拟化GetAgentFromDatabase()
,以便您可以为派生类提供返回正确值的代码,在这种情况下创建一个提供IDataReaderFunctionaity
的对象,而无需与DB通信,即
class MyDerivedExample : YourUnnamedClass
{
protected override IDataReader GetAgentFromDatabase()
{
return new MyDataReader({"AgentId", "1"}, {"FirstName", "Fred"},
...);
}
}
作为Gishu suggested而不是使用IsA关系(继承)使用HasA(对象组合),您再次拥有一个处理创建模拟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")]
。
确保只针对特定数据库运行一组测试。
答案 2 :(得分:0)
我会开始提出一些想法,并会一路更新:
答案 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方法..
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...
答案 5 :(得分:0)
至于我的观点,GetAgentFromDatabase()方法不能通过额外的测试来测试,因为它的代码完全由Select()方法的测试覆盖。代码可以没有分支,所以在这里创建一个额外的测试没有意义。 如果从多个方法调用GetAgentFromDatabase()方法,您应该自己测试它。