Limpar e organizar práticas durante o teste de integração para evitar bancos de dados sujos

9

Estou codificando testes em c # e resolvi com essa estrutura:

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
}

O conceito era "todo teste limpa a bagunça que faz". No entanto, alguns testes estão deixando o banco de dados sujo e com falha nos testes que se seguem.

Qual é o caminho certo para fazer isso? (minimizar erros, minimizar o tempo de execução)

  • Deletes everything» Insert defaults» Insert test data»Executar teste?
  • Insert defaults» Insert test data» Executar teste » Delete everything?

  • Atualmente :

    • (por sessão) Deletes everything»Insert defaults
    • (por teste) Insert test data»Executar teste»Delete test data
dialex
fonte

Respostas:

7

Além do fato de se tratar de um teste de integração, em oposição a um teste de unidade, as operações que você descreve normalmente são realizadas Setupe / ou Teardownmétodos. Estruturas como o nUnit permitem decorar métodos de classe com esses atributos para indicar se o método é um método de instalação ou desmontagem.

Seus testes devem ficar mais limpos e menores, pois a instalação e a limpeza são feitas fora do próprio teste.

Provavelmente, vários testes podem reutilizar os mesmos dados, o que é uma vantagem, além de se opor à inserção / remoção em todos os testes. Voltando ao nUnit , os atributos FixtureSetupe FixtureTeardownajudam a configurar dados para vários testes ao mesmo tempo.

Eu usaria uma estrutura de teste em uma tentativa / captura, pois muitos desses recursos de teste são construídos na própria estrutura. xUnit, nUnit, mesmo a estrutura de teste integrada da Microsoft são todas opções sólidas e ajudarão na configuração e limpeza de registros de banco de dados de maneira consistente.

Jon Raynor
fonte
8

O ponto que você deve ter em mente com esses testes é que o maior número possível deles deve estar interagindo com uma simulação do banco de dados, e não com o próprio banco de dados. A maneira padrão de conseguir isso é injetar uma camada de acesso ao banco de dados na lógica que você está testando aqui, usando interfaces. Dessa forma, o código de teste pode criar conjuntos de dados na memória antes de cada teste e depois descartá-los posteriormente. Todos os testes podem ser executados em paralelo e não se afetam. Isso torna seus testes mais rápidos, mais fáceis de escrever e entender e mais robustos.

Então você precisa testar a própria camada de acesso ao banco de dados. Como você terá apenas alguns desses testes, eles podem, por exemplo, criar uma tabela de teste (ou mesmo banco de dados), exclusiva para esse teste, e preenchê-la com dados de teste. Após a execução do teste, toda a tabela de teste / banco de dados é destruída. Novamente, esses testes devem poder ser executados em paralelo, portanto, não devem ter um impacto significativo no tempo geral de execução do teste.

David Arno
fonte
Bem, isso é uma pequena mudança para nós agora (começamos o teste de unidade há 3 meses). Supondo que, por enquanto, usaremos um banco de dados real para testes, qual é a ordem padrão / segura de fazer isso - exclua tudo, insira tudo e execute o teste?
Dia28
1
Se a reestruturação estiver fora de questão, então - no seu caso - a resposta de @ JonRaynor fornece sua melhor opção.
David Arno
5

O grande problema com bancos de dados e testes (unitários) é que os bancos de dados são muito bons em coisas persistentes.

A solução usual é não usar um banco de dados real em seus testes de unidade, mas sim zombar do banco de dados ou usar um banco de dados na memória que possa ser facilmente apagado completamente entre os testes.
Somente ao testar o código que interage diretamente com o banco de dados ou nos testes de ponta a ponta, o banco de dados real será usado.

Bart van Ingen Schenau
fonte
5

Trabalhando em um servidor C # com SQL Server e PetaPoco , essa é a abordagem que adotamos para limpar dados em testes de unidade.

Um teste de unidade típico teria Setup e Teardown da seguinte maneira:

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

    // ... 
}

Onde PlatformDataObject é uma classe responsável pela comunicação com o banco de dados, por exemplo, executando Select Insert Update Deletes. Todos os tipos * DataObject herdam ServerDataObject - a classe base possui métodos para interromper, reverter ou confirmar a transação.

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

Todos os testes de unidade chamariam RollbackTransaction () e, finalmente, chamariam IDbTransaction.Rollback ().

Nos testes, achamos rotineiro criar uma nova instância de um * DataObject, criar algumas linhas usando instruções Insert, executar testes nelas (Selects, Updates etc ...) e depois reverter.

Podemos configurar um conjunto de dados de teste antes de todos os testes serem executados usando um SetUpFixture - uma classe executada uma vez antes de todos os testes serem executados e excluir / reverter os dados em desmontagem após a execução de todos os testes.

Dr. Andrew Burnett-Thompson
fonte