
Olá! Meu nome é Yuri Skvortsov , nossa equipe está envolvida em testes automatizados em Rosbank. Uma de nossas tarefas é desenvolver ferramentas para automatizar testes funcionais.
Neste artigo, quero falar sobre uma solução que foi concebida como uma pequena utilidade auxiliar para resolver outros problemas, mas que acabou se tornando uma ferramenta independente. Estamos falando sobre o framework Fast-Unit, que permite escrever testes de unidade em um estilo declarativo e transforma o desenvolvimento de testes de unidade em um construtor de componentes. O projeto foi desenvolvido principalmente para testar nosso produto principal - Tladianta - uma estrutura BDD unificada para testar 4 plataformas: Desktop, Web, Mobile e Rest.
Para começar, testar uma estrutura de automação não é uma tarefa comum. No entanto, neste caso, não era parte de um projeto de teste, mas um produto independente, então rapidamente percebemos a necessidade de unidades.
No primeiro estágio, tentamos usar ferramentas prontas como assertJ e Mockito, mas rapidamente encontramos algumas das características técnicas de nosso projeto:
- Tladianta já usa JUnit4 como uma dependência, o que torna difícil usar uma versão diferente de JUnit e complica Antes;
- Tladianta contém componentes para trabalhar com diferentes plataformas, tem muitas entidades que são “extremamente próximas” em termos de funcionalidade, mas com diferentes hierarquias e diferentes comportamentos;
- «» ( ) ;
- , , , , ;
- - (, Appium , , , );
- , : Mockito .
Inicialmente, quando acabamos de aprender a substituir o driver, criar elementos Selenium falsos e escrever a arquitetura básica para o equipamento de teste, os testes eram assim:
@Test
public void checkOpenHint() {
ElementManager.getInstance().register(xpath,ElementManager.Condition.VISIBLE,
ElementManager.Condition.DISABLED);
new HintStepDefs().open(("");
assertTrue(TestResults.getInstance().isSuccessful("Open"));
assertTrue(TestResults.getInstance().isSuccessful("Click"));
}
@Test
public void checkCloseHint() {
ElementManager.getInstance().register(xpath);
new HintStepDefs().close("");
assertTrue(TestResults.getInstance().isSuccessful("Close"));
assertTrue(TestResults.getInstance().isSuccessful("Click"));
}
Ou mesmo assim:
@Test
public void fillFieldsTestOld() {
ElementManager.getInstance().register(ElementManager.Type.CHECK_BOX,"//check-box","",
ElementManager.Condition.NOT_SELECTED);
ElementManager.getInstance().register(ElementManager.Type.INPUT,"//input","");
ElementManager.getInstance().register(ElementManager.Type.RADIO_GROUP,
"//radio-group","");
DataTable dataTable = new Cucumber.DataTableBuilder()
.withRow("", "true")
.withRow("", "not selected element")
.withRow(" ", "text")
.build();
new HtmlCommonSteps().fillFields(dataTable);
assertEquals(TestResults.getInstance().getTestResult("set"),
ElementProvider.getInstance().provide("//check-box").force().getAttribute("test-id"));
assertEqualsTestResults.getInstance().getTestResult("sendKeys"),
ElementProvider.getInstance().provide("//input").force().getAttribute("test-id"));
assertEquals(TestResults.getInstance().getTestResult("selectByValue"),
ElementProvider.getInstance().provide("//radio-group").force().getAttribute("test-id"));
}
Não é difícil encontrar o que está sendo testado no código acima, bem como entender as verificações, mas existe uma quantidade enorme de código. Se você incluir software para verificar e descrever erros, será muito difícil de ler. E estamos apenas tentando verificar se o método foi chamado no objeto desejado, enquanto a lógica real das verificações é extremamente primitiva. Para escrever tal teste, você precisa saber sobre ElementManager, ElementProvider, TestResults, TickingFuture (um wrapper para implementar uma mudança no estado de um elemento durante um determinado tempo). Esses componentes eram diferentes em diferentes projetos, não tivemos tempo para sincronizar as mudanças.
Outro desafio foi o desenvolvimento de algum padrão. Nossa equipe conta com a vantagem dos automatizadores, muitos de nós não temos experiência suficiente no desenvolvimento de testes unitários e, embora, à primeira vista, seja simples, a leitura do código uns dos outros é bastante trabalhosa. Tentamos liquidar a dívida técnica com rapidez suficiente e, quando surgiram centenas desses testes, tornou-se difícil manter. Além disso, o código acabou ficando sobrecarregado com configurações, as verificações reais foram perdidas e as correias grossas levaram ao fato de que, em vez de testar a funcionalidade do framework, nossas próprias correias foram testadas.
E quando tentamos transferir os desenvolvimentos de um módulo para outro, ficou claro que precisávamos trazer a funcionalidade geral. Naquele momento, surgiu a ideia não só de criar uma biblioteca com as melhores práticas, mas também de criar um processo de desenvolvimento de uma unidade única dentro desta ferramenta.
Mudando a filosofia
Se você olhar o código como um todo, verá que muitos blocos de código são repetidos “sem significado”. Testamos métodos, mas usamos construtores o tempo todo (para evitar a possibilidade de algum erro ser armazenado em cache). A primeira transformação - movemos a validação e geração de instâncias testadas para anotações.
@IExpectTestResult(errDesc = " set", value = "set",
expected = "//check-box", convertedBy = Converters.XpathToIdConverter.class, soft = true)
@IExpectTestResult(errDesc = " sendKeys", value = "sendKeys",
expected = "//input", convertedBy = Converters.XpathToIdConverter.class, soft = true)
@IExpectTestResult(errDesc = " selectByValue", value = "selectByValue",
expected = "//radio-group", convertedBy = Converters.XpathToIdConverter.class, soft = true)
@Test
public void fillFieldsTestOld() {
ElementManager.getInstance().register(ElementManager.Type.CHECK_BOX, "//check-box", "",
ElementManager.Condition.NOT_SELECTED);
ElementManager.getInstance().register(ElementManager.Type.INPUT, "//input", "");
ElementManager.getInstance().register(ElementManager.Type.RADIO_GROUP,
"//radio-group", "");
DataTable dataTable = new Cucumber.DataTableBuilder()
.withRow("", "true")
.withRow("", "not selected element")
.withRow(" ", "text")
.build();
runTest("fillFields", dataTable);
}
O que mudou?
- As verificações foram delegadas a um componente separado. Agora você não precisa saber como os itens são armazenados, teste os resultados.
- : errDesc , .
- , , , – runTest, , .
- .
- - , .
Gostamos dessa forma de notação e decidimos simplificar outro componente complexo da mesma maneira - a geração de elementos. A maioria de nossos testes são dedicados a etapas prontas, e devemos ter certeza de que funcionam corretamente, porém, para tais verificações, é necessário “lançar” completamente o aplicativo falso e preenchê-lo com elementos (lembre-se que estamos falando de Web, Desktop e Mobile, as ferramentas para as quais diferem fortemente).
@IGenerateElement(type = ElementManager.Type.CHECK_BOX)
@IGenerateElement(type = ElementManager.Type.RADIO_GROUP)
@IGenerateElement(type = ElementManager.Type.INPUT)
@Test
@IExpectTestResult(errDesc = " set", value = "set",
expected = "//check-box", convertedBy = Converters.XpathToIdConverter.class, soft = true)
@IExpectTestResult(errDesc = " sendKeys", value = "sendKeys",
expected = "//input", convertedBy = Converters.XpathToIdConverter.class, soft = true)
@IExpectTestResult(errDesc = " selectByValue", value = "selectByValue",
expected = "//radio-group", convertedBy = Converters.XpathToIdConverter.class, soft = true)
public void fillFieldsTest() {
DataTable dataTable = new Cucumber.DataTableBuilder()
.withRow("", "true")
.withRow("", "not selected element")
.withRow(" ", "text")
.build();
runTest("fillFields", dataTable);
}
Agora o código de teste tornou-se completamente o modelo, os parâmetros estão claramente visíveis e toda a lógica é movida para os componentes do modelo. As propriedades padrão tornaram possível remover linhas vazias e deram amplas oportunidades para sobrecarga. Este código está quase em linha com a abordagem, pré-condição, validação e ação do BDD. Além disso, todas as ligações foram retiradas da lógica dos testes, você não precisa mais saber sobre gerenciadores, armazenamentos de resultados de testes, o código é simples e fácil de ler. Como as anotações em Java quase não são personalizáveis, introduzimos um mecanismo para conversores que podem receber o resultado final de uma string. Este código não só verifica o fato de chamar o método, mas também o id do elemento que o executou. Quase todos os testes que existiam naquela época (mais de 200 unidades) foram rapidamente transferidos para essa lógica, trazendo-os para um único modelo. Os testes se tornaram o que deveriam ser - documentação,não código, então chegamos à declaratividade. É esta abordagem que formou a base da Fast-Unit - declaratividade, testes de autodocumentação e isolamento da funcionalidade testada, o teste é totalmente dedicado à verificação de um método de teste.
Continuamos a desenvolver
Agora era necessário adicionar a capacidade de criar tais componentes de forma independente no âmbito dos projetos, adicionar a capacidade de controlar a sequência de sua operação. Para fazer isso, desenvolvemos o conceito de fases: ao contrário do Junit, todas essas fases existem independentemente dentro de cada teste e são executadas no momento em que o teste é executado. Como uma implementação padrão, estabelecemos o seguinte ciclo de vida:
- Package-generate - processando anotações relacionadas a package-info. Os componentes associados a eles fornecem downloads de configuração e preparação geral do chicote.
- Gerar classe - processar anotações associadas a uma classe de teste. As ações de configuração relacionadas à estrutura são executadas aqui, adaptando-a à ligação preparada.
- Gerar - anotações de processamento associadas ao próprio método de teste (ponto de entrada).
- Teste - preparando uma instância e executando o método em teste.
- Assert - executando verificações.
As anotações a serem processadas são descritas mais ou menos assim:
@Target(ElementType.PACKAGE) //
@IPhase(value = "package-generate", processingClass = IStabDriver.StabDriverProcessor.class,
priority = 1) // ( )
public @interface IStabDriver {
Class<? extends WebDriver> value(); // ,
class StabDriverProcessor implements PhaseProcessor<IStabDriver> { //
@Override
public void process(IStabDriver iStabDriver) {
//
}
}
}
O recurso Fast-Unit é que o ciclo de vida pode ser substituído por qualquer classe - ele é descrito pela anotação ITestClass, que é projetada para indicar a classe e as fases em teste. A lista de fases é especificada simplesmente como um array de string, permitindo a mudança de composição e a sequência de fases. Os métodos que tratam das fases também são encontrados por meio de anotações, portanto, é possível criar o manipulador necessário em sua classe e marcá-lo (além disso, a substituição dentro da classe está disponível). Uma grande vantagem foi que essa separação nos permitiu dividir o teste em camadas: se ocorrer um erro no teste finalizado durante a fase de geração ou geração do pacote, o chicote de teste está danificado. Se gerar classe - há problemas nos mecanismos de configuração do framework. Se dentro da estrutura de teste houver um erro na funcionalidade testada.A fase de teste pode fornecer tecnicamente erros tanto na vinculação quanto na funcionalidade testada, portanto, agrupamos os possíveis erros de vinculação em um tipo especial - InnerException.
Cada fase é isolada, ou seja, não depende e não interage diretamente com outras fases, a única coisa que se passa entre as fases são os erros (a maioria das fases será ignorada se ocorrer um erro nas anteriores, mas isso não é necessário, por exemplo, a fase de afirmação funcionará mesmo assim).
Aqui, provavelmente, a questão já surgiu: de onde vêm as instâncias de teste. Se o construtor estiver vazio, é óbvio: usando a API Reflection, você simplesmente cria uma instância da classe em teste. Mas como você pode passar parâmetros nesta construção ou configurar a instância depois que o construtor foi acionado? O que fazer se o objeto estiver sendo construído pelo construtor ou, em geral, for um teste estático? Para isso, foi desenvolvido o mecanismo de provedores, que escondem atrás de si a complexidade do construtor.
Parametrização padrão:
@IProvideInstance
CheckBox generateCheckBox() {
return new CheckBox((MobileElement) ElementProvider.getInstance().provide("//check-box")
.get());
}
Sem parâmetros - sem problemas (estamos testando a classe CheckBox e registrando um método que criará instâncias para nós). Visto que o provedor padrão é substituído aqui, não há necessidade de adicionar nada nos próprios testes, eles usarão automaticamente esse método como uma fonte. Este exemplo ilustra claramente a lógica da unidade rápida - ocultamos o complexo e o desnecessário. Do ponto de vista do teste, não importa como e de onde vem o elemento móvel empacotado com a classe CheckBox. Tudo o que importa para nós é que haja algum objeto CheckBox que atenda aos requisitos especificados.
Injeção automática de argumentos: vamos supor que temos um construtor como este:
public Mask(String dataFormat, String fieldFormat) {
this.dataFormat = dataFormat;
this.fieldFormat = fieldFormat;
}
Em seguida, um teste dessa classe usando injeção de argumento terá a seguinte aparência:
Object[] dataMask={"_:2_:2_:4","_:2/_:2/_:4"};
@ITestInstance(argSource = "dataMask")
@Test
@IExpectTestResult(errDesc = " ", value = FAST_RESULT,
expected = "12/10/2012")
public void convert() {
runTest("convert","12102012");
}
Provedores nomeados
Finalmente, se vários provedores forem necessários, usamos a vinculação de nomes, não apenas ocultando a complexidade do construtor, mas também revelando seu real significado. O mesmo problema pode ser resolvido assim:
@IProvideInstance("")
Mask createDataMask(){
return new Mask("_:2_:2_:4","_:2/_:2/_:4");
}
@ITestInstance("")
@Test
@IExpectTestResult(errDesc = " ", value = FAST_RESULT,
expected = "12/10/2012")
public void convert() {
runTest("convert","12102012");
}
IProvideInstance e ITestInstance são anotações associadas que permitem que você diga ao método onde obter a instância em teste (para estática, ele simplesmente retorna nulo, uma vez que esta instância é finalmente usada por meio da API Reflection). A abordagem do provedor fornece muito mais informações sobre o que realmente está acontecendo no teste, substituindo a chamada ao construtor por alguns parâmetros com texto que descreve as pré-condições, portanto, se o construtor mudar repentinamente, teremos apenas que corrigir o provedor, mas o teste permanecerá inalterado até que a funcionalidade real mude. Se, durante a revisão, você encontrar vários fornecedores, prestará atenção na diferença entre eles e, portanto, nas peculiaridades do comportamento do método testado. Mesmo sem conhecer a estrutura, mas apenas conhecendo os princípios de operação da Fast-Unit,o desenvolvedor será capaz de ler o código de teste e entender o que o método testado faz.
Conclusões e resultados
Nossa abordagem acabou apresentando muitas vantagens:
- Fácil portabilidade de teste.
- Escondendo a complexidade dos bindings, a possibilidade de refatorá-los sem quebrar os testes.
- Compatibilidade com versões anteriores garantida - as alterações nos nomes dos métodos serão registradas como erros.
- Os testes se transformaram em documentação bastante detalhada para cada método.
- A qualidade das inspeções melhorou significativamente.
- O desenvolvimento do teste de unidade tornou-se um processo pipeline, e a velocidade de desenvolvimento e revisão aumentou significativamente.
- Estabilidade dos testes desenvolvidos - embora o framework e a própria Fast-Unit estejam em desenvolvimento ativo, não há degradação dos testes
Apesar da aparente complexidade, fomos capazes de implementar essa ferramenta rapidamente. Agora a maioria das unidades estão escritas nele e já confirmaram sua confiabilidade com uma migração bastante complexa e volumosa, eles foram capazes de identificar defeitos bastante complexos (por exemplo, na espera por elementos e verificações de texto). Conseguimos eliminar rapidamente o déficit técnico e estabelecer um trabalho eficaz com as unidades, tornando-as parte integrante do desenvolvimento. Agora estamos considerando opções para uma implementação mais ativa desta ferramenta em outros projetos fora de nossa equipe.
Problemas e planos atuais:
- , . , ( - ).
- .
- .
- , -.
- Fast-Unit junit4, junit5 testng