Alors, comment ne pas souffrir de tests fonctionnels?

J'ai été invité à écrire cet article en discutant des rapports de Heisenbug 2021 dans notre chat d'entreprise. Cela est dû au fait qu'une grande attention est portée à l'écriture "correcte" des tests. Entre guillemets - parce que sur le papier, tout est vraiment logique et raisonné, mais dans la pratique, ces tests s'avèrent plutôt lents.





Cet article s'adresse plutôt aux débutants en programmation, mais peut-être que quelqu'un pourra s'inspirer de l'une des approches décrites ci-dessous.





Je pense que tout le monde connaît les principes des bons tests:





  • , .. (, HTTP- )





  • , ..





  • , .. CI





, : pipeline 3000 !






, , , .. . .





API :





  1. ( )





  2. ( )





  3. HTTP-









, , . HTTP API, API.





( , ) , . , , : Redis/RabbitMQ HTTP , .





, DI- .





:





{
  "method": "patch",
  "uri": "/v2/project/17558/admin/items/physical_good/sku/not_existing_sku",
  "headers": {
    "Authorization": "Basic MTc1NTg6MTIzNDVxd2VydA=="
  },
  "data": {
    "name": {
      "en-US": "Updated name",
      "ru-RU": " "
    }
  }
}
      
      



{
  "status": 404,
  "data": {
    "errorCode": 4001,
    "errorMessage": "[0401-4001]: Can not find item with urlSku = not_existing_sku and project_id = 17558",
    "statusCode": 404,
    "transactionId": "x-x-x-x-transactionId-mock-x-x-x"
  }
}
      
      



<?php declare(strict_types=1);

namespace Tests\Functional\Controller\Version2\PhysicalGood\AdminPhysicalGoodPatchController;

use Tests\Functional\Controller\ControllerTestCase;

class AdminPhysicalGoodPatchControllerTest extends ControllerTestCase
{
    public function dataTestMethod(): array
    {
              return [
                // Negative cases
                'Patch -- item doesn\'t exist' => [
                        '001_patch_not_exist'
                ],
            ];
    }
}
      
      



:





TestFolder
├── Fixtures
│   └── store
│   │   └── item.yml
├── Request
│   └── 001_patch_not_exist.json
├── Response
│   └── 001_patch_not_exist.jsonTables
│   └── 001_patch_not_exist
│       └── store
│           └── item.yml
└── AdminPhysicalGoodPatchControllerTest.php
      
      



, . json yml ( ), ( ).





...

, , , , .





1.

— , .





( , ), . , , .





— .





— , ? .. ?





, 1 , , , , ! .





2.

, . , ( ).





667 . . , ?





, , CI-.





#!/usr/bin/env bash

if [[ ! -f "dump-cache.sql" ]]; then
    echo 'Generating dump'
    #     
    migrations_dir="./migrations" sh ./scripts/helpers/fetch_migrations.sh
    #    
    migrations_dir="./migrations" host="percona" sh ./scripts/helpers/migrate.sh

    #        (store, delivery)
    mysqldump --host=percona --user=root --password=root \
      --databases store delivery \
      --single-transaction \
      --no-data --routines > dump.sql

    cp dump.sql dump-cache.sql
else
    echo 'Extracting dump from cache'
    cp dump-cache.sql dump.sql
fi
      
      



, CI . - , git- .





CI-job (gitlab)
build migrations:
  stage: build
  image: php72:1.4
  services:
    - name: percona:5.7
  cache:
    key:
      files:
        - scripts/helpers/fetch_migrations.sh
    paths:
      - dump-cache.sql
  script:
    - bash ./scripts/ci/prepare_ci_db.sh
  artifacts:
    name: "$CI_PROJECT_NAME-$CI_COMMIT_REF_NAME"
    paths:
      - dump.sql
    when: on_success
    expire_in: 30min

      
      



3.

. , , . :









  1. :





















19 ( 27 ) 10 ( ): 10 18 .





:





  • , . , DI-.





  • AUTO INCREAMENT , TRUNCATE. , .





public static function setUpBeforeClass(): void
{
        parent::setUpBeforeClass();
        foreach (self::$onSetUpCommandArray as $command) {
            self::getClient()->$command(self::getFixtures());
        }
}

...

/**
 * @dataProvider dataTestMethod
 */
public function testMethod(string $caseName): void
{
        /** @var Connection $connection */
        $connection = self::$app->getContainer()->get('doctrine.dbal.prodConnection');
        $connection->beginTransaction();
        
        $this->traitTestMethod($caseName);
        $this->assertTables(\glob($this->getCurrentDirectory() . '/Tables/' . $caseName . '/**/*.yml'));
        
        $connection->rollBack();
}

      
      



4.

API , , .. . / , , ( , ).





:





  • , , . , - , .





dbunit, . , .





public function tearDown(): void
{
        parent::tearDown();
        //       DB-  
        //        
        self::$onSetUpCommandArray = [];
}

public static function tearDownAfterClass(): void
{
        parent::tearDownAfterClass();
        self::$onSetUpCommandArray = [
            Client::COMMAND_TRUNCATE,
            Client::COMMAND_INSERT
        ];
}

      
      



5.

— , . , . , .





pipeline’, .





pipeline’ ( testsuite phpunit). .





<testsuite name="functional-v2">
        <directory>./../../tests/Functional/Controller/Version2</directory>
</testsuite>
      
      



functional-v2:
  extends: .template_test
  services:
    - name: percona:5.7
  script:
    - sh ./scripts/ci/migrations_dump_load.sh
    - ./vendor/phpunit/phpunit/phpunit --testsuite functional-v2 --configuration config/test/phpunit.ci.v2.xml --verbose
      
      



, , , paratest. .





, .. . , ( ), , .. - .





:





  • CI —





  • , -





  • , - ( , ) . CI, .





...

6.

, . . , , . - bootstrap , .





( ). , , , .. DI- (, - , ..).





, , . , .





interface StateResetInterface
{
    public function resetState();
}
      
      



$container = self::$app->getContainer();
foreach ($container->getKnownEntryNames() as $dependency) {
        $service = $container->get($dependency);
        if ($service instanceof StateResetInterface) {
                $service->resetState();
        }
}
      
      




L'écriture de tests est toujours le même compromis que l'écriture de l'application proprement dite. Il est nécessaire de partir du fait que pour vous est plus prioritaire, et ce qui peut être donné. On nous parle souvent unilatéralement des tests «idéaux», qui en réalité peuvent être difficiles à mettre en œuvre, le travail est lent ou le soutien demande beaucoup de travail.





Après toutes les optimisations, le temps d'exécution dans CI pour les tests fonctionnels a diminué à 12-15 minutes. Bien sûr, je doute que les techniques décrites ci-dessus soient utiles dans leur forme originale, mais j'espère qu'elles m'ont inspiré et m'ont donné mes propres idées!





Quelle approche utilisez-vous pour rédiger des tests? Avez-vous besoin de les optimiser et quelles méthodes avez-vous utilisées?








All Articles