Dificuldades com testes unitários em sistemas data-centric

Pessoal

Ao longo da minha carreira, já passei por 3 projetos em que me deparei com o cenário-problema que eu vou descrever abaixo.

O objetivo do meu post é ouvir opiniões de vocês a respeito dos problemas que eu encontrei nestes cenários para a implementação de testes unitários.

Em todos os casos eram sistemas que acessavam bancos de dados legados com as seguintes características:

  • O banco de dados legado é um Oracle 8i
  • Existe um total de 3.000 tabelas neste banco de dados
  • O volume dos dados em produção beira os 8 TERA
  • 90% destas tabelas se relacionam entre si de forma declarada (milhares de constraints de FK)
  • Os relacionamentos declarados são bastante complexos pois existem muitas chaves primárias compostas por N chaves estrangeiras em cascatas de até 5 níveis
  • Meu sistema precisa manipular dados de 1% destas tabelas (cerca de 30 delas)
  • Meu sistema também tem algumas tabelas adicionadas a este banco de dados que são exclusivas dele (cerca de 15)
  • Toda a lógica de negócio depende demais do estado destas tabelas do sistema legado
  • A maior parte da lógica de negócio se resume a adicionar, alterar ou excluir dados de algumas tabelas verificando dezenas de pré-condições em outras
  • Todas as queries precisam de ajustes muito finos para serem performáticas e também porque DBAs fazem auditorias regulares no BD e pressionam os desenvolvedores caso encontrem queries com execution plans ?menos do que perfeitos?

Por causa desses requisitos o design do sistema, e por consequência dos testes unitários, tem as seguintes restrições:

  • Não é possível usar um framework de persistência de objetos como o Hibernate pois:
  • A complexidade dos relacionamentos entre as tabelas dificulta o mapeamento objeto-relacional.
  • A grande quantidade de relacionamentos de vários tipos torna difícil o mapeamento de apenas um grupo de tabelas e mapear todo o schema não vale a pena.
  • Os desenvolvedores precisam ter controle total sobre o SQL das queries e os frameworks de persistência tendem a gerar SQL automaticamente com poucas opções de customização.
  • Várias queries precisam ser feitas por stored procedures já existentes do BD.
  • A inviabilidade de utilizar um framework de persistência deixa como única alternativa o uso de JDBC direto, dentro de DAOs.
  • O código dos DAOs contém a maior parte de toda a lógica de negócio, pois eles são responsáveis por inserir e alterar dados e fazer muitas e muitas verificações em várias tabelas.
  • Assim, os DAOs são os componentes mais suscetíveis a erro e os que mais deveriam ser testados, especialmente devido ao grande número de exceções que podem acontecer no JDBC.
  • Além disso existem muitas possibilidades de inconsistências no conjunto de tabelas que o sistema manipula e cada possibilidade dessas merece um cenário de teste que garanta o comportamento do sistema.

O grande problema aqui é realizar testes unitários nos DAOs que dependem de muitos estados diferentes do banco de dados.

Muitos diriam que o ideal seria utilizar uma ferramenta como DBUnit para inicializar o banco de dados com dados de teste em cada cenário, só que este tipo de operação tem um custo inviável em um banco de dados de 3.000 tabelas.
Inicializar apenas as tabelas alvo com dados de teste também fica difícil porque existem muitas constraints entre elas e outras tabelas fora do escopo do sistema. A única opção neste caso me parece ser desabilitar as constraints em uma versão de teste do BD.

Entretanto, depender de um Oracle para fazer testes unitários dos DAOs é algo que me desagrada profundamente, pois são bancos de dados difíceis de instalar e nem sempre você consegue colocar um na sua máquina de desenvolvimento.

Eu acho que eu deveria ser capaz de rodar testes unitários puramente em Java, na segurança e conforto da minha máquina, a qualquer momento, e ter um segundo conjunto de ?testes de integração? em que eu testaria meus DAOs contra um banco de dados real. Estes últimos eu rodaria com menos frequência.

Para conseguir isso, pensei seriamente em fazer mocks de java.sql.Connection, java.sql.PreparedStatement, java.sql.ResultSet e assim por diante, e assim simular as N condições-problema que podem aparecer no BD. Só que esta abordagem é também extremamente trabalhosa e produz um volume muito grande de código de teste que tem alto custo para ser mantido, mesmo utilizando JMock.

No final das contas eu não consigo chegar numa solução em que a implementação de testes unitários não seja extremamente trabalhosa e não consuma um período absurdo de tempo.

A viabilidade de testes não está em ser rápido para construir, e sim rápido para executar e ter os resultados. Teste gasta tempo para se fazer sim, é trabalhoso sim, e é muito importante sim, e a recompensa vem quando vc precisar fazer uma alteração no teu sistema e esta alteração pode causar um efeito colateral em outra parte do sistema.

[]'s

Eu acho que nesse caso você pode tentar fazer testes de mais alto nível (e.g. funcionais e de aceitação). Se conseguir fazer integrado também melhor ainda.

Eu sou um grande defensor de testes automatizados, como muitos aqui, mas tem situações onde o custo é inviável, como você mesmo descreveu. Sendo assim, acho melhor automatizar onde o custo compense. Sistemas desse tipo são realmente complicados.

Uma solução interessante seria você tentar de alguma forma isolar o banco de dados do seu sistema desse banco de dados legado e simular o comportamento esperado deste na hora que for fazer seus testes unitários.

Esse problema de banco de dados com zilhões de tabelas é bem típico de Empresas Bancárias e/ou Seguradoras. Já trabalhei em empresas de ambos os tipos e senti na pele o que você está passando. Em todas as vezes, procurei isolar ao máximo as tabelas em um schema separado, para ter um ambiente mais coeso para trabalhar.

Uma opção ao jdbc puro seria a utilização do Ibatis - http://ibatis.apache.org/

Com o ibatis se tem controle total sobre o SQL.

Já para o caso dos testes, acho que os cenários de testes poderiam ser montados em arquivos texto (csv por ex.), e teria uma classe que efetua a instanciação dos mocks para execução dos testes, dessa forma a cada novo cenário criado, bastaria incluir os dados no csv.

[quote=gcobr]
(…)
No final das contas eu não consigo chegar numa solução em que a implementação de testes unitários não seja extremamente trabalhosa e não consuma um período absurdo de tempo.[/quote]

Não ha duvida de que em sistemas orientados a banco de dados DAO é " the way to go". O que não ficou claro
no seu texto é até onde existe dependência do banco. Por exemplo, quando vc grava um certo registro vc fala que teste várias condições. Esse texte é feito pelo proprio dao no momento do insert ? Ou seja, vc executa um monte de queries ou procedures e depois se tudo deu certo vc finalmente dá o insert ?

Por outro lado, existe mapeamento OR no seu sistema ? Se sim, para quais tabelas ( as suas, as do banco legado , ou ambas ?)

Um transação implica em alterar tanto as tabelas legadas como as suas , ou as legadas são apenas de validação ?

Para conseguir um teste unitário os DAOs não ha outro jeito além de escrever e ler em um banco de testes.
Mas para verificar os contragimentos, tv vc não precise dos DAOs assim tanto e isso pode ser uma forma de isolar e ganhar poder de abstração para liberar os testes de escrever e ler do banco.

Por exemplo, um constangimento pode ser colocado em uma interface. uma implementação consulta o banco, outra não.
Se o objetivo é testar como o sistema reage quando o contragimento não é seguido não necessita de realmente fazer nada na classe além de retorna false ou lançar um erro

// no codigo de inicialização do teste
// verifica se os dados estão duplicados.
// o false simula um falha 
ConstrangimentoDeDuplicação constrang = new ConstrangimentoDeDuplicaçãoOffLine(false);

// no codigo de alguma classe
if (! constrang.accept(dados)){ // verifica se os dados
   // fluxo alternativo
}

// fluxo normal

[quote=wariows]A viabilidade de testes não está em ser rápido para construir, e sim rápido para executar e ter os resultados. Teste gasta tempo para se fazer sim, é trabalhoso sim, e é muito importante sim, e a recompensa vem quando vc precisar fazer uma alteração no teu sistema e esta alteração pode causar um efeito colateral em outra parte do sistema.
[]'s[/quote]

Você não considera o tempo e o custo de implementação deles um fator importante?

Nas vezes em que passei por estas situações eu não consegui convencer os gerentes dos respectivos projetos de que era importante investir mais tempo implementando testes do que o código principal. Mas eu juro que eu tentei!
Aliás, vejo esse como um dos principais entraves para a disseminação de testes automatizados. Milhares de gerentes vão preferir entregar sistemas meia-boca e meio-testados, com custo menor e uma qualidade “passável”.

Outra coisa que na minha opinião agrava ainda mais o problema é que:

Suponhamos que eu consiga convencer os gerentes de que é melhor a equipe investir mais tempo implementando testes do que o código em si.

Então, fazemos isso e conseguimos uma cobertura muito de testes automatizados (unitários, funcionais, de integração, etc.).

Aí tempos depois aparecem alterações a serem feitas e como temos todos aqueles testes prontos, já podemos ficar bem mais tranquilos, certo?? Eu acho que “nem tão certo assim” porque:

  • Quando alterarmos o código e reexecutarmos os testes, vamos ter vários deles quebrados.
  • Vamos então ter que investigar cada cenário para descobrir se ele não passou porque realmente quebramos algo no código ou porque o código do teste precisa também ser alterado.
  • Num sistema data-centric vamos levar mais tempo para corrigir os cenários de teste do que para alterar a funcionalidade em questão, e aí novamente o fator custo pesa bastante.

Vejo que hoje em dia o único argumento que temos para defender os testes automatizados é a qualidade que eles conferem ao nosso código, só que esse argumento é muito fraco diante de gerências de TI que precisam agilizar processos de negócios. Essas gerências sempre tem seu desempenho avaliado por seus superiores com base em critérios muito diferentes. Na maioria das vezes os critérios são baseados em custos, na agilidade no atendimento das demandas do negócio da empresa, e por aí vai …

Depois de escrever esse último parágrafo me bateu um desânimo. :cry:

quote=Emerson Macedo
Uma solução interessante seria você tentar de alguma forma isolar o banco de dados do seu sistema desse banco de dados legado e simular o comportamento esperado deste na hora que for fazer seus testes unitários.
(…)[/quote]
Concordo. Ainda não desisti de encontrar uma maneira elegante de fazer isso …

quote=Emerson Macedo
Esse problema de banco de dados com zilhões de tabelas é bem típico de Empresas Bancárias e/ou Seguradoras. Já trabalhei em empresas de ambos os tipos e senti na pele o que você está passando. Em todas as vezes, procurei isolar ao máximo as tabelas em um schema separado, para ter um ambiente mais coeso para trabalhar.[/quote]
Exato!! Meus casos também são de empresas desse tipo!!

Me parece que algumas versões do Oracle, como a 8i, venderam a rodo no Brasil.

Houve uma adoção em massa e hoje em dia temos milhares de empresas com sistemas de missão crítica ou muito crítica totalmente dependentes desses bancos de dados. (pior que isso só mainframe)

E o pior é que muitas dessas empresas também usaram tecnologias fraquinhas, como o Oracle Forms 6i, para a implementação das sua camada de aplicação. Já vi sistemas imensos assim, com todas as regras de negócio totalmente atreladas a centenas e centenas de forms. As redundâncias são alucinantes!!!

Convencer gerência só mesmo mostrando casos de sucesso de TDD:

Um dos motivos pelo qual nunca gostei muito de hibernate/JPA é porque ele se baseia em um mundo bonito onde a equipe de desenvolvimento tem total controle sobre o banco de dados. Ok, dá para flexibilizar aqui, mudar um pouquinho ali e tal para ajustar, mas quando você chega em um cenário com milhares de tabelas, schemas fora de controle, regras de integridade obscuras, uso intensivo de stored procedures e os problemas que você relacionou, ele cai por terra.

Infelizmente a sua situação não é a exceção, é a regra. E é por isso que uma significativa porção das boas práticas de desenvolvimento de software não são implementadas na prática. Muitas delas assumem que o desenvolvedor não tem limitações na hora de desenvolver software.

Você não tem como desacoplar o seu DAO da implemntação do BD, por isso que você não consegue testá-los independentemente.

Quando você vai pensar no DAO, você isola a sua aplicação do BD (o que obviamente não será nem um pouco fácil). Olhe para o BD como se ele fosse uma interface com uma implementação mágica e obscura de onde você conhece apenas a assinatura dos métodos. Não estou falando para você ver os DAOs como interfaces, mas ver o BD como interfaces. Então, você extrai um conjunto de métodos e interfaces que correspondam as operações que você faz no BD, levando em conta que haverá triggers, check-constraints, stored-procedures, efeitos colaterais estranhos, exceções bizarras e tudo mais, mas neste ponto, esqueça que trata-se de um BD, trate-o apenas como um conjunto de objetos quaisquer com uma porrada de métodos estranhos que você tem que usar. Simplifica essas interfaces até não ter mais nada que possa ser retirado delas. E então você adquire a interface do DAO, minimizando o acoplamento ao BD. Então, você poderá fazer uma outra implementação que é um mock dessas interfaces. E posteriormente, você faz uma implementação que acessa um BD de mock.

Você deve estar pensando que isso não é nada de novo, e de fato não é. Mas olhar para o sistema repensando e refatorando nesse modo pode lhe servir para tentar reduzir ao máximo o acoplamento ao BD, simplificando os DAOs que você tem atualmente, o que vai lhe ajudar a criar testes melhores e ter um acoplamento muito mais fraco.

Isso resolve o seu problema 100%? Não, não resolve. Pois a raiz do problema, que o BD e o DAO não podem ser desacoplados, não é resolvido. Mas, novamente reafirmo, isso ajuda bastante.

Esqueca cara, nada vai dar jeito nessa arquitetura atual…

E no proximo sistema a ser desenvolvido relute ao maximo em fazer integracao via banco de dados. So entao vc podera utilizar e se beneficiar das boas praticas de desenvolvimento de software OO.

Respondendo ao sergiotaborda:

Sim, esse é um dos casos de uso.
Se uma das verificações falhar o método do DAO que está fazendo todas essas coisas vai lançar uma exceção de aplicação.

Não existe mapeamento OR, para nenhuma. Não me parece interessante usar só para as minhas.

Sim, altera todas.

Desculpa a ignorância, mas o que é um ‘constrangimento’?

[quote=gcobr]
Desculpa a ignorância, mas o que é um ‘constrangimento’?[/quote]

O sistema ou cada objeto têm um espaço de estados que é o conjunto de todos os estados possíveis para o objeto
Por exemplo um inteiro pode ter qualquer valor entre um máximo e um mínimo. Mas quando , para efeitos de algum processo,
o objeto não pode ter um certo conjunto de estados, então o espaço de estados original está constrangido ( constranger : “dificultar os movimentos; apertar;oprimir; obrigar, forçar;”). Por exemplo em uma aplicação que calcule a distancia entre estrelas em anos-luz o numero não poderá ser negativo. Ou seja, de todos os valores (estados) possiveis para o inteiro apenas a parte positiva é usada.

Quando você faz uma validação isso é porque existe uma regra que os dados têm que obedecer. Se existe regra é porque existe um constrangimento dos valores que os dados podem ter.

se o seu sistema não tem OR isso significa que vc não trabalha orientado a objetos. O seu sistema é um sistema puramente processual. A menos que vc injete algum nivel de indireção ( encapsulamneto) de forma a poder plugar logicas diferentes em pontos difernentes do sistema é impossivel criar uma boa bateria de testes. É que “teste” implica automáticamente “ambiente controlado”. E “ambiente controlado” implica em que vc tenha mais do que um ambiente possivel. E isso é , básicamente impossivel (ok, muito, muito , muito complexo e dificil e caro) sem uma estrutura OO.

É melhor escolher outra palavra no lugar de constrangimento, Sérgio.

Pelo menos aqui no Brasil, a primeira coisa que pensamos da palavra constrangimento é violência emocional, embaraço e acanhamento; conotações muito mais fortes que “dificultar os movimentos”.

Neste contexto, as palavras Restrição ou Limitação caem bem melhor.

Existem casos em que você não consegue testar uma classe unitariamente, geralmente porque criar um mock para suas dependências é quase que impossível. Neste caso:

  • Tenha testes de integração para esta classe
  • Como regra geral não coloque qualquer tipo de regra nesta

Eu tenho um ambiente parecido, mas em menor escala.

Eu tenho um banco de dados de teste com a mesma estrutura, mas menos dados, em que a cada teste, eu reseto o estado do banco.

Dependendo do tamanho dao sua base fica inviável, mas por enquanto está funcionando.

[quote=Bruno Laturner]É melhor escolher outra palavra no lugar de constrangimento, Sérgio.

Pelo menos aqui no Brasil, a primeira coisa que pensamos da palavra constrangimento é violência emocional, embaraço e acanhamento; conotações muito mais fortes que “dificultar os movimentos”.

Neste contexto, as palavras Restrição ou Limitação caem bem melhor.[/quote]

  1. O contexto é tecnico, não ha como interpretar constrangimento como sinonimo disso que vc falou.
  2. Isso que vc falou é chamado de constrangimento porque em alguma medida ou forma encaixa com a definição de
    constranger :“dificultar os movimentos; apertar;oprimir; obrigar, forçar;”. Ou seja, a palavra “constragimento” não tem nenhum
    significa essencial associado ao que vc falou.
  3. Nem limitação nem restrição definem a mesma coisa porque o sentido está relacionado a “forçar” e “dificultar”. Os objetos que verificam regras de validação forçam um certo estado para os objetos: eles constrangem o estado do objeto.
  4. Constrain é o temo em inglês do qual Constrangimento é a tradução direta. E embora se define como uma restrição ou limitação é “constrain” que é a palavra técnica. Tudo bem, da próxima escrevo em ingles :?