Como testar a camada de acesso a dados?

17

Eu tenho um método DAO que utiliza o Spring para acesso JDBC. Ele calcula a taxa de sucesso de um vendedor em vender um item.

Aqui está o código:

public BigDecimal getSellingSuccessRate(long seller_id) {
    String sql = "SELECT SUM(IF(sold_price IS NOT NULL, 1, 0))/SUM(1) 
                  FROM transaction WHERE seller_id = ?";
    Object[] args = {seller_id};
    return getJdbcTemplate().queryForObject(sql, args, BigDecimal.class);
}

Como devo testar esse método ou qualquer método DAO com JUnit? Quais são algumas das práticas recomendadas para testar a lógica de acesso a dados? Estou pensando em testá-lo em um banco de dados incorporável carregado com alguns dados, mas não deveríamos fazer testes de integração semelhantes a um ambiente de produção em termos de RDBMS e o esquema?

Michael
fonte
Confira DBUnit . Foi feito especificamente para resolver seu problema.
24413 Sergio Sergio

Respostas:

15

O problema com o uso de um banco de dados 'real' para teste de unidade é a configuração, remoção e isolamento dos testes. Você não precisa criar um banco de dados MySQL totalmente novo e criar tabelas e dados apenas para um teste de unidade. Os problemas com isso estão relacionados à natureza externa do banco de dados e seu banco de dados de teste está inoperante, seus testes de unidade falham. Também há problemas em garantir que você tenha um banco de dados exclusivo para teste. Eles podem ser superados, mas há uma resposta mais simples.

Zombar do banco de dados é uma opção, no entanto ele não testa as consultas reais executadas. Ele pode ser usado como uma solução muito mais simples quando você deseja garantir que os dados do DAO passem pelo sistema corretamente. Mas, para testar o próprio DAO, você precisa que algo por trás do DAO tenha os dados e as consultas sejam executadas corretamente.

A primeira coisa a fazer é usar um banco de dados em memória. O HyperSQL é uma excelente opção para isso, pois tem a capacidade de emular o dialeto de outro banco de dados - para que as pequenas diferenças entre os bancos de dados permaneçam as mesmas (tipos de dados, funções e similares). O hsqldb também possui alguns recursos interessantes para teste de unidade.

db.url=jdbc:hsqldb:file:src/test/resources/testData;shutdown=true;

Isso carrega o estado do banco de dados (as tabelas, dados iniciais) do testDataarquivo. shutdown=truedesligará automaticamente o banco de dados quando a última conexão for fechada.

Usando a injeção de dependência , faça com que os testes de unidade selecionem um banco de dados diferente daquele utilizado pela produção (ou teste ou local).

Seu DAO usa o banco de dados injetado para o qual você pode iniciar testes no banco de dados.

Os testes de unidade serão parecidos com (um monte de coisas chatas não incluídas por questões de brevidade):

    @Before
    public void setUpDB() {
        DBConnection connection = new DBConnection();
        try {
            conn = connection.getDBConnection();
            insert = conn.prepareStatement("INSERT INTO data (txt, ts, active) VALUES (?, ?, ?)");
        } catch (SQLException e) {
            e.printStackTrace();
            fail("Error instantiating database table: " + e.getMessage());
        }
    }

    @After
    public void tearDown() {
        try {
            conn.close();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

    private void addData(String txt, Timestamp ts, boolean active) throws Exception {
        insert.setString(1, txt);
        insert.setTimestamp(2, ts);
        insert.setBoolean(3, active);
        insert.execute();
    }

    @Test
    public void testGetData() throws Exception {
        // load data
        Calendar time = Calendar.getInstance();
        long now = time.getTimeInMillis();
        long then1h = now - (60 * 60 * 1000);  // one hour ago
        long then2m = now - (60 * 1000 * 2);   // two minutes ago
        addData("active_foo", new Timestamp(then1h), true);     // active but old
        addData("inactive_bar", new Timestamp(then1h), false);  // inactive and old
        addData("active_quz", new Timestamp(then2m), true);     // active and new
        addData("inactive_baz", new Timestamp(then2m), false);  // inactive and new

        DataAccess dao = new DataAccess();
        int count = 0;
        for (Data data : dao.getData()) {
            count++;
            assertTrue(data.getTxt().startsWith("active"));
        }

        assertEquals("got back " + count + " rows instead of 1", count, 1);
    }

E, portanto, você tem um teste de unidade que chama o DAO e está usando os dados que foram configurados em um banco de dados dinâmico que existe durante a duração do teste. Você não precisa se preocupar com recursos externos ou com o estado do banco de dados antes da execução ou com a restauração para um estado conhecido (bem, o 'estado conhecido' é 'não existe', o que é trivial para reverter).

O DBUnit pode fazer muito do que descrevi em um processo mais simples na configuração do banco de dados, na criação de tabelas e no carregamento dos dados. Se você precisar usar o banco de dados real por algum motivo, essa é de longe a melhor ferramenta a ser usada.

O código acima é parte de um projeto que eu escrevi para a prova de conceito TestingWithHsqldb on github


fonte
2
Eu não sabia sobre a parte em que o HSQL pode zombar do dialeto de outro fornecedor de banco de dados. Obrigado.
28513 Michael
1
@ Dog, isso pode ser feito através das propriedades do banco de dados , como as sql.syntax_mys=trueque alteram a maneira como o hsqldb funciona: "Esta propriedade, quando definida como verdadeira, habilita o suporte aos tipos TEXT e AUTO_INCREMENT e também permite a compatibilidade com outros aspectos desse dialeto". while sql.syntax_ora=true"Esta propriedade, quando definida como verdadeira, habilita o suporte a tipos não-padrão. Também habilita as sintaxes DUAL, ROWNUM, NEXTVAL e CURRVAL e também permite a compatibilidade com alguns outros aspectos desse dialeto".
DBUnit é a maneira :)
Silviu Burcea
O @SilviuBurcea DBUnit certamente torna muito mais fácil a instalação de um ambiente complexo de teste de banco de dados do que fazê-lo manualmente. Às vezes, ainda é útil saber como fazê-lo manualmente, se necessário (a abordagem 'manual' mencionada acima pode ser migrada para outros idiomas em que o DBUnit não é uma opção).
Você pode dar uma olhada no Acolyte
cchantep
2

Primeiro, você nunca deve fazer testes em um ambiente de produção. Você deve ter um ambiente de teste que espelhe seu ambiente de produção e faça testes de integração lá.

Se você fizer isso, poderá fazer várias coisas.

  • Escreva testes de unidade para verificar se o SQL apropriado está sendo enviado a um item falso usando uma estrutura de simulação, como o Mockito. Isso garantirá que seu método esteja fazendo o que deveria e tira a integração de cena.
  • Escreva scripts SQL de teste demonstrando a adequação do SQL que você testou em seus testes de unidade. Isso pode ajudar com quaisquer problemas de ajuste que você possa encontrar, como também é possível executar explicações e outras com base em seus scripts de teste.
  • Use DBUnit, como mencionado por @Sergio.
Matthew Flynn
fonte
Woops quando eu disse que o ambiente de produção realmente significava uma simulação dele. Obrigado pela sua resposta, vou dar uma olhada no Mockito, porque é algo que eu também queria aprender.
24513 Michael
1

Em nosso projeto, cada desenvolvedor está executando um banco de dados vazio, sua estrutura é a mesma do banco de dados de produção.

Em cada teste de unidade TestInitialize, criamos uma conexão e transação com o banco de dados, além de alguns objetos padrão necessários para cada teste. E tudo é revertido após o final de cada método ou classe.

Dessa forma, é possível testar a camada sql. De fato, toda consulta ou chamada ao banco de dados deve ser testada dessa maneira.

A desvantagem é que é lento, então o colocamos em um projeto separado dos nossos testes de unidade regulares. É possível acelerar isso usando um banco de dados na memória, mas a idéia permanece a mesma.

Carra
fonte
Se você estiver usando um banco de dados na memória, uma abordagem drop-create antes da execução de todos os conjuntos de testes poderá ser usada no lugar da transação de reversão, o que é muito mais rápido.
Downhillski
Nunca pensei em fazê-lo dessa maneira antes. Em nossos testes, a maioria dos testes cria um usuário 'x', porém único. Criar um banco de dados uma vez significaria alterar os testes para reutilizar esses objetos.
Carra
Eu sei, estamos na mesma página e gosto da sua abordagem. sua abordagem garante que cada caso de teste possa ser executado independentemente, independentemente da ordem, e sempre que for executado, o estado da tabela de dados é o mesmo.
21418 Downhillski
Isso está correto, a ordem não importa então. Já vimos testes falharem antes, porque a ordem de execução do teste de unidade é diferente em nosso PC de compilação e em nossa máquina local.
Carra