Como escrever autotestes de maneira eficaz - testes subcutâneos

Vamos imaginar uma situação hipotética (na qual mergulhamos regularmente). Você foi atribuído a um projeto para "cortar" a automação. Você recebe um grande plano de teste com um grande número (milhares deles!) De testes "manuais", e eles dizem que você precisa fazer alguma coisa, e logo ali. E também, de forma rápida e estável.

É tarde demais para escrever testes de unidade, ou mesmo pensar em TDD , o código do produto já foi escrito. Sua palavra, camarada autotester!

imagem

Felizmente, existe um pequeno truque que permitirá aumentar a cobertura e tornar os testes estáveis ​​e rápidos - Testes subcutâneos (“testes subcutâneos”), mas primeiro as primeiras coisas.


A essência do problema


O primeiro reflexo condicional do automatizador é fazer o Selenium (bem, lá, Selenide ou algum outro prodígio para testes de interface do usuário). Esse é um padrão do setor, mas há muitos motivos pelos quais ele não decola:

  • Os testes de interface do usuário são lentos. Não há como escapar disso. Eles podem ser executados em paralelo, arquivados e executados um pouco mais rápido, mas permanecerão lentos.
  • Os testes de interface do usuário são instáveis. Em parte porque são lentos. E também porque o navegador da Web e a interface do usuário não foram criados para serem controlados por um computador (essa tendência está mudando atualmente, mas não o fato de que isso é bom).
  • UI- — . . ( , , , «» - , ).
  • , , , UI- . . ID XPath . , «» - - . , , — - , .
  • Alguém dirá que algumas funcionalidades simplesmente não podem ser testadas de outra forma. Eu direi que, se houver funcionalidade que possa ser testada apenas por testes de interface do usuário (com exceção da própria lógica da interface do usuário), isso pode ser um bom sinal de problemas de arquitetura no produto.

A única vantagem real dos testes de interface do usuário é que eles permitem que você “faça” verificações mais ou menos úteis sem a necessidade de mergulhar e estudar o código do próprio produto. O que dificilmente é uma vantagem a longo prazo. Uma explicação mais detalhada de por que isso pode ser ouvido nesta apresentação .

Solução alternativa


Como um caso muito simples, vamos considerar um aplicativo que consiste em um formulário no qual você pode inserir um nome de usuário válido. Se você digitar um nome de usuário que corresponda às regras - o usuário será criado no sistema e registrado no banco de dados.



O código fonte do aplicativo pode ser encontrado aqui: github.com/senpay/login-form . Você foi avisado - no aplicativo existem muitos erros e não existem ferramentas e estruturas da moda.

Se você tentar "lançar" uma folha de verificação para este aplicativo, poderá obter algo como:
NúmeroPassosResultados esperados
1 11. Digite um nome de usuário válido
2. Clique no botão "Login"
1.
2. Um novo usuário é criado.
21. Digite um nome de usuário vazio
2. Clique no botão "Login"
1.
2. A mensagem de erro é dada.

Parece simples? Simplesmente! Posso escrever testes de interface do usuário? Pode. Um exemplo dos testes escritos (junto com uma estrutura completa de três níveis ) pode ser encontrado em LoginFormTest.java se você for para o rótulo uitests no git ( git checkout uitests ):

public class LoginFormTest {

    SelenideMainPage sut = SelenideMainPage.INSTANCE;
    private static final String APPLICATION_URL = "http://localhost:4567/index";

    @BeforeClass
    public static void setUpClass() {
        final String[] args = {};
        Main.main(args);
        Configuration.browser = "firefox";
    }

    @Before
    public void setUp() {
        open(APPLICATION_URL);
    }

    @After
    public void tearDown() {
        close();
    }

    @Test
    public void shouldBeAbleToAddNewUser() {
        sut.setUserName("MyCoolNewUser");
        sut.clickSubmit();
        Assert.assertEquals("Status: user MyCoolNewUser was created", sut.getStatus());
        Assert.assertTrue(sut.getUsers().contains("Name: MyCoolNewUser"));
    }

    @Test
    public void shouldNotBeAbleToAddEmptyUseName() {
        final int numberOfUsersBeforeTheTest = sut.getUsers().size();
        sut.clickSubmit();
        Assert.assertEquals("Status: Login cannot be empty", sut.getStatus());
        Assert.assertEquals(numberOfUsersBeforeTheTest, sut.getUsers().size());
    }
}


Algumas métricas para este código:
Tempo de execução: ~ 12 segundos (12 segundos 956 milissegundos na última vez em que executei esses testes)
Cobertura de código
Classe: 100%
Método: 93,8% (30/32)
Linha: 97,4% (75/77 )

Agora, vamos supor que os Autotestes funcionais possam ser gravados no nível "imediatamente abaixo" da interface do usuário. Essa técnica é chamada de testes subcutâneos ("testes subcutâneos" - testes que testam imediatamente abaixo do nível da lógica de exibição) e foi proposta por Martin Fowler há muito tempo [ 1 ].

Quando as pessoas pensam em autotestes "sem interface do usuário", geralmente pensam imediatamente no REST / SOAP ou em sua API. Mas a API (Application Programming Interface) é um conceito muito mais amplo, não afetando necessariamente o HTTP e outros protocolos pesados.

Se escolhermos um código de produto , podemos encontrar algo interessante:
public class UserApplication {

    private static IUserRepository repository = new InMemoryUserRepository();
    private static UserService service = new UserService(); {
        service.setUserRepository(repository);
    }

    public Map<String, Object> getUsersList() {
        return getUsersList("N/A");
    }

    public Map<String, Object> addUser(final String username) {
        final String status = service.addUser(username);
        final Map<String, Object> model = getUsersList(status);
        return model;
    }

    private Map<String, Object> getUsersList(String status) {
        final Map<String, Object> model = new HashMap<>();
        model.put("status", status);
        model.put("users", service.getUserInfoList());
        return model;
    }
}


Quando clicamos em uma interface do usuário, um desses métodos é chamado, ou um novo objeto Usuário é adicionado, ou uma lista de objetos Usuário já criados é retornada. E se usarmos esses métodos diretamente ? Afinal, esta é uma API real! E o mais importante, o REST e outras APIs também funcionam com o mesmo princípio - eles chamam um determinado método de "nível de controlador".

Usando esses métodos diretamente, podemos escrever um teste mais simples e melhor:
public class UserApplicationTest {

    private UserApplication sut;

    @Before
    public void setUp() {
       sut = new UserApplication();
    }

    @Test
    public void shouldBeAbleToAddNewUser() {
        final Map<String, Object> myCoolNewUser = sut.addUser("MyCoolNewUser");
        Assert.assertEquals("user MyCoolNewUser was created", myCoolNewUser.get("status"));
        Assert.assertTrue(((List) myCoolNewUser.get("users")).contains("Name: MyCoolNewUser"));
    }

    @Test
    public void shouldNotBeAbleToAddEmptyUseName() {
        final Map<String, Object> usersBeforeTest = sut.getUsersList();
        final int numberOfUsersBeforeTheTest = ((List) usersBeforeTest.get("users")).size();
        final Map<String, Object> myCoolNewUser = sut.addUser("");
        Assert.assertEquals("Login cannot be empty", myCoolNewUser.get("status"));
        Assert.assertEquals(numberOfUsersBeforeTheTest, ((List) myCoolNewUser.get("users")).size());
    }
}


Este código está disponível sob os subtestes de etiquetas :

git checkout subctests


Vamos tentar coletar métricas?
Tempo de execução: ~ 21 milissegundos
Cobertura de código :
Classe: 77,8%
Método: 78,1 (30/32)
Linha: 78,7 (75/77)

Perdemos um pouco de cobertura, mas a velocidade dos testes aumentou 600 vezes !!!

Quão importante / significante é a perda de cobertura neste caso? Depende da situação. Perdemos um pouco de código de cola, que pode ser (ou não) importante (eu recomendo determinar qual código é perdido como exercício).

Essa perda de cobertura justifica a introdução de testes pesados ​​no nível da interface do usuário? Também depende da situação. Podemos, por exemplo:
  • Adicione um teste de interface do usuário para verificar o código de cola ou
  • Se não esperamos mudanças frequentes no código de cola - deixe-o sem autoteste ou
  • Se tivermos algum tipo de teste "manual", há uma grande chance de que os problemas com o código de cola sejam notados pelo testador, ou
  • Crie outra coisa (a mesma implantação do Canary)


Eventualmente


  • Os autotestes funcionais não precisam ser gravados no nível da interface do usuário ou da API REST / SOAP. O uso de “testes subcutâneos” em muitas situações testará a mesma funcionalidade com maior velocidade e estabilidade.
  • Uma das desvantagens da abordagem é uma certa perda de cobertura.
  • Uma maneira de evitar a perda de cobertura é o " Modelo de testes de recursos "
  • Mas mesmo com a perda de cobertura, o aumento de velocidade e estabilidade é significativo.


Uma versão em inglês do artigo está disponível aqui .

All Articles