
Provavelmente, em algum lugar existe um artigo ideal que imediatamente e completamente revela o tópico da arquitetura de teste, fácil de escrever, ler e suportar, e de forma que seja compreensível para iniciantes, com exemplos de implementação e áreas de aplicação. Gostaria de apresentar a minha visão deste “artigo ideal” no formato que sonhei, só depois de receber a primeira tarefa “escrever autotestes”. Para fazer isso, vou falar sobre abordagens bem conhecidas e não tão conhecidas para autotestes da web, por que, como e quando usá-los, bem como sobre soluções bem-sucedidas para armazenar e criar dados.
Olá, Habr! Meu nome é Diana, sou a chefe do grupo de testes de interface do usuário, tenho automatizado testes de web e desktop por cinco anos. Os exemplos de código serão em java e para a web, mas, na prática, foi testado, as abordagens são aplicáveis a python com desktop.
No começo era ...
No começo havia uma palavra, e havia muitas palavras, e elas preenchiam todas as páginas uniformemente com código, independente de suas arquiteturas e princípios DRY (não se repita - não há necessidade de repetir o código que você já escreveu três parágrafos acima).
Folha
Na verdade, a arquitetura do "footcloth", também conhecido como "sheet", também conhecido como código não estruturado empilhado em uma pilha que preenche uniformemente a tela, não é tão ruim e é bastante aplicável nas seguintes situações:
- Um clique rápido em três linhas (ok, duzentos e três) para projetos muito pequenos;
- Para exemplos de código na mini demo;
- Para o primeiro código no estilo "Hello Word" entre autotestes.
O que você precisa fazer para obter a arquitetura do lençol? basta escrever todo o código necessário em um arquivo, uma tela comum.
import com.codeborne.selenide.Condition;
import com.codeborne.selenide.WebDriverRunner;
import org.testng.annotations.Test;
import static com.codeborne.selenide.Selenide.*;
public class RandomSheetTests {
@Test
void addUser() {
open("https://ui-app-for-autotest.herokuapp.com/");
$("#loginEmail").sendKeys("test@protei.ru");
$("#loginPassword").sendKeys("test");
$("#authButton").click();
$("#menuMain").shouldBe(Condition.appear);
$("#menuUsersOpener").hover();
$("#menuUserAdd").click();
$("#dataEmail").sendKeys("mail@mail.ru");
$("#dataPassword").sendKeys("testPassword");
$("#dataName").sendKeys("testUser");
$("#dataGender").selectOptionContainingText("");
$("#dataSelect12").click();
$("#dataSelect21").click();
$("#dataSelect22").click();
$("#dataSend").click();
$(".uk-modal-body").shouldHave(Condition.text(" ."));
WebDriverRunner.closeWebDriver();
}
}
Se você está apenas começando a se familiarizar com os autotestes, então a "planilha" já é suficiente para completar uma tarefa de teste simples, especialmente se você demonstrar bom conhecimento de design de teste e boa cobertura. Mas isso é muito fácil para projetos de grande escala, então se você tem ambições, mas não tem tempo para executar cada caso de teste idealmente, então pelo menos seu gita deve ter um exemplo de uma arquitetura mais complexa.
PageObject
Ouviu rumores de que o PageObject está obsoleto? Você simplesmente não sabe como cozinhar!
A principal unidade de trabalho neste padrão é uma "página", ou seja, um conjunto completo de elementos e ações com eles, por exemplo, MenuPage - uma classe que descreve todas as ações com um menu, ou seja, cliques em guias, expandindo itens suspensos e assim por diante.

É um pouco mais difícil compor um PageObject para a janela modal (para abreviar "modal") de criação do objeto. O conjunto de campos de classe é claro: todos os campos de entrada, caixas de seleção, listas suspensas; e para métodos existem duas opções: você pode fazer ambos os métodos universais "preencher todos os campos modais", "preencher todos os campos modais com valores aleatórios", "verificar todos os campos do modalk" e métodos separados "preencher o nome", "verificar o nome", "Preencha a descrição" e assim por diante. O que usar em um caso particular é determinado por prioridades - a abordagem "um método para todo o modal" aumenta a velocidade de gravação de um teste, mas em comparação com a abordagem "um método para cada campo", ela perde muito na legibilidade do teste.
Exemplo
Vamos compor um objeto de página comum de criação de usuários para os dois tipos de testes:
:
:
. : , , , , — . , , , .
public class UsersPage {
@FindBy(how = How.ID, using = "dataEmail")
private SelenideElement email;
@FindBy(how = How.ID, using = "dataPassword")
private SelenideElement password;
@FindBy(how = How.ID, using = "dataName")
private SelenideElement name;
@FindBy(how = How.ID, using = "dataGender")
private SelenideElement gender;
@FindBy(how = How.ID, using = "dataSelect11")
private SelenideElement var11;
@FindBy(how = How.ID, using = "dataSelect12")
private SelenideElement var12;
@FindBy(how = How.ID, using = "dataSelect21")
private SelenideElement var21;
@FindBy(how = How.ID, using = "dataSelect22")
private SelenideElement var22;
@FindBy(how = How.ID, using = "dataSelect23")
private SelenideElement var23;
@FindBy(how = How.ID, using = "dataSend")
private SelenideElement save;
@Step("Complex add user")
public UsersPage complexAddUser(String userMail, String userPassword, String userName, String userGender,
boolean v11, boolean v12, boolean v21, boolean v22, boolean v23) {
email.sendKeys(userMail);
password.sendKeys(userPassword);
name.sendKeys(userName);
gender.selectOption(userGender);
set(var11, v11);
set(var12, v12);
set(var21, v21);
set(var22, v22);
set(var23, v23);
save.click();
return this;
}
@Step("Fill user Email")
public UsersPage sendKeysEmail(String text) {...}
@Step("Fill user Password")
public UsersPage sendKeysPassword(String text) {...}
@Step("Fill user Name")
public UsersPage sendKeysName(String text) {...}
@Step("Select user Gender")
public UsersPage selectGender(String text) {...}
@Step("Select user variant 1.1")
public UsersPage selectVar11(boolean flag) {...}
@Step("Select user variant 1.2")
public UsersPage selectVar12(boolean flag) {...}
@Step("Select user variant 2.1")
public UsersPage selectVar21(boolean flag) {...}
@Step("Select user variant 2.2")
public UsersPage selectVar22(boolean flag) {...}
@Step("Select user variant 2.3")
public UsersPage selectVar23(boolean flag) {...}
@Step("Click save")
public UsersPage clickSave() {...}
private void set(SelenideElement checkbox, boolean flag) {
if (flag) {
if (!checkbox.isSelected()) checkbox.click();
} else {
if (checkbox.isSelected()) checkbox.click();
}
}
}
:
@Test
void addUser() {
baseRouter.authPage()
.complexLogin("test@protei.ru", "test")
.complexOpenAddUser()
.complexAddUser("mail@test.ru", "pswrd", "TESTNAME", "", true, false, true, true, true)
.checkAndCloseSuccessfulAlert();
}
:
@Test
void addUserWithoutComplex() {
//Arrange
baseRouter.authPage()
.complexLogin("test@protei.ru", "test");
//Act
baseRouter.mainPage()
.hoverUsersOpener()
.clickAddUserMenu();
baseRouter.usersPage()
.sendKeysEmail("mail@test.ru")
.sendKeysPassword("pswrd")
.sendKeysName("TESTNAME")
.selectGender("")
.selectVar11(true)
.selectVar12(false)
.selectVar21(true)
.selectVar22(true)
.selectVar23(true)
.clickSave();
//Assert
baseRouter.usersPage()
.checkTextSavePopup(" .")
.closeSavePopup();
}
. : , , , , — . , , , .
O resultado final é que todas as ações com páginas são encapsuladas dentro das páginas (a implementação fica oculta, apenas ações lógicas estão disponíveis), portanto, as funções de negócio já são utilizadas no teste. E isso, por sua vez, permite que você escreva suas próprias páginas para cada plataforma (web, desktop, celulares) sem alterar os testes.
A única pena é que interfaces absolutamente idênticas são raras em plataformas diferentes.
Para reduzir a discrepância entre as interfaces, existe a tentação de complicar as etapas individuais, elas são retiradas em classes intermediárias separadas e os testes se tornam cada vez menos legíveis, até duas etapas: "faça login, faça bem", o teste acabou. Além da web, não havia interfaces adicionais em nossos projetos, e temos que ler casos com mais frequência do que escrever, portanto, por uma questão de legibilidade, PageObjects históricos adquiriram uma nova aparência.
PageObject é um clássico que todos conhecem. Você pode encontrar muitos artigos sobre essa abordagem com exemplos em quase todas as linguagens de programação. O uso de PageObject é freqüentemente usado para julgar se um candidato sabe algo sobre como testar interfaces de usuário. Executar uma atribuição de teste usando essa abordagem é o que a maioria dos empregadores espera, e muito disso vive em projetos de produção, mesmo que apenas a web esteja testando.
O que mais acontece?
Curiosamente, nem um único PageObject!
- O padrão ScreenPlay é frequentemente encontrado, sobre o qual você pode ler, por exemplo, aqui . Não se enraizou em nosso país, pois usar abordagens de bdd sem envolver pessoas que não podem ler o código é uma violência sem sentido contra os automatizadores.
- js- , PageObject, - , , .
- - , , ModelBaseTesting, . , .
E contarei com mais detalhes sobre o Elemento de Página, que permite reduzir a quantidade do mesmo tipo de código, enquanto aumenta a legibilidade e fornece um entendimento rápido dos testes, mesmo para quem não está familiarizado com o projeto. E nele (com seus próprios blackjacks e preferências, é claro!) Os populares frameworks não-js htmlElements, Atlas e Epam's JDI são construídos.
O que é elemento de página?
Para construir o padrão Elemento da Página, vamos começar com o elemento de nível mais baixo. Como diz o Wikcionário , um "widget" é um software primitivo de uma interface gráfica de usuário que tem uma aparência padrão e executa ações padrão. Por exemplo, o widget mais simples "Botão" - você pode clicar nele, você pode verificar o texto e a cor. No "campo de entrada" você pode inserir texto, verificar o texto inserido, clicar, verificar a exibição do foco, verificar o número de caracteres inseridos, inserir o texto e pressionar "Enter", verificar o espaço reservado, verificar o destaque do campo "obrigatório" e o texto do erro, e pronto, o que mais pode ser necessário em um caso particular. Além disso, todas as ações com este campo são padrão em qualquer página.

Existem widgets mais complexos para os quais as ações não são tão óbvias, por exemplo, tabelas de conteúdo em árvore. Ao escrevê-los, você precisa se basear no que o usuário faz com esta área do programa, por exemplo:
- Clique em um elemento do índice com o texto especificado,
- Verificar a existência de um elemento com o texto fornecido,
- Verificar a indentação de um elemento com o texto fornecido.
Os widgets podem ser de dois tipos: com um localizador no construtor e com um localizador costurado no widget, sem a capacidade de alterá-lo. O índice é geralmente um na página, seu método de pesquisa na página pode ser deixado "dentro" das ações com o índice, não faz sentido retirar seu localizador separadamente, uma vez que o localizador pode ser danificado acidentalmente do lado de fora e não há benefício em armazená-lo separadamente. Por sua vez, um campo de texto é uma coisa universal, ao contrário, você precisa trabalhar com ele apenas através do localizador do construtor, pois pode haver muitos campos de entrada ao mesmo tempo. Se aparecer pelo menos um método destinado a apenas um campo de entrada especial, por exemplo, com um clique adicional na dica suspensa, isso não é mais apenas um campo de entrada, é hora de criar seu próprio widget para ele.
Para reduzir o caos geral, widgets, como elementos de página, são combinados nas mesmas páginas, das quais, aparentemente, o nome Elemento de página é composto.
public class UsersPage {
public Table usersTable = new Table();
public InputLine email = new InputLine(By.id("dataEmail"));
public InputLine password = new InputLine(By.id("dataPassword"));
public InputLine name = new InputLine(By.id("dataName"));
public DropdownList gender = new DropdownList(By.id("dataGender"));
public Checkbox var11 = new Checkbox(By.id("dataSelect11"));
public Checkbox var12 = new Checkbox(By.id("dataSelect12"));
public Checkbox var21 = new Checkbox(By.id("dataSelect21"));
public Checkbox var22 = new Checkbox(By.id("dataSelect22"));
public Checkbox var23 = new Checkbox(By.id("dataSelect23"));
public Button save = new Button(By.id("dataSend"));
public ErrorPopup errorPopup = new ErrorPopup();
public ModalPopup savePopup = new ModalPopup();
}
Para usar tudo o que foi criado acima em testes, você precisa se referir sequencialmente à página, widget, ação, assim obtemos a seguinte construção:
@Test
public void authAsAdmin() {
baseRouter
.authPage().email.fill("test@protei.ru")
.authPage().password.fill("test")
.authPage().enter.click()
.mainPage().logoutButton.shouldExist();
}
Você pode adicionar uma camada de etapa clássica se houver necessidade disso em sua estrutura (a implementação da biblioteca remota em Java para RobotFramework requer uma classe de etapa como entrada, por exemplo) ou se desejar adicionar anotações para relatórios bonitos. Fizemos um gerador baseado em anotações, se tiver interesse, escreva nos comentários, avisaremos.
Um exemplo de uma classe de etapa de autorização
public class AuthSteps{
private BaseRouter baseRouter = new BaseRouter();
@Step("Sigh in as {mail}")
public BaseSteps login(String mail, String password) {
baseRouter
.authPage().email.fill(mail)
.authPage().password.fill(password)
.authPage().enter.click()
.mainPage().logoutButton.shouldExist();
return this;
}
@Step("Fill E-mail")
public AuthSteps fillEmail(String email) {
baseRouter.authPage().email.fill(email);
return this;
}
@Step("Fill password")
public AuthSteps fillPassword(String password) {
baseRouter.authPage().password.fill(password);
return this;
}
@Step("Click enter")
public AuthSteps clickEnter() {
baseRouter.authPage().enter.click();
return this;
}
@Step("Enter should exist")
public AuthSteps shouldExistEnter() {
baseRouter.authPage().enter.shouldExist();
return this;
}
@Step("Logout")
public AuthSteps logout() {
baseRouter.mainPage().logoutButton.click()
.authPage().enter.shouldExist();
return this;
}
}
public class BaseRouter {
// , ,
public AuthPage authPage() {return page(AuthPage.class);}
public MainPage mainPage() {return page(MainPage.class);}
public UsersPage usersPage() {return page(UsersPage.class);}
public VariantsPage variantsPage() {return page(VariantsPage.class);}
}
Essas etapas são muito semelhantes às etapas dentro das páginas, praticamente não diferentes. Mas separá-los em classes separadas abre espaço para geração de código, enquanto o link físico com a página correspondente não é perdido. Ao mesmo tempo, se você não escrever etapas na página, o significado do encapsulamento desaparecerá e, se você não adicionar uma classe de etapas a pageElement, a interação com a página ainda permanecerá separada da lógica de negócios.
, , . . , , , « , ». — , page object , !
Seria errado falar sobre a arquitetura de um projeto sem tocar nos métodos de operação conveniente com dados de teste.
A maneira mais fácil é passar os dados diretamente no teste "como estão" ou em variáveis. Isso é bom para arquitetura de folha, mas grandes projetos ficam confusos.
Outro método é armazenar dados como objetos, acabou por ser o melhor para nós, pois coleta todos os dados relativos a uma entidade em um único lugar, tirando a tentação de misturar tudo e usar algo no lugar errado. Além disso, esse método tem muitas melhorias adicionais que podem ser úteis em projetos individuais.
Para cada entidade, é criado um modelo que a descreve, que no caso mais simples contém os nomes e tipos de campos, por exemplo, aqui está o modelo do usuário:
public class User {
private Integer id;
private String mail;
private String name;
private String password;
private Gender gender;
private boolean check11;
private boolean check12;
private boolean check21;
private boolean check22;
private boolean check23;
public enum Gender {
MALE,
FEMALE;
public String getVisibleText() {
switch (this) {
case MALE:
return "";
case FEMALE:
return "";
}
return "";
}
}
}
Life hack nº 1: se você tem uma arquitetura parecida com o resto de interação cliente-servidor (objetos json ou xml vão entre o cliente e o servidor, e não pedaços de código ilegível), então você pode google json para o objeto <seu idioma>, provavelmente o gerador de que você precisa já existe ...
Hack de vida nº 2: se seus desenvolvedores de servidor escrevem na mesma linguagem de programação orientada a objetos, então você pode usar seus modelos.
Life hack # 3: se você é um javist e uma empresa permite que você use bibliotecas de terceiros, e não há colegas nervosos por perto, prevendo muita dor para os hereges que usam bibliotecas adicionais em vez do Java puro e bonito, pegue o Lombok ! Sim, geralmente IDEpode gerar getters, setters, toString e construtores. Mas, ao comparar nossos modelos do Lombok e os de desenvolvimento sem Lombok, é visível um lucro de centenas de linhas de código "vazio" que não carrega lógica de negócios para cada classe. Ao usar um Lombok, não é preciso bater nas mãos de quem mistura campos e getters com setters, a aula fica mais fácil de ler, dá para ter uma ideia do objeto de uma vez, sem passar por três telas.
Assim, temos wireframes de objetos nos quais precisamos esticar os dados de teste. Os dados podem ser armazenados como variáveis estáticas finais, por exemplo, isso pode ser útil para o administrador do sistema principal, a partir do qual outros usuários são criados. É melhor usar o final, para que não haja a tentação de alterar os dados nos testes, porque então o próximo teste, ao invés do administrador, pode pegar um usuário “impotente”, sem falar no lançamento paralelo de testes.
public class Users {
public static final User admin = User.builder().mail("test@protei.ru").password("test").build();
}
Para obter dados que não afetam outros testes, você pode usar o padrão "protótipo" e clonar sua instância em cada teste. Decidimos tornar mais fácil: escrever um método que randomize os campos da classe, algo assim:
public static User getUserRandomData() {
User user = User.builder()
.mail(getRandomEmail())
.password(getShortLatinStr())
.name(getShortLatinStr())
.gender(getRandomFromEnum(User.Gender.class))
.check11(getRandomBool())
.check21(getRandomBool())
.check22(getRandomBool())
.check23(getRandomBool())
.build();
//business-logic: 11 xor 12 must be selected
if (!user.isCheck11()) user.setCheck12(true);
if (user.isCheck11()) user.setCheck12(false);
return user;
}
Ao mesmo tempo, os métodos que criam aleatoriedade direta devem ser colocados em uma classe separada, uma vez que serão usados em outros modelos também:

No método para obter um usuário aleatório, foi usado o padrão "construtor" , que é necessário para não criar um novo tipo de construtor para cada conjunto necessário Campos. Em vez disso, é claro, você pode simplesmente chamar o construtor desejado.
Este método de armazenamento de dados usa o padrão Value Object, com base no qual você pode adicionar qualquer um de seus desejos, dependendo das necessidades do projeto. Você pode adicionar o armazenamento de objetos ao banco de dados e, assim, preparar o sistema antes do teste. Você não pode randomizar os usuários, mas carregá-los dos arquivos de propriedades (e outra biblioteca legal) Você pode usar o mesmo usuário em qualquer lugar, mas faça o chamado registro de dados para cada tipo de objeto, no qual o valor do contador ponta a ponta será adicionado ao nome ou outro campo exclusivo do objeto, e o teste sempre terá seu próprio testUser_135 exclusivo.
Você pode escrever seu próprio armazenamento de objeto (pool de objetos do google e flyweight), a partir do qual você pode solicitar as entidades necessárias no início do teste. O depósito dá um de seus objetos prontos para o trabalho e o marca como ocupado. Ao final do teste, o objeto é devolvido ao depósito, onde é limpo conforme necessário, marcado como livre e entregue no próximo teste. Isso é feito se as operações de criação de objetos exigem muitos recursos e, com essa abordagem, o armazenamento funciona independentemente dos testes e pode preparar dados para os seguintes casos.
Criação de dados
Para os casos de edição de usuário, você definitivamente precisará de um usuário criado que irá editar, enquanto, em geral, o teste de edição não se importa de onde esse usuário veio. Existem várias maneiras de criá-lo:
- pressione os botões com as mãos antes do teste,
- deixe os dados do teste anterior,
- implantar antes do teste do backup,
- crie clicando nos botões diretamente no teste,
- usar a API.
Todos esses métodos têm desvantagens: se você precisar inserir algo no sistema manualmente antes do teste, então este é um teste ruim e, portanto, são chamados de autotestes, porque devem atuar o mais independentemente possível das mãos humanas.
Usar os resultados do teste anterior viola o princípio da atomicidade e não permite que você execute o teste separadamente, você terá que executar o lote inteiro e os testes de interface do usuário não são tão rápidos. É considerado uma boa forma de escrever testes para que todos possam correr em esplêndido isolamento e sem danças adicionais. Além disso, um bug na criação de um objeto que descartou o teste anterior não garante de forma alguma um bug na edição, e em tal design, o teste de edição cairá em seguida, e é impossível descobrir se a edição está funcionando.
Usar o backup (uma imagem salva do banco de dados) com os dados necessários para o teste já é uma abordagem mais ou menos boa, especialmente se o backup for implantado automaticamente ou se os próprios testes colocarem os dados no banco de dados. No entanto, por que esse objeto específico é usado no teste não é óbvio, os problemas de interseção de dados também podem começar com um grande número de testes. Às vezes, o backup para de funcionar corretamente devido a uma atualização da arquitetura do banco de dados, por exemplo, se você precisar executar testes em uma versão antiga, e o backup já contém novos campos. Você pode combater isso organizando um armazenamento de backup para cada versão do aplicativo. Às vezes, o backup deixa de ser válido novamente devido à atualização da arquitetura do banco de dados - novos campos aparecem regularmente, portanto, o backup precisa ser atualizado regularmente. E de repente pode serque exatamente esse tipo de usuário de backup nunca falha, e se o usuário acabou de ser criado ou o nome foi dado a ele de forma aleatória, você encontrará um bug. Isso é chamado de “efeito pesticida”, o teste para de pegar bugs, pois o aplicativo está “acostumado” com os mesmos dados e não cai, e não há desvios para o lado.
Se o usuário for criado no teste por meio de cliques na mesma interface, o pesticida diminui e a não-obviedade da aparência do usuário desaparece. As desvantagens são semelhantes a usar os resultados do teste anterior: a velocidade é normal, e mesmo se houver um bug na criação, mesmo o menor (especialmente um bug de teste, por exemplo, o localizador do botão Salvar mudará), então não saberemos se a edição funciona.
Por fim, outra forma de criar um usuário é através da http-API do teste, ou seja, ao invés de clicar nos botões, envie imediatamente uma solicitação para criar o usuário desejado. Assim, o pesticida é reduzido ao máximo, é óbvio de onde veio o usuário, e a velocidade de criação é muito maior do que ao clicar nos botões. As desvantagens desse método são que ele não é adequado para projetos sem json ou xml no protocolo de comunicação entre o cliente e o servidor (por exemplo, se os desenvolvedores escreverem usando gwt e não quiserem escrever uma api adicional para testadores). É possível, ao usar a API, perder um trecho da lógica executada pelo painel de administração e criar uma entidade inválida. A API pode mudar, fazendo com que os testes falhem, mas geralmente isso é conhecido e ninguém precisa de mudanças por causa das mudanças, provavelmente esta é uma nova lógica que ainda terá que ser verificada.Também é possível que haja um bug no nível da API, mas nenhum método diferente de backups prontos está protegido contra isso, então é melhor combinar abordagens para criar dados.
Adicionar uma API droplet
Entre os métodos de preparação de dados, a http-API para as necessidades atuais de um teste separado e a implantação de um backup para dados de teste adicionais que não mudam nos testes, por exemplo, ícones para objetos, para que os testes desses objetos não travem quando os ícones são carregados, são os mais adequados para nós.
Para criar objetos por meio da API em Java, tornou-se mais conveniente usar a biblioteca restAssured, embora ela não seja realmente destinada a isso. Quero compartilhar algumas fichas encontradas, você sabe mais - escreva!
O primeiro problema é a autorização no sistema. Seu método precisa ser selecionado separadamente para cada projeto, mas há uma coisa em comum - a autorização precisa ser colocada na especificação da solicitação, por exemplo:
public class ApiSettings {
private static String loginEndpoint="/login";
public static RequestSpecification testApi() {
RequestSpecBuilder tmp = new RequestSpecBuilder()
.setBaseUri(testConfig.getSiteUrl())
.setContentType(ContentType.JSON)
.setAccept(ContentType.JSON)
.addFilter(new BeautifulRest())
.log(LogDetail.ALL);
Map<String, String> cookies = RestAssured.given().spec(tmp.build())
.body(admin)
.post(loginEndpoint).then().statusCode(200).extract().cookies();
return tmp.addCookies(cookies).build();
}
}
Você pode adicionar a capacidade de salvar cookies para um usuário específico, então o número de solicitações ao servidor diminuirá. A segunda extensão possível desse método é salvar os Cookies recebidos para o teste atual e jogá-los no driver do navegador, pulando a etapa de autorização. Os ganhos são em segundos, mas se você multiplicá-los pelo número de testes, você pode acelerar muito bem!
Tem coque para a marcha e lindos relatos, preste atenção na linha
.addFilter(new BeautifulRest())
:
Classe BeautifulRest

public class BeautifulRest extends AllureRestAssured {
public BeautifulRest() {}
public Response filter(FilterableRequestSpecification requestSpec, FilterableResponseSpecification responseSpec, FilterContext filterContext) {
AllureLifecycle lifecycle = Allure.getLifecycle();
lifecycle.startStep(UUID.randomUUID().toString(), (new StepResult()).setStatus(Status.PASSED).setName(String.format("%s: %s", requestSpec.getMethod(), requestSpec.getURI())));
Response response;
try {
response = super.filter(requestSpec, responseSpec, filterContext);
} finally {
lifecycle.stopStep();
}
return response;
}
}
Modelos de objetos se encaixam perfeitamente com restAssured, já que a própria biblioteca lida com a serialização e desserialização de modelos em json / xml (conversão de formatos json / xml para um objeto de uma determinada classe).
@Step("create user")
public static User createUser(User user) {
String usersEndpoint = "/user";
return RestAssured.given().spec(ApiSettings.testApi())
.when()
.body(user)
.post(usersEndpoint)
.then().log().all()
.statusCode(200)
.body("state",containsString("OK"))
.extract().as(User.class);
}
Se você considerar várias etapas consecutivas para a criação de objetos, poderá notar a identidade do código. Para reduzir o mesmo código, você pode escrever um método geral para criar objetos.
public static Object create(String endpoint, Object model) {
return RestAssured.given().spec(ApiSettings.testApi())
.when()
.body(model)
.post(endpoint)
.then().log().all()
.statusCode(200)
.body("state",containsString("OK"))
.extract().as(model.getClass());
}
@Step("create user")
public static User createUser(User user) {
create(User.endpoint, user);
}
Mais uma vez sobre as operações de rotina
Como parte da verificação da edição de um objeto, geralmente não nos importamos como o objeto apareceu no sistema - por meio de api ou de backup, ou foi criado por um teste de interface do usuário. Ações importantes são encontrar um objeto, clicar no ícone "editar" nele, limpar os campos e preenchê-los com os novos valores, clicar em "salvar" e verificar se todos os novos valores foram salvos corretamente. Todas as informações desnecessárias que não estão diretamente relacionadas ao teste devem ser removidas em métodos separados, por exemplo, na classe step.
@Test
void checkUserVars() {
//Arrange
User userForTest = getUserRandomData();
// ,
// - ,
// ,
usersSteps.createUser(userForTest);
authSteps.login(userForTest);
//Act
mainMenuSteps
.clickVariantsMenu();
//Assert
variantsSteps
.checkAllVariantsArePresent(userForTest.getVars())
.checkVariantsCount(userForTest.getVarsCount());
//Cleanup
usersSteps.deleteUser(userForTest);
}
É importante não se deixar levar, pois um teste que consiste apenas em ações "complexas" torna-se menos legível e mais difícil de reproduzir sem vasculhar o código.
@Test
void authAsAdmin() {
authSteps.login(Users.admin);
// , . .
// , ?
Se praticamente os mesmos testes aparecem no pacote, que diferem apenas na preparação dos dados (por exemplo, você precisa verificar se todos os três tipos de usuários "diferentes" podem executar as mesmas ações, ou se existem diferentes tipos de objetos de controle, para cada um dos quais você precisa verificar criação de objetos dependentes idênticos, ou você precisa verificar a filtragem por dez tipos de status de objeto), você ainda não pode mover as partes repetidas para um método separado. Nem um pouco se a legibilidade for importante para você!
Em vez disso, você precisa ler sobre testes orientados a dados, para Java + TestNG será algo assim:
@Test(dataProvider = "usersWithDifferentVars")
void checkUserDifferentVars(User userForTest) {
//Arrange
usersSteps.createUser(userForTest);
authSteps.login(userForTest);
//Act
mainMenuSteps
.clickVariantsMenu();
//Assert
variantsSteps
.checkAllVariantsArePresent(userForTest.getVars())
.checkVariantsCount(userForTest.getVarsCount());
}
// .
// , -.
@DataSupplier(name = "usersWithDifferentVars")
public Stream<User> usersWithDifferentVars(){
return Stream.of(
getUserRandomData().setCheck21(false).setCheck22(false).setCheck23(false),
getUserRandomData().setCheck21(true).setCheck22(false).setCheck23(false),
getUserRandomData().setCheck21(false).setCheck22(true).setCheck23(false),
getUserRandomData().setCheck21(false).setCheck22(false).setCheck23(true),
getUserRandomData().setCheck21(true).setCheck22(true).setCheck23(false),
getUserRandomData().setCheck21(true).setCheck22(false).setCheck23(true),
getUserRandomData().setCheck21(false).setCheck22(true).setCheck23(true),
getUserRandomData().setCheck21(true).setCheck22(true).setCheck23(true)
);
}
Ele usa a biblioteca Data Supplier , que é um complemento do TestNG Data Provider que permite usar coleções digitadas em vez de Object [] [], mas a essência é a mesma. Assim, obtemos um teste, que é executado tantas vezes quanto recebe os dados de entrada.
conclusões
Portanto, para criar um projeto grande, mas conveniente de autotestes de interface do usuário, você precisa:
- Descreva todos os pequenos widgets encontrados no aplicativo,
- Colete widgets em páginas,
- Crie modelos para todos os tipos de entidades,
- Adicione métodos para gerar todos os tipos de entidades com base em modelos,
- Considere um método adequado para criar entidades adicionais
- Opcional: gere ou colete arquivos de etapas manualmente,
- Escreva testes de forma que na seção das principais ações de um determinado teste não haja ações complexas, apenas operações óbvias com widgets.
Pronto, você criou um projeto baseado em PageElement com métodos simples para armazenar, gerar e preparar dados. Agora você tem uma arquitetura de fácil manutenção, gerenciamento e flexibilidade suficiente. Tanto um testador experiente quanto um novato June podem navegar facilmente pelo projeto, uma vez que os autotestes no formato de ações do usuário são mais convenientes de ler e entender.
Exemplos de código do artigo na forma de um projeto concluído são adicionados ao git .