Indíce

Nessa série vamos criar uma API RESTful totalmente testável que servirá um aplicativo criado usando Ionic. O app terá apenas um propósito: avaliar livros. Nós iremos falar desde o desenvolvimento até o deploy de tanto nossa API quanto de nosso app.

A primeira coisa que vamos fazer é criar um novo projeto com o Laravel. Você pode ver como fazer isso aqui. Eu também irei usar a máquina virtual Homestead para desenvolver nossa API — é muito fácil de instalar. Dê uma olhada nisso.

Setup Básico

Agora vamos escrever código. A primeira coisa que eu irei fazer é configurar um arquivo .env — algo assim:


APP_ENV=local
APP_DEBUG=true
APP_KEY=base64:FaFQysRPBTvIwMhaYMgGcXG5T8x8Wd2cj64dzGtDdX8=
APP_URL=http://localhost

DB_CONNECTION=mysql
DB_HOST=192.168.10.10
DB_PORT=3306
DB_DATABASE=bookapp
DB_USERNAME=homestead
DB_PASSWORD=secret

CACHE_DRIVER=file
SESSION_DRIVER=file
QUEUE_DRIVER=sync

REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379

MAIL_DRIVER=smtp
MAIL_HOST=mailtrap.io
MAIL_PORT=2525
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null

Isso está configurado para usar um banco de dados MySQL (ou MariaDB, eu uso MariaDB) que já vem com o Laravel Homestead. Nós não iremos usar isso por enquanto, portanto apenas esqueça este arquivo.

Agora vamos configurar nosso ambiente de testes. Isso é particularmente importante porque será usado toda vez que rodarmos nossos testes e no nosso serviço de Integração Contínua (falaremos disso em alguns minutos). Para isso, abra o arquivo phpunit.xml. Você verá algo assim:


<?xml version="1.0" encoding="UTF-8"?>
<phpunit backupGlobals="false"
         backupStaticAttributes="false"
         bootstrap="bootstrap/autoload.php"
         colors="true"
         convertErrorsToExceptions="true"
         convertNoticesToExceptions="true"
         convertWarningsToExceptions="true"
         processIsolation="false"
         stopOnFailure="false">
    <testsuites>
        <testsuite name="Application Test Suite">
            <directory suffix="Test.php">./tests</directory>
        </testsuite>
    </testsuites>
    <filter>
        <whitelist processUncoveredFilesFromWhitelist="true">
            <directory suffix=".php">./app</directory>
            <exclude>
                <file>./app/Http/routes.php</file>
            </exclude>
        </whitelist>
    </filter>
    <php>
        <env name="APP_ENV" value="testing"/>
        <env name="CACHE_DRIVER" value="array"/>
        <env name="SESSION_DRIVER" value="array"/>
        <env name="QUEUE_DRIVER" value="sync"/>
    </php>
</phpunit>

Nós devemos configurar variáveis de ambiente que serão usadas apenas em testes. Para fazer isso, precisamos apenas adicionar algumas novas tags env. Precisamos fazer duas coisas: rodar nossos testes usando SQLite e rodá-los na memória — isso é importante porque não queremos usar um banco de dados real. Para fazer isso, adicione as variáveis DB_DATABASE e DB_CONNECTION. Assim:

<?xml version="1.0" encoding="UTF-8"?>
<phpunit backupGlobals="false"
         backupStaticAttributes="false"
         bootstrap="bootstrap/autoload.php"
         colors="true"
         convertErrorsToExceptions="true"
         convertNoticesToExceptions="true"
         convertWarningsToExceptions="true"
         processIsolation="false"
         stopOnFailure="false">
    <testsuites>
        <testsuite name="Application Test Suite">
            <directory suffix="Test.php">./tests</directory>
        </testsuite>
    </testsuites>
    <filter>
        <whitelist processUncoveredFilesFromWhitelist="true">
            <directory suffix=".php">./app</directory>
            <exclude>
                <file>./app/Http/routes.php</file>
            </exclude>
        </whitelist>
    </filter>
    <php>
        <env name="APP_ENV" value="testing"/>
        <env name="CACHE_DRIVER" value="array"/>
        <env name="SESSION_DRIVER" value="array"/>
        <env name="QUEUE_DRIVER" value="sync"/>
        <env name="DB_CONNECTION" value="sqlite" />
        <env name="DB_DATABASE" value=":memory:" />
    </php>
</phpunit>

Agora, vamos pensar um pouco — do que o nosso app realmente precisa? Bom, ele precisa de uma tabela reviews, certo? Também seria legal ter uma tabela chamada books para guardarmos alguns livros e seus autores. Vamos começar criando a tabela books. Rode o comando php artisan make:model Book -m — esse comando cria um model chamado Book e a flag -m cria uma migration para esse model.

Agora, vamos configurar nossa migration. Vá para o arquivo recém-criado e adicione um campo string title e string author. O último deve ser nullable. Assim:


<?php 

public function up()
{
    Schema::create('books', function (Blueprint $table) {
        $table->increments('id');
        $table->string('name');
        $table->string('author')->nullable();
        $table->timestamps();
    });
}

?>

Também é necessário criar a migration da tabela reviews. Essa tabela deve ter os seguintes campos: string title, text description, text additional_information, integer note, integer user_id (uma referência à nossa tabela de usuários) e integer book_id (referência a um livro).

Algo assim:

<?php

public function up()
{
    Schema::create('reviews', function (Blueprint $table) {
        $table->increments('id');
        $table->string('title');
        $table->text('description');
        $table->text('additional_information')->nullable();
        $table->integer('note');
        $table->integer('user_id')->unsigned();
        $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
        $table->integer('book_id')->unsigned();
        $table->foreign('book_id')->references('id')->on('books')->onDelete('cascade');
        $table->timestamps();
    });
}

?>

Agora que temos nossos models configurados, nós nem precisamos rodar o comando php artisan migrate — isso será feito automaticamente nos testes.

Como TDD funciona

Test-Driven Development é uma metodologia que funciona basicamente assim: escreva um teste para uma funcionalidade — ele irá falhar. Agora, implemente essa funcionalidade, faça o teste passar, e depois refatore o código. Isso melhora a qualidade do seu código por diversas razões: nós fazemos algo com um objetivo “definido”, nós seguimos padrões e refatoramos o código constantemente.

Escrevendo Nosso Primeiro Teste

Agora é que a diversão começa! Nós vamos escrever um teste simples que verifica se livros podem ser criados e coisas do tipo. Para fazer isso, rode o comando php artisan make:test Unit/BookTest. Um arquivo deve ser criado em tests/Unit/BookTest.php, e você deve receber um Test created successfully no seu Terminal.

Ao abrir o arquivo, você perceberá que ele vem com um método chamado testExample. Ele apenas faz uma asserção de que true é verdadeiro — e, como é , o teste passa. O PHPUnit tem vários métodos de asserções e nós iremos usá-los constantemente durante a série.

Você pode deletar esse método. Agora, crie um método chamado testABookCanBeCreated. Você também pode usar underscore ao invés de camelCase — eu prefiro usar underscore. Então o nome deve ser test_a_book_can_be_created.

Nesse método, nós queremos ter certeza que um livro pode ser criado. Para isso, nós iremos fazer o seguinte:

Você provavelmente já sabe como criar e encontrar livros usando os métodos do Eloquent. No entanto, existem outros dois métodos importantes do PHPUnit que iremos usar: assertEquals e seeInDatabase. assertEquals basicamente faz uma asserção de que alguma coisa é igual à outra, seeInDatabase verifica se um array de valores pode ser encontrado numa tabela.
Nosso arquivo deve ficar mais ou menos assim:

<?php

use App\Book;
use Illuminate\Foundation\Testing\DatabaseMigrations;

class BookTest extends TestCase
{
    use DatabaseMigrations;

    public function test_a_book_can_be_created()
    {
        $book = Book::create(['name' => 'The Hobbit', 'author' => 'J.R.R Tolkien']);

        $latest_book = Book::latest()->first();

        $this->assertEquals($book->id, $latest_book->id);
        $this->assertEquals('The Hobbit', $latest_book->name);
        $this->assertEquals('J.R.R Tolkien', $latest_book->author);

        $this->seeInDatabase('books', ['id' => 1, 'name' => 'The Hobbit', 'author' => 'J.R.R Tolkien']);

    }
}


Note que importamos duas classes: Book, nossa entidade, e DatabaseMigrations, um trait que iremos usar para que as migrations rodem antes de qualquer teste. Portanto, não esqueca de usá-la.

O método assertEquals é bem simples, ele aceita dois argumentos (alguns outros não importantes) e simplesmente checa se eles são iguais. Se não forem, o teste irá falhar. O método seeInDatabase aceita uma tabela como primeiro argumento e um array de valores no segundo. Nesse caso, procuraremos na tabela books por um registro com ['id' => 1, 'name' => 'The Hobbit', 'author' => 'J.R.R Tolkien'].

Recebendo nosso primeiro erro

Agora, rode phpunit no seu Terminal — você notará que o teste falhará. Você deve receber algo assim:

PHPUnit 4.8.24 by Sebastian Bergmann and contributors.

.E

Time: 400 ms, Memory: 12.00Mb

There was 1 error:

1) BookTest::test_a_book_can_be_created
Illuminate\Database\Eloquent\MassAssignmentException: name

/Users/mateus1/Dev/aulas/api-tests-post/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Model.php:449
/Users/mateus1/Dev/aulas/api-tests-post/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Model.php:281
/Users/mateus1/Dev/aulas/api-tests-post/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Model.php:569
/Users/mateus1/Dev/aulas/api-tests-post/tests/Unit/BookTest.php:13
phar:///usr/local/Cellar/phpunit/4.5.0/libexec/phpunit-4.5.0.phar/phpunit/TextUI/Command.php:152
phar:///usr/local/Cellar/phpunit/4.5.0/libexec/phpunit-4.5.0.phar/phpunit/TextUI/Command.php:104

FAILURES!
Tests: 2, Assertions: 2, Errors: 1.

Bom, recebemos um erro.. isso é, na verdade, bom. Nossos testes nos guiarão ao erro e iremos corrigí-lo. Note que a Exception recebida foi MassAssignmentException: name — isso ocorreu porque tentamos usar o método create (Mass Assignment) sem preencher a propriedade $fillable. Então, vamos configurar isso. Vá ao arquivo app/Book.php e crie um array $fillable com os campos que podem ser preenchidos.

<?php
// app/Book.php  

namespace App;

use Illuminate\Database\Eloquent\Model;

class Book extends Model
{
    protected $fillable = ['name', 'author'];
}

Agora, rode o comando phpunit novamente. Você deve receber isso:

PHPUnit 4.8.24 by Sebastian Bergmann and contributors.

..

Time: 382 ms, Memory: 12.00Mb

OK (2 tests, 6 assertions)  

Agora o teste está passando!
Você talvez tenha notado que o PHPUnit nos diz que existem dois testes — isso ocorre porque eu ainda não removi o teste ExampleTest. Você pode deletá-lo sem problemas. Nas próximas etapas, nós vamos configurar o Travis CI e no post seguinte iremos de fato testar nossa API.

Configurando o Travis CI

Travis CI é um servico de integracão contínua. Ele basicamente configura uma máquina virtual com softwares que você queria (PHP, Ruby, Node, etc) e roda comandos que você define. Ele irá rodar os comandos toda vez que você “enviar” código para o seu repositório Git. O Travis é pago para projetos privados. Se você está escrevendo um projeto open source, entre em travis-ci.org. Se você está escrevendo projetos pessoais, acesse travis-ci.com. Eles ofereem licenca de estudante, que eu, por exemplo, uso. Obrigado Travis! :)

Então comecaremos escrevendo um arquivo .env.travis. Ele será usado ao rodar testes no Travis. Apenas escreva algo assim:

APP_ENV=local
APP_DEBUG=true
APP_KEY=base64:FaFQysRPBTvIwMhaYMgGcXG5T8x8Wd2cj6

DB_HOST=127.0.0.1
DB_DATABASE=book_test
DB_USERNAME=travis
DB_PASSWORD=

CACHE_DRIVER=file
SESSION_DRIVER=file
QUEUE_DRIVER=sync

MAIL_DRIVER=smtp
MAIL_HOST=mailtrap.io
MAIL_PORT=2525
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null

Agora crie um arquivo chamado .travis.yml e coloque isso nele:


language: php

php:
  - 7.0

before_script:
  - composer self-update
  - composer install
  - mysql -e 'create database book_test;'
  - cp .env.travis .env
  - php artisan key:generate
  - php artisan migrate

script: phpunit

Nós estamos dizendo o seguinte para o Travis:

Basicamente, isso é Integração Contínua. Toda vez que dermos um git push, o Travis irá rodar nossos testes automaticamente. :)