Kécécé ?
Tests fonctionnels
Tests
End to End (e2e)
Tests unitaires
- 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
- Les cas normaux
- Les cas d'erreurs
- Les cas "limites" (ceux qui doivent passer mais sont un peu chelou)
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)
);
}
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!"
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.')
})
- 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
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: 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
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
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!"
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"
}
"""
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
Given the following fixtures are loaded:
| currency |
| company |
| companyGroup |
/**
* @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()>
"""
La librairie se charge de lire ce code et enregistrer en base de données.
Given the dump "some-dump.sql" is loaded
On peut avoir une step custom qui se charge de trouver le dump dans un dossier pré défini, puis l'exécute.
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
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: 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
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"
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 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 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
}
Je crois que c'est tout !