Les tests

Kécécé ?

Les différents types de test

Tests fonctionnels

Tests

End to End (e2e)

Tests unitaires

Tester :

- Assurer la non régression

- Documenter le code

- Avoir une appli plus fiable

- L’entièreté de l'appli est testée régulièrement

Utilité :

- Les cas normaux

- Les cas d'erreurs 

- Les cas "limites" (ceux qui doivent passer mais sont un peu chelou)

1. Les tests unitaires

  • Atomique
  • Nombreux
  • Rapide
function add(int $first, int $second)
{
  return $first + $second;
}

public function testAdd() 
{
  assertEquals(3, add(2, 1));
  assertEquals(0, add(2, -2));
  
  assertError(
    'Invalid argument type, string given, int expected', 
    add('1', 2)
  );
  assertError(
    'Invalid argument type, string given, int expected', 
    add('string', 2)
  );
}

2. Les tests fonctionnels

  • Teste une fonctionnalité (login, ajout au panier, etc...)
  • Moins nombreux que les tests unitaires
  • Plus long (besoin de fixtures, d'un serveur)
public function testIndex()
{
    $client = static::createClient();

    $crawler = $client->request('GET', '/hello/Fabien');

    $this->assertTrue($crawler->filter('html:contains("Hello Fabien!")')->count() > 0);
}
public function testLoginWithBadCredentials()
{
    $client = static::createClient();

    $crawler = $client->request('GET', '/login');
    
    $form = $crawler->selectButton('Se connecter')->form([
        'email' => 'john@doe.fr',
        'password' => 'fakepassword'
    ]);
    
    $client->submit($form);
    
    $this->assertResponseRedirects('/login');
    $this->assertSelectorExists('.alert.alert-danger');
}
 Scenario: Login with bad credentials
   Given I am on the homepage
   When I click "Login"
   And I fill in "email" with "john@doe.fr"
   And I fill in "password" with "fakepassword"
   And I press "Login"
   Then I should see "Invalid email or password" in element ".alert.alert-danger"
 Scenario: Index
   Given I am on "/hello/Fabien"
   Then I should see "Hello Fabien!"

3. Les tests End To End (e2e)

  • Teste une fonctionnalité complète
  • Principalement sur les fonctionnalité critiques
  • Plus long (besoin d'un navigateur, du javascript, de fixtures, d'un serveur)
it('can add a product to my cart and pay' () => {
  cy.visit('https://my-site.com')
  cy.contains('Nos produits').click()
  cy.url().should('include', '/products')
  cy.contains('Un super produit').click()
  cy.url().should('include', '/produits/un-super-produit')
  cy.get('#add-to-cart').click()
  cy.get('.alert.alert-success').should('contain', 'Produit ajouté au panier !')
  cy.contains('Voir mon panier').click()
  cy.url().should('include', '/mon-panier')
  cy.get('Commander').click()
  cy.url().should('include', '/payment/3ds/prestataire-de-paiment').waitReturn()
  cy.get('#order-payment-result').should('include', 'Votre paiment a bien été pris en compte.')
})

Behat

- Permet d'écrire les tests fonctionnels

- Utilise une syntax proche du langage parlé

- Peut être customisé

Dans un mode utopique, les POs écrivent les scénarios de test

Structure

Quelle est la fonctionnalité que je vais tester ? En tant que quel utilisateur ? Dans quel but ?

1. Feature

2. Scénario

Pour cette fonctionnalité, je peux avoir plusieurs comportements (succès, erreur, différente erreur, ...)

3. Step

Décris le scénario, étape par étape.

Feature

Feature: Login
    As a user
    In order to buy products
    I should be able to login and access the product list page
    
Feature: Administration
    As an admin
    In order to configurate my website
    I should be able to login and access the administration panel
    

Scénario

Scenario: Login
	Given I am on the homepage
	When I click on "Login"
	Then I should be on "/login"
	When I fill in "username" with "john@doe.fr"
	And I fill in "password" with "fakepassword"
	And I click on "Submit"
	Then I should be on "/"
	And I should see "Welcome John!"
	And I should see 20 ".product" elements
Scenario: I can add a product to my cart
	Given I am logged in
	And I am on "/products"
	When I click on ".products:fist .add-to-cart"
	And I click on "Voir mon panier"
	Then I should be on "/cart"
	And I should see 1 ".product-cart" element
	And I should see "25" in the "#cart-total" element

Step

Given I am on the homepage

When I click on "Login"
When I fill in "username" with "john@doe.fr"

Then I should be on "/login"
Then I should see "Welcome John!"

Les steps

Il existe différents types de steps : 

- D'état : "Given"

- D'action : "When"

- D'assertion : "Then"

 

 

Les types servent à structurer les scénarios, les rendre lisible et compréhensible pour le développeur.

Une step `Given I should see "Welcome John !"` n'a pas trop de sens.

Chaque step est liée à du code PHP.

Cette liaison est faite grâce aux Contextes.

Les contextes vont mapper les phrases des scénarios à des méthodes PHP grâce à des annotations

# dans le scenario
Given I am on the homepage
/**
* @Given I am on the homepage
*/
public function iAmOnTheHomepage() 
{
	$this->iAmOnThePage('/')
}

/**
* @Given I am on the page :path
*/
public function iAmOnThePage(string $path)
{
	$client = $this->getClient();
	$client->browse($path);
}

Il existe de nombreux contextes fait par la communauté : 

- JsonContext (pour vérifier un retour json)

- ApiContext (pour envoyer des requêtes et vérifer les réponses)

- BrowserContext (pour avoir des step de navigation comme sur un navigateur)

- FileContext 

- etc...

Il est aussi possible de créer son propre contexte, pour y ajouter des steps personalisées.

Par exemple sur Prometheus, nous avons réécris la step :

    /**
     * Add the "Accept: application/json" header and the "/api" prefix to the routes
     *
     * @When I send a request to :path
     * @When I send a :method request to :path
     * @When I send a :method request to :path with body:
     */
    public function iSendARequestTo(string $path, string $method = 'GET', PyStringNode $body = null): void
    {
        $this->apiContext->setRequestHeader('Accept', 'application/json');
        if ($body) {
            $this->apiContext->setRequestHeader('Content-Type', 'application/json');
            $this->apiContext->setRequestBody($body);
        }
        $this->apiContext->requestPath('/api'.$path, $method);
    }

On s'en sert comme ca :

When I send a request to "/catalogs"
# équivalent à
When I send a "GET" request to "/catalogs"

When I send a "PUT" request to "/currencies/1" with body:
"""
{
	"name": "moula"
}
"""

Les fixtures

Il existe différents moyen de charger des fixtures : 

- Charger exactement les fixtures requises pour le tests au début

- Charger un dump sql

- Sauvergarder plusieurs dumps et ne charger que ceux nécessaires

- Charger des fixtures via du yaml ou autres dans des fichiers externes

Quelques exemples :

Given the following fixtures are loaded:
	| currency     |
	| company      |
	| companyGroup |

Sur Prometheus

  /**
  * @Given the following fixtures are loaded:
  */
  public function theFollowingSeedersAreLoaded(TableNode $table): void
  {
      $this->resetDatabase();
      foreach ($table->getColumn(0) as $resourceName) {
          $class = 'Database\\Seeders\\'.ucfirst($resourceName).'Seeder';
          (new $class())->run();
      }
  }

Nous précisons au début du test de quelles données nous aurons besoin et notre step s'occupe de trouver le Seeder correspondant et de l'exécuter

Given the following fixtures are loaded:
"""
	App\Models\Currency:
		currency_eur:
			id: 1
			name: Euros
			iso_code: EUR
		currency_usd:
			id: 2
			name: Dollars
			iso_code: USD
	App\Model\User:
		user_{1..5}:
			id: <current()>
			username: <username()>
			age: <numberBetween(18, 90)>
			email: <email()>
"""

Avec une librairie externe

La librairie se charge de lire ce code et enregistrer en base de données.

Given the dump "some-dump.sql" is loaded

Avec un dump sql

On peut avoir une step custom qui se charge de trouver le dump dans un dossier pré défini, puis l'exécute.

La configuration

behat.yml

default:
  formatters:
    progress: true
    pretty: false
  suites:
    default:
      contexts:
        - FeatureContext
        - DatabaseContext
        - Imbo\BehatApiExtension\Context\ApiContext

  extensions:
    Imbo\BehatApiExtension:
      apiClient:
        base_uri: https://testing.prometheus-api.exotec
        timeout: 5.0
        verify: false

Les bonus

Background

Scenario: See the product list
	Given I am logged in with user "john"
	And I am on "/products"
	Then I should see 10 ".products" elements

Scenario: Add to cart
	Given I am logged in with user "john"
	And I am on "/products"
	When I click on "add to cart"
	Then I should see "Success"
Background:
	Given I am logged in with user "john"
	And I am on "/products"

Scenario: See the product list
	Then I should see 10 ".products" elements

Scenario: Add to cart
	When I click on "add to cart"
	Then I should see "Success"

Les steps au début sont identiques et répétées

Background s'occupe de relire les steps qu'il contient avant chaque scénario

Scenario Outline

Scenario: See product that exists
	Given I am on "/products/1"
	Then I should see "Product 1"
    
Scenario: See product that does not exists
	Given I am on "/products/9999999999"
	Then I should see "This product does not exist"
   
Scenario: See evil product
	Given I am on "/products/666"
	Then I should see "This product is evil!"
Scenario Outline: See product
	Given I am on "/products/<id>"
	Then I should see "<text>"
    
    Examples:
    	| id         | text                        |
        | 1          | Product 1                   |
    	| 9999999999 | This product does not exist |
        | 666        | This product is evil!       |

Les scenarios sont très similaires

Outline permet d'executer le même scenario plusieurs fois avec des données différentes

And et But

Pour apporter de la clarté, tous les différents types de step qu'on a vu, peuvent être étendu avec des "And" et "But"

Scenario: Balek
	Given I am logged in as "pg_gamer_59"
	And I am on "/"
	When I go to "/les-jeux-videos"
	And I click on "acheter"
	Then I should be on "/panier"
	And I should see 1 product in my cart
	But I should not see "Votre panier est vide"

Les hooks

Il est possible de se brancher avant ou apres, une feature, un scénario, une step ou toute la suite de test avec ces annotations :

- BeforeSuite
- AfterSuite
- BeforeFeature
- AfterFeature
- BeforeScenario
- AfterScenario
- BeforeStep
- AfterStep
/** @BeforeFeature */
public static function doWhateverYouNeed(FeatureEvent $event)
{
}

Les tags

Les features et les scénarios peuvent être taggés:

@account
Scenario: Login

@account
Scenario: Sign up

@account
Scenario: Bad login
 
@account
Scenario: Forgot password
	

Ca permet de regrouper les tests par parties de l'applications, splitter les tests en différents groupes sur la CI, ne pas retester toute la suite quand on developpe

Les tags + hooks

Les tags peuvent être combinés aux hooks pour faire des actions à certains moments seulement sur les scenarios taggés avec le bon tag

@database
Scenario: See products
/** 
* @BeforeScenario @database
*/
public static function resetDatabase(FeatureEvent $event)
{
	// drop and recreate database
}

Des questions ?

Je crois que c'est tout !

Les tests avec Behat

By keversc

Les tests avec Behat

  • 199