• Nenhum resultado encontrado

Testabilidade

No documento Padrões de testes automatizados (páginas 191-195)

Testabilidade de software mede a facilidade da escrita de testes de qualidade dentro de um contexto [142]. Testar um sistema ou uma funcionalidade nem sempre é trivial, seja por meio de testes manuais ou automatizados. Primeiramente, tem de ser possível controlar o software, isto é, executá-lo de acordo com as necessidades do teste. Posteriormente, é necessário observar os efeitos colaterais causados por uma determinada execução que serão comparados com valores esperados.

Para que seja possível controlar e observar um sistema apropriadamente é necessário que ele esteja bem modularizado e isolado, pois, caso contrário, os testes podem ser difíceis ou até impossíveis de serem realizados. Testes complicados de serem criados ou mantidos tendem a ser pouco legíveis e muito suscetíveis a erros. Tudo isso aumenta o custo-benefício da automação de testes.

Testabilidade não é uma métrica intrínseca ao sistema, como total de linhas de código, número de classes etc. É necessário medir diversos fatores para então calcular o grau de testabilidade de acordo com alguma fórmula matemática baseada em determinações subjetivas. Por isso, é importante ter cuidado ao interpretar esta métrica, já que ela pode ser mais ou menos apropriada para um contexto específico. Também é preciso cautela ao comparar o grau de testabilidade entre sistemas que possuem contextos diferentes.

A Figura 10.4 mostra um exemplo do grau de testabilidade de um módulo do software Eclipse me- dido com a ferramenta Testability-Explorer [69]. A ferramenta analisa o código-fonte de cada classe Java em busca de variáveis de classe mutáveis, incoerências segundo a Lei de Demeter3[88] e classes

que não permitem injetar dependências. As informações coletadas são convertidas em um grau de testa- bilidade que representa custo para se testar uma classe do sistema, sendo assim, quanto maior o custo, pior.

Figura 10.4: Grau de testabilidade do módulo Workbench do software Eclipse, medido com a ferramenta Testability-Explorer.

As classes que possuem boa ou excelente testabilidade são mais fáceis de testar automaticamente, enquanto as que possuem baixa testabilidade (Needs Work) requerem mais atenção. Pode ser bem com- plexo criar testes para as classes com baixa testabilidade, pois, geralmente, é necessário uma grande quantidade de código para preparar o ambiente de testes apropriadamente. Em algumas situações, pode até ser fácil de implementar os testes, mas é provável que eles contenham antipadrões, tais como testes lentos, intermitentes e frágeis.

3A Lei de Demeter propõe um estilo de design em que cada unidade deve ter conhecimentos limitados sobre outras

unidades. Uma unidade só deve trocar informações com suas dependências imediatas. Este princípio é particularmente útil para criar sistemas orientados a objetos com baixo acoplamento.

10.3.1 Padrões e Antipadrões Influenciam a Testabilidade

A testabilidade de um sistema está diretamente relacionada com o bom design [117, 97, 27, 78]. O grau de testabilidade pode ser avaliado a partir de boas práticas de modularização e de programação orientada a objetos, que ajudam a tornar os sistemas mais simples e coesos, e, consequentemente, os testes ficam mais claros e fáceis de implementar.

Um dos fatores mais comuns que afetam a testabilidade do sistema está relacionado com a im- plementação do construtor de objetos. Objetos que são difíceis de criar e de configurar dificultam a criação de testes isolados. Existem inúmeros antipadrões não nomeados que prejudicam a testabilidade de um sistema. A Figura 10.5 apresenta exemplo de construtores escritos em Java com alguns destes antipadrões. Para facilitar, os comentários de cada exemplo estão nas próprias figuras.

Já a Figura 10.6 mostra objetos que são fáceis de serem criados e isolados por meio do padrão Injeção de Dependência [113]. Quando a criação dos objetos é mais complexa, é recomendado que o processo de criação seja isolado em objetos próprios, através do uso de padrões de projeto de criação, tais como Builder, Factory e Prototype [61].

A definição das responsabilidades dos objetos e da forma como eles irão interagir determinam o designde um sistema orientado a objetos. Isso é uma das tarefas mais difíceis e delicadas da orientação a objetos, pois qualquer falha pode comprometer a manutenibilidade e a testabilidade. A Figura 10.7 apresenta um método pouco coeso que viola a Lei de Demeter [88], portanto torna os testes difíceis de serem isolados. A Figura 10.8 apresenta como seria o método refatorado para aumentar a coesão e testabilidade do sistema.

Uma das boas práticas de orientação a objetos é definir uma responsabilidade por classe. Esse padrão torna as classes fáceis de serem implementadas, entendidas e testadas. Quando uma classe possui muitas responsabilidades, o conjunto de casos de testes para testá-la tende a aumentar consideravelmente, já que as combinações de dados de entrada podem aumentar fatorialmente. Além disso, os testes tendem a ter a legibilidade prejudicada. Quanto mais complexa for uma classe, maior serão os métodos de set up e mais verificações são necessárias por casos de teste.

Existem vários indícios que ajudam a identificar se uma classe está realizando mais tarefas do que deveria. O primeiro deles se dá pela facilidade de entendimento da classe. Outros indícios são os atributos da classe raramente usados, que podem indicar que ela deve ser repartida entre outras menores. Mais um indício comum é o uso de nomes de classes e variáveis muito genéricas, tais como manager, context, environment, principal, container, runner etc. Devido ao caráter extremamente abstrato dessas classes, é coerente que muitas responsabilidades sejam associadas a elas.

Ainda, a modelagem incorreta de heranças entre classes, por exemplo, que ferem o Princípio de Sub- stituição de Liskov [89], podem tornar os testes difíceis de serem modularizados e, consequentemente, propiciar a replicação de código-fonte (vide Seção 6.3.3).

Outro aspecto que prejudica a automação de testes são os estados globais mutáveis, tais como var- iáveis globais e classes com o padrão Singleton [61]. Estados globais são acessíveis a vários casos de testes, portanto, os testes não ficam completamente isolados. Isso significa que se um caso de teste alterar um valor global, outros testes poderão falhar.

Uma solução que torna os testes mais complexos e lentos, mas que pode resolver o problema de maneira padronizada, é utilizar os métodos de set up para definir o estado inicial das variáveis globais antes da execução dos testes. Contudo, esta solução requer que os testes sejam executados individual- mente e sequencialmente, o que inviabiliza o uso de ferramentas de otimização de testes que executam paralelamente os casos de testes.

Esses padrões e antipadrões citados compõem apenas uma pequena parcela das inúmeras formas de implementação que influenciam a testabilidade do sistema. Ainda, existem diversos fatores próprios de cada linguagem de programação que também podem facilitar ou dificultar os testes de software. O que é válido para todas elas é que definir e escrever os casos de testes antes da própria implementação propicia a criação de código altamente testável, já que o código do sistema se adapta aos testes, e não o contrário.

1 public class ObjetoComContrutorDeBaixaTestabilidade1 {

2 // Não da para isolar (classe não tem método setter) 3 private Dependencia dependencia = new Dependencia() ;

4 public ObjetoComContrutorDeBaixaTestabilidade1() {

5 } 6 } 7

8 public class ObjetoComContrutorDeBaixaTestabilidade2 {

9 private Dependencia dependencia;

10 public ObjetoComContrutorDeBaixaTestabilidade2() {

11 // Não da para isolar (classe não tem método setter)

12 dependencia = new Dependencia() ;

13 } 14 }

15

16 public class ObjetoComContrutorDeBaixaTestabilidade3 {

17 private Dependencia dependencia;

18 public ObjetoComContrutorDeBaixaTestabilidade3() {

19 // Arcabouços que usam reflexão para criação de objetos precisam do construtor

padrão.

20 // Mas é necessário cuidados porque o objeto não está inicializado 21 // apropriadamente enquanto não forem injetadas as dependências. 22 }

23 public void setDependencia(Dependencia dependencia) {

24 this.dependencia = dependencia;

25 } 26 }

27

28 public class ObjetoComContrutorDeBaixaTestabilidade4 {

29 public ObjetoComContrutorDeBaixaTestabilidade4() {

30 // Muita lógica no contrutor pode prejudicar a testabilidade.

31 // É necessário um trabalho adicional para criar o objeto apropriadamente. 32 // Use algum padrão de projeto de criação de objetos.

33 if (x == 3) { ... } 34 for(int i = 0; i < n; i++) { ... } 35 while(true) { ... } 36 } 37 } 38

39 public class ObjetoComContrutorDeBaixaTestabilidade5 {

40 public ObjetoComContrutorDeBaixaTestabilidade5(Dependencia dependencia) {

41 // Testes não ficam isolados da classe DependenciaGlobal

42 DependenciaGlobal.metodoGlobal(dependencia);

43 }

44 }

Figura 10.5: Exemplo de implementação de construtores que tornam os objetos difíceis de serem testa- dos.

1 public class ObjetoComContrutorDeAltaTestabilidade1 {

2 private List list;

3 public ObjetoComContrutorDeAltaTestabilidade1() {

4 // Detalhes internos do objeto podem ser instanciados no contrutor 5 // lista geralmente é uma exceção

6 list = new ArrayList() ;

7 } 8 } 9

10 public class ObjetoComContrutorDeAltaTestabilidade2 {

11 private Dependencia dependencia;

12 public ObjetoComContrutorDeAltaTestabilidade2(Dependencia dependencia) {

13 this.dependencia = dependencia; // Possível injetar dependência

14 } 15 }

Figura 10.6: Exemplo de implementação de construtores que tornam os objetos fáceis de serem testados.

1 public class A { 2

3 public metodo(B b) {

4 // Violação da Lei de Demeter:

5 // Objeto A conhece toda hierarquia de classes do objeto B 6 // Difícil de isolar: A pergunta para B por informações

7 var estado = b.getObjetoC() .getObjetoD() .getObjetoE() .getEstado() ;

8 // ... 9 }

10 }

Figura 10.7: Exemplo de implementação de métodos que são difíceis de serem testados.

1 public class A {

2

3 // Não pergunte, diga!

4 public metodo(E e) { // Possível isolar dependências

5 var estado = e.getEstado() ; // A só conhece E

6 // Objetos B, C e D são dispensáveis 7 // ...

8 } 9 }

Por isso é recomendado o uso de TFD, TDD e BDD para criação de sistemas com alta testabilidade. 10.3.2 Quando Utilizar

Testabilidade pode ser útil para analisar riscos e estabelecer quais pontos são mais críticos para se testar, principalmente para equipes que estão começando a aplicar testes automatizados em sistemas legados. Contudo, ela também é importante para acompanhar a qualidade dos testes automatizados, pois baixa testabilidade (ou alto custo para se testar) implica testes com muitos antipadrões.

Essa métrica também auxilia na identificação dos módulos do sistema em teste que precisam ser refatorados, isto é, módulos que não possuem um bom design. Dessa forma, a análise da testabilidade do sistema, antes de adicionar novas funcionalidades, é importante, pois ajuda a prevenir que uma nova porção de código seja inserida sobre uma arquitetura confusa, que pode tornar a implementação mais complicada, além de piorar o design do sistema.

TDD e TFD não só proporcionam alta cobertura dos testes, como também favorecem para que o sistema seja testável, pois a criação dos testes a priori implica que o código do sistema se adapte aos testes, e não o contrário [16]. Portanto, testabilidade também é útil para acompanhar e verificar se o desenvolvimento com TDD ou TFD está sendo feito apropriadamente.

No documento Padrões de testes automatizados (páginas 191-195)