Guia de anotação TestNG para Selenium WebDriver

Paz, trabalho, maio, residentes de Khabrovsk! Para aqueles que, como nós, entraram na semana de trabalho entre férias, preparamos uma tradução que queremos coincidir com o início da inscrição no curso Java QA Engineer , que está programado para ser lançado em 28 de maio.



TestNG é uma estrutura de teste criada por Cédric Beust que nos ajuda a satisfazer muitas de nossas necessidades de teste. O TestNG é amplamente utilizado com selênio. Deseja saber o que significa NG? Isso significa "próxima geração" . O TestNG é semelhante ao JUnit, mas é mais poderoso quando se trata de controlar o fluxo de execução do seu programa. A arquitetura da estrutura nos ajuda a tornar os testes mais estruturados e fornecer melhores pontos de validação.

Alguns recursos do TestNG que merecem atenção:

  • Anotações poderosas e variadas para apoiar seus casos de teste.
  • Executando testes em paralelo, usando dependências entre testes.
  • A flexibilidade para executar seus testes em diferentes conjuntos de dados, por meio do arquivo TestNG.xml ou pelo conceito de provedores de dados.
  • Agrupamento e priorização de casos de teste.
  • Geração de relatórios HTML, customização utilizando vários plugins.
  • Geração de logs de execução de teste.
  • Fácil integração com Eclipse, Maven, Jenkins, etc.

Normalmente, o processo de teste usando TestNG inclui as seguintes etapas:



Antes de passar para as anotações do TestNG para o Selenium, descrevemos os pré-requisitos para configurar o TestNG.

Pré-requisitos:

  • Kit de desenvolvimento Java
  • Eclipse ou qualquer outro IDE
  • TestNG instalado no Eclipse ou no seu IDE

Nota: As anotações Java podem ser usadas apenas com o Java versão 1.5 e superior.

Se você é novo no TestNG, consulte o artigo sobre como executar o primeiro script de automação com o TestNG .

Então, o que é uma anotação?

Uma anotação é um rótulo que fornece informações adicionais sobre uma classe ou método ( nota do tradutor: as anotações em java podem ser aplicadas não apenas a classes e métodos, mas também a outros elementos ). Para anotações, o prefixo "@" é usado. O TestNG usa anotações para ajudar a criar uma estrutura de teste robusta. Vejamos as anotações TestNG usadas para automatizar os testes com o Selenium.


@Test

Essa é a anotação mais importante no TestNG, que contém a principal lógica do teste . Todas as funções automatizadas estão no método com a anotação @Test . Possui vários atributos com os quais o lançamento do método pode ser configurado.

O código de exemplo abaixo verifica a transição do URL:

 @Test public void testCurrentUrl() throws InterruptedException { driver.findElement(By.xpath("//*[@id='app']/header/aside/ul/li[4]/a")) .click(); String currentUrl = driver.getCurrentUrl(); assertEquals( currentUrl, "https://automation.lambdatest.com/timeline/?viewType=build&page=1", "url did not matched"); } 

@BeforeTest

Um método com esta anotação é executado antes que o primeiro método seja executado com a anotação @Test . (Nota do tradutor: como parte do teste definido na seção de test do arquivo de configuração xml) . Você pode usar esta anotação no TestNG with Selenium para configurar seu navegador. Por exemplo, inicie um navegador e expanda-o para tela cheia, defina configurações específicas do navegador etc.

Abaixo está um exemplo para o BeforeTest, no qual o navegador se expande para tela cheia:

 @BeforeTest public void profileSetup() { driver.manage().window().maximize(); } 

@AfterTest

Os métodos marcados com esta anotação são executados após todos os métodos @Test do seu teste. ( Nota do tradutor: como parte do teste definido na seção de test no arquivo de configuração xml, a “classe atual” não está escrita corretamente no original ). Essa é uma anotação útil para fornecer resultados de teste. Você pode usar esta anotação para criar um relatório em seus testes e enviá-lo por email para as partes interessadas.

Exemplo de código:

 @AfterTest public void reportReady() { System.out.println("Report is ready to be shared, with screenshots of tests"); } 

@BeforeMethod

Os métodos com esta anotação são executados antes de cada método @Test . Você pode usá-lo para testar a conexão com o banco de dados antes de executar o teste. Ou, por exemplo, ao testar a funcionalidade que depende do logon do usuário, insira o código para inserir o sistema aqui.
A seguir, um trecho de código que demonstra o login no LambdaTest:

 @BeforeMethod public void checkLogin() { driver.get("https://accounts.lambdatest.com/login"); driver.findElement(By.xpath("//input[@name='email']")).sendKeys("sadhvisingh24@gmail.com"); driver.findElement(By.xpath("//input[@name='password']")).sendKeys("activa9049"); driver.findElement(By.xpath("//*[@id='app']/section/form/div/div/button")).click(); } 

@AfterMethod

Os métodos com esta anotação são iniciados após cada método @Test . Você pode usar esta anotação para tirar capturas de tela sempre que executar o teste.
A seguir, um trecho de código que demonstra como fazer uma captura de tela:

 @AfterMethod public void screenShot() throws IOException { TakesScreenshot scr = ((TakesScreenshot) driver); File file1 = scr.getScreenshotAs(OutputType.FILE); FileUtils.copyFile(file1, new File(":\\test-output\\test1.PNG")); } 

@BeforeClass

Um método com esta anotação será executado antes do primeiro método de teste na classe atual. Você pode usar esta anotação para configurar propriedades do navegador, inicializar o driver, abrir um navegador com o URL desejado etc.

Código de exemplo para BeforeClass:

 @BeforeClass public void appSetup() { driver.get(url); } 

@AfterClass

Um método com esta anotação será executado após o último método de teste na classe atual. Essa anotação no TestNG pode ser usada para executar ações para limpar recursos após a conclusão do teste, como fechar o driver, etc.
A seguir, é apresentado um exemplo de um trecho de código mostrando o fechamento do driver:

 @AfterClass public void closeUp() { driver.close(); } 

@BeforeSuite

Um conjunto de testes (conjunto) pode consistir em várias classes; essa anotação é executada antes de todos os métodos de teste de todas as classes. Esta anotação marca o ponto de entrada na inicialização. A @BeforeSuite no TestNG pode ser usada para executar funções comuns, como configurar e executar o Selenium ou drivers da Web remotos, etc.
Um exemplo de anotação @BeforeSuite no TestNG mostrando a configuração do driver:

 @BeforeSuite public void setUp() { System.setProperty( "webdriver.chrome.driver", ":\\selenium\\chromedriver.exe"); driver = new ChromeDriver(); } 

@AfterSuite

Esta anotação no TestNG inicia após a execução de todos os métodos de teste em todas as classes. Essa anotação pode ser usada para limpar antes de concluir os testes quando você usa várias classes, por exemplo, fechando drivers etc.
Abaixo está um trecho de código para a anotação @AfterSuite no TestNG for Selenium:

 @AfterSuite public void cleanUp() { System.out.println("All close up activities completed"); } 

@BeforeGroups

O TestNG pode agrupar testes usando o atributo group nas anotações @Test . Por exemplo, se você deseja combinar todas as funções semelhantes relacionadas ao gerenciamento de usuários, poderá marcar os testes, como painel (painel do usuário), perfil (perfil), transações (transações) e similares, em um grupo, como user_management. A @BeforeGroups no TestNG ajuda a iniciar determinadas ações na frente do grupo de teste especificado. Esta anotação pode ser usada se o grupo focar em uma funcionalidade, conforme indicado no exemplo acima. A @BeforeGroup pode conter uma função de login necessária para executar testes em um grupo, por exemplo, testando um painel de usuário, perfil de usuário etc.

Um exemplo do uso de @BeforeGroups :

 @BeforeGroups("urlValidation") public void setUpSecurity() { System.out.println("url validation test starting"); } 

@AfterGroups

Esta anotação é iniciada após a execução de todos os métodos de teste do grupo especificado.

Código de exemplo para a anotação @AfterGroups no TestNG for Selenium:

 @AfterGroups("urlValidation") public void tearDownSecurity() { System.out.println("url validation test finished"); } 

Exemplo

O código abaixo mostra exemplos de uso de todas as anotações discutidas acima:

 import org.apache.commons.io.FileUtils; import org.openqa.selenium.By; import org.openqa.selenium.OutputType; import org.openqa.selenium.TakesScreenshot; import org.openqa.selenium.WebDriver; import org.openqa.selenium.chrome.ChromeDriver; import org.testng.annotations.*; import java.io.File; import java.io.IOException; import java.util.concurrent.TimeUnit; import static org.testng.Assert.assertEquals; public class AnnotationsTestNG { private WebDriver driver; private String url = "https://www.lambdatest.com/"; @BeforeSuite public void setUp() { System.setProperty( "webdriver.chrome.driver", ":\\selenium\\chromedriver.exe"); driver = new ChromeDriver(); driver.manage().timeouts().implicitlyWait(20, TimeUnit.SECONDS); System.out.println("The setup process is completed"); } @BeforeTest public void profileSetup() { driver.manage().window().maximize(); System.out.println("The profile setup process is completed"); } @BeforeClass public void appSetup() { driver.get(url); System.out.println("The app setup process is completed"); } @BeforeMethod public void checkLogin() { driver.get("https://accounts.lambdatest.com/login"); driver.findElement(By.xpath("//input[@name='email']")).sendKeys("sadhvisingh24@gmail.com"); driver.findElement(By.xpath("//input[@name='password']")).sendKeys("activa9049"); driver.findElement(By.xpath("//*[@id='app']/section/form/div/div/button")).click(); System.out.println("The login process on lamdatest is completed"); } @Test(groups = "urlValidation") public void testCurrentUrl() throws InterruptedException { driver.findElement(By.xpath("//*[@id='app']/header/aside/ul/li[4]/a")).click(); Thread.sleep(6000); String currentUrl = driver.getCurrentUrl(); assertEquals(currentUrl, "https://automation.lambdatest.com/timeline/?viewType=build&page=1", "url did not matched"); System.out.println("The url validation test is completed"); } @AfterMethod public void screenShot() throws IOException { TakesScreenshot scr = ((TakesScreenshot) driver); File file1 = scr.getScreenshotAs(OutputType.FILE); FileUtils.copyFile(file1, new File(":\\test-output\\test1.PNG")); System.out.println("Screenshot of the test is taken"); } @AfterClass public void closeUp() { driver.close(); System.out.println("The close_up process is completed"); } @AfterTest public void reportReady() { System.out.println("Report is ready to be shared, with screenshots of tests"); } @AfterSuite public void cleanUp() { System.out.println("All close up activities completed"); } @BeforeGroups("urlValidation") public void setUpSecurity() { System.out.println("url validation test starting"); } @AfterGroups("urlValidation") public void tearDownSecurity() { System.out.println("url validation test finished"); } } 

Relatório TestNG:



Saída do console:



Nota do tradutor:

  • para executar o teste, altere o caminho para chromedriver.exe no método setUp () e o caminho para a pasta da captura de tela no método screenShot (). Além disso, para executar com êxito o teste, o método checkLogin () deve ter um nome de usuário e senha válidos.
  • Dependências usadas do maven:

 <dependencies> <dependency> <groupId>org.testng</groupId> <artifactId>testng</artifactId> <version>6.14.3</version> <scope>test</scope> </dependency> <dependency> <groupId>org.seleniumhq.selenium</groupId> <artifactId>selenium-chrome-driver</artifactId> <version>3.141.59</version> <scope>test</scope> </dependency> <dependency> <groupId>commons-io</groupId> <artifactId>commons-io</artifactId> <version>2.6</version> <scope>test</scope> </dependency> </dependencies> 
Sequência de anotação TestNG para Selenium

As anotações descritas acima são executadas em tempo de execução na seguinte ordem:

  • Beforesuite
  • Beforetest
  • Beforeclass
  • BeforeGroups
  • Beforemethod
  • Teste
  • Aftermethod
  • Pós-grupos
  • Afterclass
  • Pós-teste
  • Aftersuite

Aqui está a ordem de sua execução:



Atributos usados ​​com anotações no TestNG

As anotações no TestNG possuem atributos que você pode usar para personalizar. Eles ajudam a configurar a ordem de execução dos métodos de teste.

Esses atributos são:

  • descrição : você pode especificar uma descrição do método de teste.
    Por exemplo, @Test (description = ”este teste verifica o login”).
  • alwaysRun : esse atributo garante que o método de teste seja sempre executado, mesmo se os testes dos quais depende forem descartados. Quando o valor do atributo for verdadeiro, esse método sempre será executado.
    Por exemplo, @Test (alwaysRun = true).
  • dataProvider : define o nome do provedor de dados para o método de teste. Suponha que você execute seus testes em vários navegadores e, no método de teste com o atributo dataProvider, você pode adicionar parâmetros para o navegador e sua versão, que serão transmitidos ao método pelo provedor de dados. Nesse caso, o teste que contém esse atributo utilizará essa entrada para executar os testes em vários navegadores.
    Por exemplo, @Test (dataProvider = ”cross-browser-testing”).
  • depende de Um teste com esse atributo será executado apenas se o teste do qual depende for bem-sucedido. Se o teste do qual o método depende cair, o teste não será iniciado.
    Por exemplo, @Test (depenOnmethod = "login").
  • groups : ajuda a agrupar seus métodos de teste focados em uma funcionalidade em um grupo.
    Por exemplo, @Test (groups = ”Payment_Module”).
    Este atributo também permite controlar quais testes executar. Ao executar testes, você pode ignorar alguns grupos ou, inversamente, executar apenas alguns grupos. Tudo o que você precisa fazer é especificar os grupos necessários no arquivo TestNG.xml. Na marca de include , especifique os grupos que você deseja executar e na marca de exclude que deseja ignorar.
  • DependOnGroups : executa as funções dos dois atributos mencionados acima, ou seja, determina a dependência do método de teste no grupo especificado. Este método de teste será iniciado somente após a conclusão do grupo de testes especificado.
    Por exemplo, @Test (depenOnMethods = "Payment_Module").
  • prioridade : nos ajuda a priorizar os métodos de teste. Quando o TestNG executa os métodos de teste, pode fazê-lo em ordem aleatória. Em um cenário em que você deseja que seus testes sejam executados na ordem correta, você pode usar o atributo priority. A prioridade padrão para todos os métodos de teste é 0. Primeiro, os testes são executados com um valor de prioridade mais baixo.
    Por exemplo, @Test (prioridade = 1), @Test (prioridade = 2). Nesse caso, o teste com prioridade igual a um será executado primeiro e, em seguida, o teste com prioridade dois.
  • ativado : esse atributo é usado quando você precisa ignorar e não executar um teste específico. Tudo o que você precisa fazer é configurá-lo como false.
    Por exemplo, @Test (ativado = falso).
  • timeout : define o tempo pelo qual o teste deve ser concluído. Se a execução do teste exceder o tempo especificado pelo atributo, o teste falhará com uma exceção lançando org.testng.internal.thread.ThreadTimeoutException
    Por exemplo, @Test (timeOut = 500). Observe que o tempo é indicado em milissegundos .
  • invocationCount : funciona como um loop. O teste será executado quantas vezes for especificado em invocationCount.
    Por exemplo, @Test (invocationCount = 5) será executado 5 vezes.
  • invocationTimeOut : usado em conjunto com o atributo invocationCount acima. O valor desse atributo junto com invocationCount indica que o teste será executado quantas vezes for especificado em invocationCount e durante o tempo especificado no atributo invocationTimeOut.
    Por exemplo, @Test (invocationCount = 5, invocationTimeOut = 20).
  • pectedExceptions : Ajuda a lidar com exceções que devem ser lançadas no método de teste. Se a exceção especificada no atributo for lançada pelo método de teste, o teste foi bem-sucedido. Caso contrário, a ausência de uma exceção ou o lançamento de outra exceção não especificada no atributo falharão no teste.
    Por exemplo, @Test ( @Test = {ArithmeticException.class}).

Acima estão os atributos usados ​​com anotações no TestNG with Selenium. A seguir, um trecho de código que demonstra o uso dos atributos acima:

 import static org.testng.Assert.assertEquals; import java.io.File; import java.io.IOException; import org.apache.commons.io.FileUtils; import org.openqa.selenium.By; import org.openqa.selenium.OutputType; import org.openqa.selenium.TakesScreenshot; import org.openqa.selenium.WebDriver; import org.openqa.selenium.WebElement; import org.openqa.selenium.chrome.ChromeDriver; import org.testng.annotations.AfterClass; import org.testng.annotations.AfterGroups; import org.testng.annotations.AfterMethod; import org.testng.annotations.AfterSuite; import org.testng.annotations.AfterTest; import org.testng.annotations.BeforeClass; import org.testng.annotations.BeforeGroups; import org.testng.annotations.BeforeSuite; import org.testng.annotations.BeforeTest; import org.testng.annotations.Test; public class AnnotationsTest { private WebDriver driver; private String url = "https://www.lambdatest.com/"; @BeforeSuite public void setUp() { System.setProperty( "webdriver.chrome.driver", ":\\selenium\\chromedriver.exe"); driver = new ChromeDriver(); System.out.println("The setup process is completed"); } @BeforeTest public void profileSetup() { driver.manage().window().maximize(); System.out.println("The profile setup process is completed"); } @BeforeClass public void appSetup() { driver.get(url); System.out.println("The app setup process is completed"); } @Test(priority = 2) public void checkLogin() { driver.get("https://accounts.lambdatest.com/login"); driver.findElement(By.xpath("//input[@name='email']")).sendKeys("sadhvisingh24@gmail.com"); driver.findElement(By.xpath("//input[@name='password']")).sendKeys("xxxxx"); driver.findElement(By.xpath("//*[@id='app']/section/form/div/div/button")).click(); System.out.println("The login process on lamdatest is completed"); } @Test(priority = 0, description = "this test validates the sign-up test") public void signUp() throws InterruptedException { WebElement link = driver.findElement(By.xpath("//a[text()='Free Sign Up']")); link.click(); WebElement organization = driver.findElement(By.xpath("//input[@name='organization_name']")); organization.sendKeys("LambdaTest"); WebElement firstName = driver.findElement(By.xpath("//input[@name='name']")); firstName.sendKeys("Test"); WebElement email = driver.findElement(By.xpath("//input[@name='email']")); email.sendKeys("User622@gmail.com"); WebElement password = driver.findElement(By.xpath("//input[@name='password']")); password.sendKeys("TestUser123"); WebElement phoneNumber = driver.findElement(By.xpath("//input[@name='phone']")); phoneNumber.sendKeys("9412262090"); WebElement termsOfService = driver.findElement(By.xpath("//input[@name='terms_of_service']")); termsOfService.click(); WebElement button = driver.findElement(By.xpath("//button[text()='Signup']")); button.click(); } @Test(priority = 3, alwaysRun = true, dependsOnMethods = "check_login", description = "this test validates the URL post logging in", groups = "url_validation") public void testCurrentUrl() throws InterruptedException { driver.findElement(By.xpath("//*[@id='app']/header/aside/ul/li[4]/a")).click(); String currentUrl = driver.getCurrentUrl(); assertEquals( currentUrl, "https://automation.lambdatest.com/timeline/?viewType=build&page=1", "url did not matched"); System.out.println("The url validation test is completed"); } @Test(priority = 1, description = "this test validates the logout functionality", timeOut = 25000) public void logout() throws InterruptedException { Thread.sleep(6500); driver.findElement(By.xpath("//*[@id='userName']")).click(); driver.findElement(By.xpath("//*[@id='navbarSupportedContent']/ul[2]/li/div/a[5]")).click(); } @Test(enabled = false) public void skipMethod() { System.out.println("this method will be skipped from the test run using the attribute enabled=false"); } @Test(priority = 6, invocationCount = 5, invocationTimeOut = 20) public void invocationcountShowCaseMethod() { System.out.println("this method will be executed by 5 times"); } @AfterMethod() public void screenshot() throws IOException { TakesScreenshot scr = ((TakesScreenshot) driver); File file1 = scr.getScreenshotAs(OutputType.FILE); FileUtils.copyFile(file1, new File(":\\test-output\\test1.PNG")); System.out.println("Screenshot of the test is taken"); } @AfterClass public void closeUp() { driver.close(); System.out.println("The close_up process is completed"); } @AfterTest public void reportReady() { System.out.println("Report is ready to be shared, with screenshots of tests"); } @AfterSuite public void cleanUp() { System.out.println("All close up activities completed"); } @BeforeGroups("urlValidation") public void setUpSecurity() { System.out.println("url validation test starting"); } @AfterGroups("urlValidation") public void tearDownSecurity() { System.out.println("url validation test finished"); } } 

Saída para o console:



Relatório TestNG:



Anotações adicionais no TestNG

Existem algumas anotações mais úteis para nos ajudar a alcançar nossos objetivos.

@DataProvider

Um método com esta anotação é usado para fornecer dados a um método de teste no qual o atributo dataProvider está definido. Esse método ajuda na criação de testes orientados a dados nos quais vários conjuntos de valores de entrada podem ser transmitidos. O método deve retornar uma matriz ou objeto bidimensional.

A anotação @DataProvider possui dois atributos:

  • nome - este atributo é usado para indicar o nome do provedor de dados. Se não especificado, o nome do método padrão será usado.
  • paralelo - esse atributo permite executar testes em paralelo com dados diferentes. Ter esse atributo é uma das vantagens do TestNG sobre o Junit. O padrão é falso.

O exemplo abaixo mostra o uso da anotação @DataProvider com o nome e os atributos paralelos.

 @DataProvider(name = "SetEnvironment", parallel = true) public Object[][] getData() { Object[][] browserProperty = new Object[][]{ {Platform.WIN8, "chrome", "70.0"}, {Platform.WIN8, "chrome", "71.0"} }; return browserProperty; } 

@Factory

Esta anotação ajuda a executar várias classes de teste através de uma classe de teste. Simplificando, ele define e cria testes dinamicamente.

O trecho de código abaixo mostra o uso da anotação @Factory , que ajuda a chamar métodos de classe de teste.

 import org.testng.annotations.Test; import org.testng.annotations.Factory; class FactorySimplyTest1 { @Test public void testMethod1() { System.out.println("This is to test for method 1 for Factor Annotation"); } } class FactorySimpleTest2 { @Test public void testMethod2() { System.out.println("This is to test for method 2 for Factor Annotation"); } } public class FactoryAnnotation { @Factory() @Test public Object[] getTestFactoryMethod() { Object[] factoryTest = new Object[2]; factoryTest[0] = new FactorySimplyTest1(); factoryTest[1] = new FactorySimpleTest2(); return factoryTest; } } 

Saída do console:



@Parameters

Esta anotação permite passar parâmetros para seus testes através do arquivo TestNG.xml. Isso é útil quando você precisa transferir uma quantidade limitada de dados para seus testes. Para conjuntos de dados complexos e grandes, é melhor usar a anotação @DataProvider ou Excel.

Exemplo de uso:

 @Parameters({"username", "password"}) @Test() public void checkLogin(String username, String password) { driver.get("https://accounts.lambdatest.com/login"); driver.findElement(By.xpath("//input[@name='email']")).sendKeys(username); driver.findElement(By.xpath("//input[@name='password']")).sendKeys(password); driver.findElement(By.xpath("//*[@id='app']/section/form/div/div/button")).click(); System.out.println("The login process on lamdatest is completed"); } 

No arquivo TestNG.xml, os parâmetros são definidos da seguinte maneira:

 <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd"> <suite name="Suite"> <test thread-count="5" name="Annotations"> <parameter name="username" value="sadhvisingh24@gmail.com" /> <parameter name="password" value="XXXXX" /> <classes> <class name="Parameter_annotation"/> </classes> </test> <!-- Annotations --> </suite> <!-- Suite --> 

@Listener

Esta anotação ajuda no registro e relatório. Existem vários ouvintes:

  • IExecutionListener
  • IAnnotationTransformer
  • ISuiteListener
  • ITestListener

Mas deixaremos a descrição desses ouvintes e seu uso para outro artigo.

Isso é tudo!

O ponto principal a considerar ao trabalhar com todas essas anotações e atributos é que você precisa usar o Java versão 1.5 ou superior, pois as anotações não são suportadas nas versões anteriores do java.

Todas as anotações acima e os atributos TestNG ajudam a melhorar a estrutura e a legibilidade do código. Isso ajuda a fornecer relatórios detalhados, o que torna os relatórios de status mais fáceis e mais úteis. O uso dessas anotações no TestNG for Selenium depende inteiramente de seus requisitos comerciais. Portanto, é importante escolher as anotações corretas e usá-las corretamente. Você pode tentar essas anotações no TestNG no LambdaTest Selenium Grid agora mesmo!

Aqueles que leram até o final são convidados para um webinar aberto gratuito, que será realizado em 14 de maio pelo nosso professor, Dmitry Eremin . Bem, de acordo com a tradição estabelecida, estamos aguardando seus comentários, amigos.

Source: https://habr.com/ru/post/pt450872/


All Articles