¿Cómo podría refactorizar este método de tipo de fábrica y la llamada a la base de datos para ser comprobable?

StackOverflow https://stackoverflow.com/questions/1233486

Pregunta

Estoy tratando de aprender cómo hacer Pruebas y burlas de unidades. Entiendo algunos de los principios de TDD y pruebas básicas. Sin embargo, estoy buscando refactorizar el siguiente código que se escribió sin pruebas y estoy tratando de entender cómo debe cambiar para que sea comprobable.

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 están en una sola clase. El código relacionado con la base de datos en GetAgentFromDatabase está relacionado con las bibliotecas empresariales.

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

¿Fue útil?

Solución

Estás en lo correcto al mover GetAgentFromDatabase () a 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 definí la interfaz IAgentDataProvider de la siguiente manera:

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

Entonces, AgentRepository es la clase bajo prueba. Nos burlaremos de IAgentDataProvider e inyectaremos la dependencia. (Lo hice con Moq , pero puedes 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() { ... };
    }
}

(omití la implementación de la clase FakeAgentDataReader , que implementa IDataReader y es trivial: solo necesita implementar Read () y Dispose () para que las pruebas funcionen.)

El propósito de AgentRepository aquí es tomar objetos IDataReader y convertirlos en objetos Agent debidamente formados. Puede ampliar el accesorio de prueba anterior para probar casos más interesantes.

Después de la prueba de unidad AgentRepository aislada de la base de datos real, necesitará pruebas de unidad para una implementación concreta de IAgentDataProvider , pero ese es un tema para una pregunta por separado. HTH

Otros consejos

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. Tienes varias opciones,

  1. Virtualice el GetAgentFromDatabase () para que pueda proporcionar una clase derivada con código para devolver los valores correctos, en este caso, crear un objeto que proporcione IDataReaderFunctionaity sin hablando con el DB es decir

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

    Sin embargo, ambos resultan en un montón de código que simplemente define un conjunto de resultados que se devolverán cuando se los consulte. 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 es realmente bueno para hacer 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();
    }
    

Considérelo por un momento. 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 puedan usarse en futuras pruebas.

Hay dos cosas a tener en cuenta Asegúrese de que sus 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 particular.

Comenzaré a presentar algunas ideas y las actualizaré en el camino:

  • SqlDatabase sqlDb = new SqlDatabase (" MyConnectionString "); - Debe evitar los operadores nuevos mezclados con la lógica. Debe construir xo tener operaciones lógicas; evite que ocurran 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 desea realizar una prueba unitaria (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ía separar la recuperación de Reader a otra clase, para que pueda burlarse de esta clase mientras prueba el código de fábrica.

En mi opinión, normalmente solo debe preocuparse por hacer que sus propiedades / métodos públicos sean verificables. Es decir. siempre que Select (int agentId) funcione, normalmente no le importa cómo lo hace a través de GetAgentFromDatabase (int agentId) .

Lo que tiene parece razonable, ya que imagino que se puede probar con algo como lo siguiente (suponiendo que su clase se llame AgentRepository)

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

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

Suponiendo que está intentando probar el método de selección público de la clase [NoName] ..

  1. Mueva el método GetAgentFromDatabase () a una interfaz, por ejemplo, IDB_Access. Deje que NoName tenga un miembro de interfaz que se pueda establecer como un parámetro ctor o una propiedad. Entonces, ahora que tiene una costura, puede cambiar el comportamiento sin modificar el código en el método.
  2. Cambiaría el tipo de retorno del método anterior para devolver algo más general: parece que lo está utilizando como una tabla hash. Deje que la implementación de producción de IDB_Access use IDataReader para crear la tabla hash 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. privado Hashtable GetAgentFromDatabase (int agentId)
  3. A continuación, para su prueba de unidad, puede trabajar con un trozo (o usar algo más avanzado como un marco simulado)

.

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

En mi opinión, el método GetAgentFromDatabase () no debe probarse mediante una prueba adicional, porque su código está completamente cubierto por la prueba del método Select (). No hay ramas por las que el código pueda caminar, por lo que no tiene sentido crear una prueba adicional aquí. Si se llama al método GetAgentFromDatabase () desde varios métodos, debería probarlo solo.

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top