6
respostas

Spring não limpa as tabelas do banco de dados

Eu fiz um teste diferente do que o passado no vídeo, mas percebi que o Spring não está limpando minha tabela quando executo um teste após o outro. Segue exemplo:

ProdutoDAOTest

package br.com.casadocodigo.loja.daos;

// imports omitidos

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = { JPAConfiguration.class, ProdutoDAO.class, DataSourceConfigurationTest.class })
@ActiveProfiles("test")
public class ProdutoDAOTest {

    @Autowired
    private ProdutoDAO daoProduto;

    /**
     * Inicializar o banco de dados antes de rodar os testes.
     */
    @Before
    public void init() {
        // 50 reais
        List<Produto> livrosImpressos = ProdutoBuilder.newProduto(TipoPreco.IMPRESSO, BigDecimal.TEN).more(4)
                .buildAll();
        // 100 reais
        List<Produto> livrosEbook = ProdutoBuilder.newProduto(TipoPreco.EBOOK, BigDecimal.TEN.add(BigDecimal.TEN))
                .more(4).buildAll();

        // 150 reais
        List<Produto> livrosCombo = ProdutoBuilder
                .newProduto(TipoPreco.COMBO, BigDecimal.TEN.add(BigDecimal.TEN.add(BigDecimal.TEN))).more(4).buildAll();

        gravaMuitos(livrosCombo);
        gravaMuitos(livrosEbook);
        gravaMuitos(livrosImpressos);
    }

    /**
     * Percorre uma lista de produtos e salva eles no banco de dados
     * 
     * @param lista
     */
    private void gravaMuitos(List<Produto> lista) {
        lista.forEach(daoProduto::grava);
    }

    /**
     * Teste que vai fazer o processo de verificação dos valores com a API de
     * stream para verificar se o código da consulta no BD esta correto
     */
    @Test
    public void deveSomarTodosOsPrecosPeloTipoDoLivroEbook() {
        // busco todos os produtos do banco de dados
        List<Produto> todos = daoProduto.listaTodos();

        // pego todos os produtos que tem o tipo de preco EBOOK
        // PS: criei o método na classe produto que verifica se possui tal tipo
        // de preço em seus precos
        List<Produto> ebooks = todos
                .stream()
                .filter(produto -> produto.temTipo(TipoPreco.EBOOK))
                .collect(Collectors.toList());

        // faço a soma dos valores dos preços para cada produto, somando o preço
        // para ebook
        // PS: método getValorParaTipo(TipoPreco.EBOOK) percorre a lista de
        // preços e pega o valor do EBOOK
        BigDecimal valorEbooksStream = ebooks
                .stream()
                .map(p -> p.getValorParaTipo(TipoPreco.EBOOK))
                .reduce(BigDecimal.ZERO, BigDecimal::add);

        // valor da soma na consulta do BD
        BigDecimal valorEbooksDAO = daoProduto.somaProdutoPeloTipo(TipoPreco.EBOOK);

        Assert.assertEquals(valorEbooksDAO, valorEbooksStream);
    }

}

JPAConfiguration

package br.com.casadocodigo.loja.configuration;

// imports omitidos

@EnableTransactionManagement
public class JPAConfiguration {

    @Bean
    public JpaTransactionManager transactionManager(EntityManagerFactory emf) {
        return new JpaTransactionManager(emf);
    }

    @Bean
    public LocalContainerEntityManagerFactoryBean entityManagerFactory(DataSource dataSource) {
        LocalContainerEntityManagerFactoryBean factoryBean = new LocalContainerEntityManagerFactoryBean();
        factoryBean.setJpaVendorAdapter(getAdapter());
        factoryBean.setDataSource(dataSource);
        factoryBean.setJpaProperties(aditionalProperties());

        factoryBean.setPackagesToScan("br.com.casadocodigo.loja.models");

        return factoryBean;
    }

    private Properties aditionalProperties() {
        Properties props = new Properties();
        props.setProperty("hibernate.dialect", "org.hibernate.dialect.MySQL5Dialect");
        props.setProperty("hibernate.show_sql", "true");
        props.setProperty("hibernate.format_sql", "false");
        props.setProperty("hibernate.hbm2ddl.auto", "update");

        return props;
    }

    private JpaVendorAdapter getAdapter() {
        return new HibernateJpaVendorAdapter();
    }

    @Bean
    @Profile("dev")
    public DriverManagerDataSource dataSource() {
        DriverManagerDataSource ds = new DriverManagerDataSource();
        ds.setUsername("root");
        ds.setPassword("root");
        ds.setDriverClassName("com.mysql.jdbc.Driver");
        ds.setUrl("jdbc:mysql://localhost:3306/casadocodigo");
        return ds;
    }

}

e, no pacote de teste:

DataSourceConfigurationTest

package br.com.casadocodigo.loja.configuration;

import javax.sql.DataSource;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.jdbc.datasource.DriverManagerDataSource;

@Configuration
public class DataSourceConfigurationTest {

    @Bean
    @Profile("test")
    public DataSource dataSource() {
        DriverManagerDataSource ds = new DriverManagerDataSource();
        ds.setUsername("root");
        ds.setPassword("root");
        ds.setDriverClassName("com.mysql.jdbc.Driver");
        ds.setUrl("jdbc:mysql://localhost:3306/casadocodigo_test");
        return ds;
    }

}

De primeira, eu fiz com que o método aditionalProperties fosse um bean, e no profile de test eu fazia com que a propriedade hibernate.hbm2ddl.auto recebesse o valor de create-drop, mas não achei tão interessante, por criar sempre o banco de dados, atitude desnecessária para esse caso.

Pensei também em criar um método com a annotation @After do JUnit, mas não queria ter que ter esse trabalho sempre que fosse escrever um teste.

Alguma ideia?

6 respostas

Fala Leonardo, tudo bem ?

Notei que no seu teste não está presente a anotação que faz jus a abertura de uma nova transação - @Transactional - quando monta seu cenário salvando dados no banco.

Inclusive estranhei a ausência pois em geral a JPA impede a alteração do estado da base de dados quando não temos um contexto transacional. Por outro lado verifiquei que você configura o transactionManager normalmente na sua configuração. Você por acaso está usando @Transactional em algum outro ponto ?

Creio que é isso que está faltando (@Transactional no contexto do seu teste), porque o Spring Test se baseia no contexto transacional pra "limpar" sua base de dados ao final do seu teste. Em suma, ele abre uma transação, altera o estado da base, realiza as devidas asserções do método, e ao final invalida (rollback) as alterações feitas nesta mesma transação.

Dê uma olhada nisso.

Espero ter ajudado. Abraço!

E ai Rafael, tudo certo e contig?

Cara, eu tenho declarado apenas em meu DAO o Transactional.

Segue o código do ProdutoDAO:

package br.com.casadocodigo.loja.daos;

// import omitido

@Repository
@Transactional
public class ProdutoDAO {

    @PersistenceContext
    private EntityManager manager;

    public void grava(Produto produto) {
        if (produto.getId() == null)
            manager.persist(produto);
        else
            manager.merge(produto);
    }

    public List<Produto> listaTodos() {
        return manager.createQuery("select distinct(p) from Produto p join fetch p.precos", Produto.class)
                .getResultList();
    }

    public Produto buscaPeloId(Long id) {
        TypedQuery<Produto> query = this.manager
                .createQuery("select distinct(p) from Produto p join fetch p.precos where p.id = :pId", Produto.class);
        query.setParameter("pId", id);
        return query.getSingleResult();

    }

    public BigDecimal somaProdutoPeloTipo(TipoPreco tipo) {
        return this.manager
                .createQuery("select sum(preco.valor) from Produto p join p.precos "
                        + "preco where preco.tipo = :pTipoPreco", BigDecimal.class)
                .setParameter("pTipoPreco", tipo).getSingleResult();
    }

}

Então, em suma, mesmo funcionando sem eu abrir uma nova transação para teste (utilizando a transação que declarei no ProdutoDAO, pois eu acho que funciona desta forma), é recomendada abrir uma nova transação para esse teste?

Opa Leonardo,

Eh isso mesmo. Se não tiver a abertura da transação no contexto do seu método de teste, o Spring Test não consegue gerenciá-la e aplicar o rollback ao invés de commitar as alterações. O runner, o contexto da aplicação web, etc, são todos criados no teste, se não houver transação ali ele sequer sabe que existe contexto transacional nas operações que ocorrem no decorrer da execução do teste.

Uma outra coisa. Não é considerada uma boa prática abrir transação no escopo dos métodos do DAO. Dessa forma o escopo das transações fica muito fechado. Imagine um cenário onde suas operações do seu DAO (ou até mesmo duas operações entre daos diferentes) precisam ser executadas numa única transação (como uma operação atômica, como dizem). Neste cenário as transações isoladas no escopo do DAO impedem esse comportamento. Exigiriam uma transação no escopo maior da operação (lógica que chama as duas operações de dao) de qualquer forma.

O ideal é justamente que as transações sejam previstas mais próximas do seu negócio (services e até mesmo controllers). A possibilidade de usar annotations pra isso justamente nos dá essa possibilidade, planejá-las fora do local onde o EntityManager é gerenciado pelo container.

Tente fazer esses ajustes. Deve funcionar. Qualquer duvida poste aqui.

Abraço!

Então a boa pratica seria não deixar para o DAO a responsabilidade de abrir uma transação?

Interessante, faz sentido, assim ele fica desacoplado e um erro não faria um rollback nas outras que, poderiam ser atreladas e executadas ao mesmo tempo.

Mas, infelizmente, ainda não funcionou comigo. Eu fiz o seguinte:

  • Tirei a annotation @Transactional do meu ProdutoDAO
  • Coloquei a annotation @Transactionalno meu método deveSomarTodosOsPrecosPeloTipoDoLivroEbook
  • Retirei a annotation @Before do meu método init e passei a chamá-lo dentro do meu método de teste deveSomarTodosOsPrecosPeloTipoDoLivroEbook

Mas, quando eu executo, o código mostrado para mim no console ainda incrementa o valor do resultado, ou seja, estão sendo adicionados mais valores e o Spring Test não está apagando a tabela depois do teste :(.

Segue classes abaixo:

ProdutoDAOTest

package br.com.casadocodigo.loja.daos;

import java.math.BigDecimal;
import java.util.List;
import java.util.stream.Collectors;

import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.transaction.annotation.Transactional;

import br.com.casadocodigo.loja.builders.ProdutoBuilder;
import br.com.casadocodigo.loja.configuration.DataSourceConfigurationTest;
import br.com.casadocodigo.loja.configuration.JPAConfiguration;
import br.com.casadocodigo.loja.models.Produto;
import br.com.casadocodigo.loja.models.TipoPreco;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = { JPAConfiguration.class, ProdutoDAO.class, DataSourceConfigurationTest.class })
@ActiveProfiles("test")
public class ProdutoDAOTest {

    @Autowired
    private ProdutoDAO daoProduto;

    /**
     * Inicializar o banco de dados antes de rodar os testes.
     */
    public void init() {
        // 50 reais
        List<Produto> livrosImpressos = ProdutoBuilder.newProduto(TipoPreco.IMPRESSO, BigDecimal.TEN).more(4)
                .buildAll();
        // 100 reais
        List<Produto> livrosEbook = ProdutoBuilder.newProduto(TipoPreco.EBOOK, BigDecimal.TEN.add(BigDecimal.TEN))
                .more(4).buildAll();

        // 150 reais
        List<Produto> livrosCombo = ProdutoBuilder
                .newProduto(TipoPreco.COMBO, BigDecimal.TEN.add(BigDecimal.TEN.add(BigDecimal.TEN))).more(4).buildAll();

        gravaMuitos(livrosCombo);
        gravaMuitos(livrosEbook);
        gravaMuitos(livrosImpressos);
    }

    /**
     * Percorre uma lista de produtos e salva eles no banco de dados
     * 
     * @param lista
     */
    private void gravaMuitos(List<Produto> lista) {
        lista.forEach(daoProduto::grava);
    }

    /**
     * Teste que vai fazer o processo de verificação dos valores com a API de
     * stream para verificar se o código da consulta no BD esta correto
     */
    @Test
    @Transactional
    public void deveSomarTodosOsPrecosPeloTipoDoLivroEbook() {

        init();

        // busco todos os produtos do banco de dados
        List<Produto> todos = daoProduto.listaTodos();

        // pego todos os produtos que tem o tipo de preco EBOOK
        // PS: criei o método na classe produto que verifica se possui tal tipo
        // de preço em seus precos
        List<Produto> ebooks = todos
                .stream()
                .filter(produto -> produto.temTipo(TipoPreco.EBOOK))
                .collect(Collectors.toList());

        // faço a soma dos valores dos preços para cada produto, somando o preço
        // para ebook
        // PS: método getValorParaTipo(TipoPreco.EBOOK) percorre a lista de
        // preços e pega o valor do EBOOK
        BigDecimal valorEbooksStream = ebooks
                .stream()
                .map(p -> p.getValorParaTipo(TipoPreco.EBOOK))
                .reduce(BigDecimal.ZERO, BigDecimal::add);

        // valor da soma na consulta do BD
        BigDecimal valorEbooksDAO = daoProduto.somaProdutoPeloTipo(TipoPreco.EBOOK);
        System.out.println(valorEbooksDAO);

        Assert.assertEquals(valorEbooksDAO.setScale(2), valorEbooksStream.setScale(2));
    }

}

ProdutoDAO (sem @Transaction)

package br.com.casadocodigo.loja.daos;

import java.math.BigDecimal;
import java.util.List;

import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.TypedQuery;

import org.springframework.stereotype.Repository;

import br.com.casadocodigo.loja.models.Produto;
import br.com.casadocodigo.loja.models.TipoPreco;

@Repository
public class ProdutoDAO {

    @PersistenceContext
    private EntityManager manager;

    public void grava(Produto produto) {
        if (produto.getId() == null)
            manager.persist(produto);
        else
            manager.merge(produto);
    }

    public List<Produto> listaTodos() {
        return manager.createQuery("select distinct(p) from Produto p join fetch p.precos", Produto.class)
                .getResultList();
    }

    public Produto buscaPeloId(Long id) {
        TypedQuery<Produto> query = this.manager
                .createQuery("select distinct(p) from Produto p join fetch p.precos where p.id = :pId", Produto.class);
        query.setParameter("pId", id);
        return query.getSingleResult();

    }

    public BigDecimal somaProdutoPeloTipo(TipoPreco tipo) {
        return this.manager
                .createQuery("select sum(preco.valor) from Produto p join p.precos "
                        + "preco where preco.tipo = :pTipoPreco", BigDecimal.class)
                .setParameter("pTipoPreco", tipo).getSingleResult();
    }

}

Deixei passar alguma coisa?

Leonardo parece besteira, mais como você não estava anotando o método de teste com @Transactional, você chegou a limpar a tabela do banco de testes antes de começar um novo teste com o método agora sim anotado corretamente? Pode ter ficado apenas um sujeira lá antes de você começar a testar novamente...

Comigo aconteceu a mesma coisa, e foi solucionado com o contexto transacional para o método (@Transaction).

Meu método ficou assim:

    @Test
    @Transactional
    public void mustSumTheValueOfAllBooksOfAPriceType() {

        List<Product> printedBooks = ProductBuilder.newProduct(PriceType.PRINTED, BigDecimal.TEN).more(3).buildAll();
        List<Product> eBooks = ProductBuilder.newProduct(PriceType.EBOOK, BigDecimal.TEN).more(3).buildAll();

        for (Product product : printedBooks) {
            productDAO.save(product);
        }

        for (Product product : eBooks) {
            productDAO.save(product);
        }

        BigDecimal value = productDAO.sumTotalValueOfBooksByPriceType(PriceType.EBOOK);

        Assert.assertEquals(new BigDecimal(40).setScale(2), value);

    }

Olá pessoal. Estou com o mesmo problema, ou seja, o rollback não está sendo executado após a execução do teste.

Seguem abaixo minhas classes.

Obrigado.

public class DataSourceConfigurationTest {

    @Bean
    @Profile("test")
    public DataSource dataSource() {
        DriverManagerDataSource dataSource = new DriverManagerDataSource();
        dataSource.setUsername("root");
        dataSource.setPassword("root");
        dataSource.setUrl("jdbc:mysql://localhost:3306/casadocodigo_test?useTimezone=true&serverTimezone=UTC");
        dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");

        return dataSource;
    }
}

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes= {JPAConfiguration.class, ProdutoDAO.class,
        DataSourceConfigurationTest.class})
@ActiveProfiles("test")
public class ProdutoDAOTest {

    @Autowired
    private ProdutoDAO produtoDAO;

    @Test
    @Transactional
    @Rollback(true)
    public void deveSomarPrecosPorTipo() {
        List<Produto> livrosImpressos = ProdutoBuilder.newProduto(TipoPreco.IMPRESSO, BigDecimal.TEN)
                .mais(3)
                .buildAll();

        List<Produto> livrosEbooks = ProdutoBuilder.newProduto(TipoPreco.EBOOK, BigDecimal.TEN)
                .mais(3)
                .buildAll();

        livrosImpressos.stream().forEach(produtoDAO::grava);
        livrosEbooks.stream().forEach(produtoDAO::grava);

        BigDecimal valor = produtoDAO.somaPrecosPorTipo(TipoPreco.EBOOK);
        Assert.assertEquals(new BigDecimal(40).setScale(2), valor);
    }
}

@EnableTransactionManagement
public class JPAConfiguration {

    @Bean
    public LocalContainerEntityManagerFactoryBean entityManagerFactory(DataSource dataSource) {
        LocalContainerEntityManagerFactoryBean factoryBean = new LocalContainerEntityManagerFactoryBean();

        JpaVendorAdapter jpaVendorAdapter = new HibernateJpaVendorAdapter();
        factoryBean.setJpaVendorAdapter(jpaVendorAdapter );
        factoryBean.setDataSource(dataSource);
        factoryBean.setJpaProperties(aditionalProperties());
        factoryBean.setPackagesToScan("br.com.casadocodigo.loja.models");

        return factoryBean;
    }

    private Properties aditionalProperties() {
        Properties jpaProperties = new Properties();
        jpaProperties.setProperty("hibernate.dialect", "org.hibernate.dialect.MySQL5Dialect");
        jpaProperties.setProperty("hibernate.show_sql", "true");
        jpaProperties.setProperty("hibernate.hbm2ddl.auto", "update");
        return jpaProperties;
    }

    @Bean
    @Profile("dev")
    public DriverManagerDataSource dataSource() {
        DriverManagerDataSource dataSource = new DriverManagerDataSource();
        dataSource.setUsername("root");
        dataSource.setPassword("root");
        dataSource.setUrl("jdbc:mysql://localhost:3306/casadocodigo?useTimezone=true&serverTimezone=UTC");
        dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");

        return dataSource;
    }

    @Bean
    public JpaTransactionManager transactionManager(EntityManagerFactory emf) {
        return new JpaTransactionManager(emf);
    }
}