en laravel, test

Pruebas en Laravel – Inicio – Capa Repository

Intentare probar desde pruebas unitarias hasta las prueba integral

En esta primera parte veo la introducción a preparar el proyecto para Test y luego el enfoque de las pruebas será ir por cada capa por cada feature, en este caso el buscador de un producto, y probar cada capa: Repository, Domain, Controler y Vista.

Primero prueba del repositorio.

php artisan make:test ProductSearch/RepositoryProductSearchTest --unit

Para ejecutar específicamente la clase:

 phpunit --filter RepositoryProductSearchTest

Ejecutar las migraciones y copiar en la base de datos “<nombre database>_test”

Para usar la base de datos, se tiene que crear un archivo de entorno para testing

El comando para actualizar el cache de testing es:

php artisan config:cache --env=testing

Se hace el setup para el test con los datos que se necesitarán

class RepositoryProductSearchTest extends TestCase
{
    // trait para que los datos no afecten a la base de datos
    use DatabaseMigrations;

    //datos que se especificaran necesarios para las pruebas
    private $nameCategoriaPrueba = "CategoriaPrueba";
    //id resultante para comparar en las pruebas
    private $idCategory = 0;

    public function setUp(): void
    {
        //metodo padre de setup
        parent::setUp();

        //asi se llama a un factory, util para crear data
        $newCategory = factory(Category::class)->create([
            //el array sirve para especificar algunos datos
            Category::NAME=>$this->nameCategoriaPrueba
        ]);
        //resto de codigo...
        $this->idCategory = $newCategory->id;
        dd($newCategory);

Comando para crear el factory

php artisan make:factory CategoryFactory --model Category

Una forma de crear el factory seria:

<?php

use Faker\Generator as Faker;

$factory->define(App\User::class, function (Faker $faker) {
    $user = new \App\Models\User();
    $user->name = $faker->name;
    $user->email = $faker->email;
    $user->rol = $faker->numberBetween(1,4);
    $user->password = bcrypt($faker->password);
    $user->estado = \ConstantesUserEstado::HABILITADO;
    
    return $user->toArray();
});

En caso tenga relación a otra tabla, se puede llamar al factory de cada modelo:

<?php

$factory->define(Product::class, function (Faker $faker) {
    $newProduct = new Product();
    $newProduct->name=$faker->name;
    ....
    //relations
    $newProduct->Market_Seller_users_id= factory(User::class)->create()->id;
    $newProduct->Market_Seller_id=factory(Seller::class)->create([
        Seller::USERS_ID=>$newProduct->Market_Seller_users_id
    ])->id;
    $newProduct->Market_id = factory(Market::class)->create([
        Market::SELLER_ID=>$newProduct->Market_Seller_id,
        Market::SELLER_USERS_ID=>$newProduct->Market_Seller_users_id
    ])->id;

    return $newProduct->toArray();
});

En como se definió puede ocurrir un comportamiento no esperado, al momento de llamar a este factory ya se crea la instancia de Seller, y si se sobrescribe las propiedades, el Seller ya fue creado, para que no lo cree hasta que se termine de definir los parámetros, se puede definir como function:

<?php

use Faker\Generator as Faker;
use App\Models\ProductPresentation;
use App\Models\Product;

$factory->define(ProductPresentation::class, function (Faker $faker) {
    
    /*
    ANTES
    $newProductPresentation = new ProductPresentation();
    $newProductPresentation->media_type = "jpg";
    $newProductPresentation->file_url=$faker->imageUrl();
    $newProductPresentation->Product_id=factory(Product::class)->create()->id;
    $newProductPresentation->is_principal=$faker->boolean;
    $newProductPresentation->orden=$faker->numberBetween();
    $newProductPresentation->name_ref=$faker->name;

    return $newProductPresentation->toArray();
    */
    return [
        ProductPresentation::MEDIA_TYPE=>ConstantesProductPresentationMediaType::IMG,
        ProductPresentation::FILE_URL=>$faker->imageUrl(),
        ProductPresentation::PRODUCT_ID=>function () {
            return Product::factory()->create()->id;
        },
        ProductPresentation::IS_PRINCIPAL=>$faker->boolean,
        ProductPresentation::ORDEN=>$faker->numberBetween(),
        ProductPresentation::NAME_REF=>$faker->name
    ];
});

para ir probado el test, se puede llamar especificando la clase y evitar llamar todo

// Para ejecutar solo un test:
phpunit --filter methodName path/to/file.php

// Para ejecutar todos los tests de una clase:
phpunit --filter RepositoryProductSearchTest

Antes de llamar la DB se tiene que crear la DB para test, se puede llenar con el comando de migrate mas el parámetro de env testing:

php artisan migrate --env=testing

Ahora el archivo repositorio quedaría :

La ubicación seria dentro de una carpeta por cada caso de uso

Describo cada parte del archivo

...
/**
 ...
 * author twiter @JavierTwiteando
 *
 * (1)
 * phpunit --filter RepositoryProductSearchTest --colors
 */
class RepositoryProductSearchTest extends TestCase
{
    // (2) trait para que los datos no afecten a la base de datos
    use DatabaseTransactions;

    //config
    //(3)
    private $sizePage = 3;
    ...
    private $productsName = [
        "A",
        "B",
        "C",
        "D",
        "E",
        "F",
        "1",
        "2"
    ];
    private $productsCreateAt = [
        "12/09/2020",
        "12/10/2020",
        "12/11/2020",
        "12/12/2020",
        "12/01/2021",
        "12/02/2021",
        "12/03/2021",
        "12/04/2021",
    ];
    ...
    public function setUp(): void
    {
        //metodo padre de setup
        parent::setUp();
        //(4)
        ...
        $newCategories = factory(Category::class,$this->sizeCategories)->create();
        $newUser = factory(User::class)->create([
            User::ROL=>\ConstantesUserRol::SELLER
        ]);
        ...
        //(3) para las comprobaciones
        $this->categorys = $newCategories;
        $this->categoryEmpty = $newCategoryEmpty;
    }
    //(5)
    function getRepositoryProduct():RepositoryProduct{
        return app(RepositoryProduct::class);
    }
    
    /** (6)
     * @test
     */
    public function search_category_exist()
    {
        //(7)
        $repositoryProduct = $this->getRepositoryProduct() ;

        //llamar al repositorio con el filtro de la categoria
        $results=$repositoryProduct
            ->addFilterCategory($this->categorys[0]->id)
            ->setPaginateSize($this->sizePage)
            ->search();

        $this->assertNotNull($results,'el resultado fue nulo');
        $this->assertEquals($this->sizeProductsByCategory,$results->total());
        $this->assertEquals($repositoryProduct->getPaginateSize(), sizeof( $results->items() ));
    }
    ...
    /**
     * phpunit --filter order_by_most_recents ./tests/Unit/ProductSearch/RepositoryProductSearchTest.php --colors
     *
     * @test
     */
    public function order_by_most_recents()
    {
        $repositoryProduct = $this->getRepositoryProduct();

        //llamar al repositorio con el filtro de la categoria
        $results=$repositoryProduct
            ->addFilterCategory($this->categorys[0]->id)
            ->orderBy(\ConstantesProductSearchOrderBy::MAS_NUEVOS)
            ->search();

        $first_item = Product::cast($results->items()[0]);
        $second_item = Product::cast($results->items()[1]);

        $this->assertNotNull($results,"el resultado fue nulo");
        $this->assertEquals($first_item->created_at, Carbon::parse($this->productsCreateAt[7]));
        $this->assertLessThan($first_item->created_at,$second_item->created_at);
    }
    ...
}

(1) Anoto cada comando necesario para probar cada parte del archivo.
(2) Se usa el trait para no guardar los datos generados para la prueba en cada test.
(3) Se anota argumentos para la generación de la data y setear el estado que tendrá el sistema para las pruebas(tamaño de paginado, fechas de creación de productos, cantidad de categorías,etc) , también se declara variables donde se guardará los datos generados y luego ser comparados.
(4) Se crea lo datos según los argumentos del estado del sistema con los factorys sobrescribiendo los datos necesarios.
(5) Se crea una clase para obtener una instancia del repositorio para las pruebas.
(6) Se crea cada función para los test indicando la anotación @test para que lo reconozca como método para testing.
(7) Se obtiene el repositorio, se indica los datos del estado del sistema a probar y se verifica con lo assert que cumpla lo esperado.

Se corren los test hasta refactorizarlos y pasarlos, primero uno por uno

y luego todos

Listo, ahora toca probar el dominio en el siguiente post (aquí el link cuando este listo)

Referencias:

Video con un ejemplo de inyección de dependencias:
https://www.youtube.com/watch?v=uARTy_sJrIo

Opciones de formas de castear un objecto a un tipo de clase:
https://stackoverflow.com/a/54481076/15149489

Articulo clave para entender los mocks:
https://kordes.dev/posts/laravel-ioc-mocking-tests-practical-example

Factorys
https://laravel.com/docs/5.3/database-testing#factory-states

Escriba un comentario

Comentario