2009-08-05 11 views
12

Estoy tratando de aprender cómo hacer pruebas unitarias y burlas. Entiendo algunos de los principios de TDD y pruebas básicas. Sin embargo, estoy buscando refacturar el siguiente código que se escribió sin pruebas y estoy tratando de entender cómo debe cambiar para que sea comprobable.¿Cómo podría refactorizar este método de fábrica y la llamada a la base de datos para poder probarlo?

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

} 

Estos dos métodos son de una sola clase. El código relacionado con la base de datos en GetAgentFromDatabase está relacionado con Enterprise Libraries.

¿Cómo podría hacer para que este comprobable? ¿Debo abstraer el método GetAgentFromDatabase en una clase diferente? ¿Debería GetAgentFromDatabase devolver algo que no sea un IDataReader? Cualquier sugerencia o sugerencia a enlaces externos sería muy apreciada.

Respuesta

9

Tiene razón sobre cómo mover GetAgentFromDatabase() en una clase separada. Así es como redefiní 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; 
    } 
} 

donde he definido la interfaz deIAgentDataProvider de la siguiente manera:

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

Así, AgentRepository es la clase que se está probando. Nos burlaremos IAgentDataProvider e inyectamos la dependencia. (Lo hice con Moq, pero puede rehacerlo fácilmente con un marco de aislamiento diferente).

[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 dejó a cabo la implementación de la clase FakeAgentDataReader, que implementa IDataReader y es trivial - sólo tiene que poner en práctica Read() yDispose() para hacer el trabajo de pruebas .)

El propósito de AgentRepository aquí es tomar IDataReader objetos y convertirlos en rectamente formada Agente objetos. Puede expandir el accesorio de prueba anterior para probar casos más interesantes.

Después de la unidad de pruebas de AgentRepository al margen de la base de datos real, se necesitarán pruebas unitarias para una aplicación concreta de IAgentDataProvider, pero eso es un tema para una cuestión aparte. HTH

0

voy a empezar a poner algunas ideas y se actualizará a lo largo del camino:

  • SqlDatabase SQLdb = new SqlDatabase ("myConnectionString"); - Debe evitar nuevos operadores mezclados con la lógica. Debe construir xor tener operaciones lógicas; evitar que sucedan al mismo tiempo. Use la inyección de Dependencia para pasar esta base de datos como parámetro, para que pueda burlarse de ella. Me refiero a esto si quieres probarlo en una unidad (no ir a la base de datos, lo que debería hacerse en algún caso más adelante)
  • IDataReader agentInformation = GetAgentFromDatabase (agentId) - Tal vez podrías separar la recuperación del Reader en alguna otra clase, por lo puedes burlarte de esta clase mientras pruebas el código de fábrica.
0

IMO normalmente solo debería preocuparse por hacer que sus propiedades/métodos públicos sean comprobables. Es decir. siempre y cuando Seleccione (int agentId) funciona normalmente no le importa cómo lo hace a través de GetAgentFromDatabase (int agentId).

Lo que tienes parece razonable, ya que me imagino que puede ser probado con algo como lo siguiente (suponiendo que su clase se llama AgentRepository)

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

En cuanto a las mejoras sugeridas. Recomendaría que se cambie la cadena de conexión del AgentRepository, ya sea por acceso público o interno.

0

Suponiendo que usted está tratando de probar la Selección de método público de la clase [NoName] ..

  1. Mover el método GetAgentFromDatabase() en una interfaz decir IDB_Access. Deje que NoName tenga un miembro de interfaz que se pueda establecer como un parámetro de ctor o una propiedad. Ahora que tiene una costura, puede cambiar el comportamiento sin modificar el código en el método.
  2. Cambiaría el tipo de devolución del método anterior para devolver algo más general: parece que lo está usando como una tabla hash. Deje que la implementación de producción de IDB_Access use el IDataReader para crear el hashtable internamente. También lo hace menos dependiente de la tecnología; Puedo implementar esta interfaz usando MySql o algún entorno que no sea MS/.net. private Hashtable GetAgentFromDatabase(int agentId)
  3. Siguiente para su prueba de la unidad, se puede trabajar con un talón (o usar algo más avanzada, como un marco maqueta)

.

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... 
0

En cuanto a mi opinión el método GetAgentFromDatabase() no deben testet por una prueba adicional, debido a que su código está totalmente cubierto por la prueba del método Select(). No hay ramas en las que el código pueda avanzar, así que no tiene sentido crear una prueba adicional aquí. Si se llama al método GetAgentFromDatabase() desde múltiples métodos, debe probarlo solo.

1

El problema aquí es decidir qué es SUT y qué es Test. Con su ejemplo, está intentando probar el método Select() y, por lo tanto, desea aislarlo de la base de datos. Tiene varias opciones,

  1. virtualizar la GetAgentFromDatabase() por lo que puede proporcionar una clase derivada con código para devolver los valores correctos, en este caso la creación de un objeto que proporciona IDataReaderFunctionaity sin hablar con el DB es decir

    class MyDerivedExample : YourUnnamedClass 
    { 
        protected override IDataReader GetAgentFromDatabase() 
        { 
         return new MyDataReader({"AgentId", "1"}, {"FirstName", "Fred"}, 
          ...); 
        } 
    } 
    
  2. Como Gishu suggested en lugar de usar relaciones IsA (herencia), use HasA (composición de objetos) donde nuevamente tiene una clase que maneja la creación de una simulación IDataReader, pero esta vez sin heredar.

    Sin embargo, ambos resultan en muchos códigos que simplemente definen un conjunto de resultados que se devuelven cuando se consulta. Es cierto que podemos mantener este código en el código de prueba, en lugar de nuestro código principal, pero es un esfuerzo. Todo lo que realmente está haciendo es definir un conjunto de resultados para consultas particulares, y sabe lo que realmente hace eso ... Una base de datos

  3. Utilicé LinqToSQL hace un tiempo y descubrí que los objetos DataContext tienen algunos métodos muy útiles , incluidos DeleteDatabase y 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(); 
    } 
    

Consideremos por un tiempo. El problema con el uso de una base de datos para pruebas unitarias es que los datos cambiarán. Elimine su base de datos y use sus pruebas para evolucionar sus datos que pueden usarse en futuras pruebas.

Hay dos cosas que tener cuidado con Asegúrate de que tus pruebas se ejecuten en el orden correcto. La sintaxis de MbUnit para esto es [DependsOn("NameOfPreviousTest")]. Asegúrese de que solo se ejecute un conjunto de pruebas en una base de datos en particular.

Cuestiones relacionadas