Rédaction efficace des autotests - tests sous-cutanés

Imaginons une situation hypothétique (dans laquelle on plonge régulièrement). Vous avez été affecté à un projet pour l'automatisation «gash». On vous donne un énorme plan de test avec un grand nombre (des milliers d'entre eux!) De tests «manuels», et ils disent que vous devez faire quelque chose, et là. Et aussi, rapidement et de manière stable.

Il est trop tard pour écrire des tests unitaires, ou même penser à TDD , le code produit est écrit depuis longtemps. Votre parole, camarade autotesteur!

image

Heureusement, il existe une petite astuce qui vous permettra d'augmenter la couverture et de rendre les tests stables et rapides - les tests sous-cutanés («tests sous-cutanés»), mais tout d'abord.


L'essence du problème


Le premier réflexe conditionnel de l'automate est de prendre du sélénium (enfin, soit là, du séléniure, soit un autre prodige pour les tests d'interface utilisateur). Il s'agit d'une telle norme de l'industrie, mais il existe de nombreuses raisons pour lesquelles elle ne décolle pas:

  • Les tests d'interface utilisateur sont lents. Il n'y a aucune échappatoire à cela. Ils peuvent être exécutés en parallèle, classés et exécutés un peu plus rapidement, mais ils resteront lents.
  • Les tests d'interface utilisateur sont instables. En partie parce qu'ils sont lents. Et aussi parce que le navigateur Web et l'interface utilisateur n'ont pas été créés pour être contrôlés par un ordinateur (cette tendance est en train de changer, mais ce n'est pas le cas).
  • UI- — . . ( , , , «» - , ).
  • , , , UI- . . ID XPath . , «» - - . , , — - , .
  • Quelqu'un dira que certaines fonctionnalités ne peuvent tout simplement pas être testées autrement. Je dirai que s'il existe des fonctionnalités qui ne peuvent être testées que par des tests d'interface utilisateur (à l'exception de la logique d'interface utilisateur elle-même), cela peut être un bon signe de problèmes architecturaux dans le produit.

Le seul véritable avantage des tests d'interface utilisateur est qu'ils vous permettent de «lancer» des vérifications plus ou moins utiles sans avoir à plonger et à étudier le code du produit lui-même. Ce qui n'est guère un avantage à long terme. Une explication plus détaillée des raisons pour lesquelles cela peut être entendu dans cette présentation .

Solution alternative


Dans un cas très simple, considérons une application consistant en un formulaire où vous pouvez entrer un nom d'utilisateur valide. Si vous entrez un nom d'utilisateur qui correspond aux règles - L'utilisateur sera créé dans le système et enregistré dans la base de données.



Le code source de l'application peut être trouvé ici: github.com/senpay/login-form . Vous avez été averti - dans l'application, il y a beaucoup de bugs et il n'y a pas d'outils ni de frameworks à la mode.

Si vous essayez de "jeter" une feuille de contrôle pour cette application, vous pouvez obtenir quelque chose comme:
NombrePasRésultats attendus
11. Entrez un nom d'utilisateur valide
2. Cliquez sur le bouton "Connexion"
1.
2. Un nouvel utilisateur est créé.
21. Entrez un nom d'utilisateur vide
2. Cliquez sur le bouton "Connexion"
1.
2. Le message d'erreur s'affiche.

Cela a-t-il l'air simple? Simplement! Puis-je écrire des tests d'interface utilisateur? Pouvez. Un exemple des tests écrits (avec un framework complet à trois niveaux ) peut être trouvé dans LoginFormTest.java si vous allez au label uitests dans 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());
    }
}


Quelques mesures pour ce code:
Temps d'exécution: ~ 12 secondes (12 secondes 956 millisecondes la dernière fois que j'ai exécuté ces tests) Classe de
couverture de code
: 100%
Méthode: 93,8% (30/32)
Ligne: 97,4% (75/77 )

maintenant , nous allons supposer que autotests fonctionnels peuvent être écrites au niveau « juste en dessous de » l'interface utilisateur. Cette technique est appelée tests sous-cutanés («tests sous-cutanés» - tests qui testent immédiatement en dessous du niveau de la logique d'affichage) et a été proposée par Martin Fowler il y a assez longtemps [ 1 ].

Lorsque les gens pensent à des autotests «non UI», ils pensent souvent immédiatement à REST / SOAP ou à son API. Mais l'API (Application Programming Interface) est un concept beaucoup plus large, n'affectant pas nécessairement HTTP et d'autres protocoles lourds.

Si nous choisissons un code produit , nous pouvons trouver quelque chose d'intéressant:
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;
    }
}


Lorsque nous cliquons sur une interface utilisateur, l'une de ces méthodes est appelée, ou un nouvel objet utilisateur est ajouté, ou une liste d'objets utilisateur déjà créés est renvoyée. Et si nous utilisons directement ces méthodes ? Après tout, c'est une véritable API! Et surtout, REST et d'autres API fonctionnent également sur le même principe - ils appellent une certaine méthode de "niveau contrôleur".

En utilisant ces méthodes directement, nous pouvons écrire un test plus simple et meilleur:
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());
    }
}


Ce code est disponible sous les sous- tests d' étiquette :

git checkout subctests


Essayons de collecter des métriques?
Temps d'exécution: ~ 21 millisecondes
Couverture du code :
Classe: 77,8%
Méthode: 78,1 (30/32)
Ligne: 78,7 (75/77)

Nous avons perdu un peu de couverture, mais la vitesse des tests a augmenté 600 fois !!!

Dans quelle mesure la perte de couverture est-elle importante / significative dans ce cas? Dépend de la situation. Nous avons perdu un petit code de colle, ce qui peut être (ou ne pas être) important (je recommande de déterminer quel code est perdu comme exercice).

Cette perte de couverture justifie-t-elle l'introduction de tests lourds au niveau de l'interface utilisateur? Cela dépend aussi de la situation. On peut par exemple:
  • Ajoutez un test d'interface utilisateur pour vérifier le code de colle, ou
  • Si nous ne nous attendons pas à des changements fréquents du code de colle - laissez-le sans autotests, ou
  • Si nous avons une sorte de test «manuel», il y a de fortes chances que le testeur remarque des problèmes avec le code de colle, ou
  • Trouvez autre chose (même déploiement des Canaries)


Finalement


  • Il n'est pas nécessaire d'écrire les autotests fonctionnels au niveau de l'interface utilisateur ou de l'API REST / SOAP. L'utilisation de «tests sous-cutanés» dans de nombreuses situations permettra de tester la même fonctionnalité avec une plus grande vitesse et stabilité.
  • L'un des inconvénients de l'approche est une certaine perte de couverture.
  • Une façon d'éviter de perdre la couverture est le « modèle de tests de fonctionnalités »
  • Mais même avec la perte de couverture, l'augmentation de la vitesse et de la stabilité est significative.


Une version anglaise de l'article est disponible ici .

All Articles