Prácticas de limpieza y organización durante las pruebas de integración para evitar bases de datos sucias

9

Estoy codificando pruebas en C # y me decidí por esta estructura:

try
{
    // ==========
    // ARRANGE
    // ==========

    // Insert into the database all test data I'll need during the test

    // ==========
    // ACT
    // ==========

    // Do what needs to be tested

    // ==========
    // ASSERT
    // ==========

    // Check for correct behavior
}
finally
{
    // ==========
    // CLEANUP
    // ==========

    // Inverse of ARRANGE, delete the test data inserted during this test
}

El concepto era "cada prueba limpia el desorden que hace". Sin embargo, algunas pruebas dejan la base de datos sucia y fallan las pruebas posteriores.

¿Cuál es la forma correcta de hacer esto? (minimizar errores, minimizar el tiempo de ejecución)

  • Deletes everything» Insert defaults» Insert test data»¿Ejecutar prueba?
  • Insert defaults» Insert test data» Ejecutar prueba » Delete everything?

  • Actualmente :

    • (por sesión) Deletes everything»Insert defaults
    • (por prueba) Insert test data»Ejecutar prueba»Delete test data
dialex
fuente

Respuestas:

7

Además del hecho de que esta es una prueba de integración en lugar de una prueba unitaria, las operaciones que describe generalmente entran Setupy / o Teardownmétodos. Los marcos como nUnit permiten decorar métodos de clase con estos atributos para indicar si el método es un método de configuración o un método de desmontaje.

Luego, sus pruebas deberían volverse más limpias y pequeñas a medida que la configuración y la limpieza se realicen fuera de la prueba misma.

Lo más probable es que las pruebas múltiples puedan reutilizar los mismos datos, por lo que es una ventaja además de optar por insertar / eliminar en cada prueba. Volviendo a nUnit , los atributos FixtureSetupy FixtureTeardownayudan a configurar los datos para múltiples pruebas a la vez.

Usaría un marco de prueba sobre un intento / captura ya que muchas de estas características de prueba están integradas en el marco mismo. xUnit, nUnit, incluso el marco de prueba incorporado de Microsoft son opciones sólidas y ayudarán con la configuración y limpieza de los registros de la base de datos de manera consistente.

Jon Raynor
fuente
8

El punto al que debe apuntar con tales pruebas es que la mayor cantidad posible de ellas debería estar interactuando con una simulación de la base de datos, en lugar de la base de datos en sí. La forma estándar de lograr esto es inyectar una capa de acceso DB en la lógica que está probando aquí, utilizando interfaces. De esa forma, el código de prueba puede crear conjuntos de datos en memoria antes de cada prueba y luego desecharlos después. Todas las pruebas pueden ejecutarse en paralelo y no se afectarán entre sí. Esto hace que sus pruebas sean más rápidas, más fáciles de escribir y comprender y más robustas.

Luego debe probar la capa de acceso a la base de datos en sí. Debido a que solo tendrá algunas de estas pruebas, pueden, por ejemplo, crear una tabla de prueba (o incluso una base de datos), única para esa prueba, y llenarla con datos de prueba. Una vez que se ha ejecutado la prueba, se destruye toda la tabla de prueba / DB. Una vez más, estas pruebas deberían poder ejecutarse en paralelo, por lo que no deberían tener un impacto significativo en el tiempo general de ejecución de la prueba.

David Arno
fuente
Bueno, eso es un pequeño cambio para nosotros en este momento (comenzamos las pruebas unitarias hace 3 meses). Suponiendo que, por el momento, usaremos una base de datos real para las pruebas, ¿cuál es el orden estándar / seguro de hacer esto? ¿Eliminar todo, insertar todo y luego ejecutar la prueba?
dialex
1
Si la reestructuración está fuera de discusión, entonces, en su caso, la respuesta de @ JonRaynor proporciona su mejor opción.
David Arno
5

El gran problema con las bases de datos y las pruebas (unitarias) es que las bases de datos son muy buenas para persistir.

La solución habitual es no usar una base de datos real en sus pruebas unitarias, sino burlarse de la base de datos o usar una base de datos en memoria que pueda borrarse fácilmente entre pruebas.
Solo cuando se prueba el código que interactúa directamente con la base de datos, o en pruebas de extremo a extremo, se usará la base de datos real.

Bart van Ingen Schenau
fuente
5

Trabajando en un servidor C # con SQL Server y PetaPoco , este es el enfoque que tomamos para limpiar los datos en las pruebas unitarias.

Una prueba unitaria típica tendría la configuración y el desmontaje de la siguiente manera:

[TestFixture]
internal class PlatformDataObjectTests
{
    private IDatabaseConfiguration _dbConfig;
    private Database _pocoDatabase;
    private PlatformDataObject _platformDto;

    [SetUp]
    public void Setup()
    {
        _dbConfig = new CommonTestsAppConfig().GetDatabaseConfiguration();
        _pocoDatabase = new Database(_dbConfig.ConnectionString, SqlClientFactory.Instance);
        _platformDto = new PlatformDataObject(_pocoDatabase);
        _platformDto.BeginTransaction();
    }

    [TearDown]
    public void TearDown()
    {
        Console.WriteLine("Last Sql: {0}", _pocoDatabase.LastCommand);

        _platformDto.RollbackTransaction();
        _platformDto.Dispose();
    }

    // ... 
}

Donde PlatformDataObject es una clase responsable de comunicarse con la base de datos, por ejemplo, realizar Seleccionar Insertar Actualizar Eliminar. Todos los tipos de * DataObject heredan ServerDataObject: la clase base tiene métodos para abortar, revertir o confirmar la transacción.

/// <summary>
/// A Data-Transfer Object which allows creation and querying of Platform types from the database
/// </summary>
[ExportType(typeof(IPlatformDataObject))]
public class PlatformDataObject : ServerDataObject, IPlatformDataObject
{
    private static readonly ILog Log = LogManager.GetLogger(typeof (ProductDataObject));

    private const string PlatformTable = "t_Platform";

    public PlatformDataObject(IPocoDatabase pocoDatabase) : base(pocoDatabase)
    {
    }

    ... 
}

/// <summary>
/// A base Data-Transfer Object type
/// </summary>
public abstract class ServerDataObject : IServerDataObject
{
    protected const string Star = "*";

    private readonly IPocoDatabase _pocoDatabase;

    public ServerDataObject(IPocoDatabase pocoDatabase)
    {
        _pocoDatabase = pocoDatabase;
    }

    public string LastCommand
    {
        get { return PocoDatabase.LastCommand; }
    }

    public IPocoDatabase PocoDatabase
    {
        get { return _pocoDatabase; }
    }

    public int TransactionDepth
    {
        get { return _pocoDatabase.TransactionDepth; }
    }

    public bool TransactionAborted { get; private set; }

    public void BeginTransaction()
    {
        _pocoDatabase.BeginTransaction();
    }

    public void AbortTransaction()
    {
        _pocoDatabase.AbortTransaction();
    }

    public void RollbackTransaction()
    {
        TransactionAborted = true;
    }

    public virtual void Dispose()
    {
        if (TransactionAborted)
            _pocoDatabase.AbortTransaction();
        else
            _pocoDatabase.CompleteTransaction();
    }
}

Todas las pruebas unitarias llamarían RollbackTransaction (), finalmente llamarían IDbTransaction.Rollback ().

En las pruebas, encontramos que era una rutina crear una nueva instancia de un * DataObject, crear algunas filas usando instrucciones Insertar, realizar pruebas en ellas (selecciones, actualizaciones, etc.) y luego revertir.

Podemos configurar un conjunto de datos de prueba antes de que todas las pruebas se ejecuten utilizando un SetUpFixture , una clase que se ejecuta una vez antes de que se ejecuten todas las pruebas, y eliminar / deshacer los datos en desmontaje después de que se ejecuten todas las pruebas.

Dr. Andrew Burnett-Thompson
fuente