Skip to content

Instantly share code, notes, and snippets.

@xthiago
Last active December 29, 2020 18:17
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save xthiago/11c3adb15e95bffb9c1b423602808660 to your computer and use it in GitHub Desktop.
Save xthiago/11c3adb15e95bffb9c1b423602808660 to your computer and use it in GitHub Desktop.
Testes com ID

Tem uns testes aqui que acredito que a legibilidade seria muito melhor se tivéssemos uma comparação de strings entre a resposta esperada e a efetiva:

// [..] setup dos dados
$firstPage = actionToFetchFirstPage();
$expectedFirstPage = <<<EOL
{
    "data":[
        {
            "name":"xthiago",
            "files":[
                {
                    "id":"3207c5d2-a9a7-4c56-bc57-2a4249e11357",
                    "name": "File 1",
                    "uri":"https://google.com/file1"
                },
                {
                    "id":"3207c5d2-a9a7-4c56-bc57-2a4249e11357",
                    "name": "File 2",
                    "uri":"https://google.com/file1"
                }
            ]
        }
    ]
}
EOL;

$this->assertEquals($expectedFirstPage, $firstPage);

O problema é com a forma como o arquivo é adicionado:

$user = new User();
$user->addFile('File1', 'https://google.com/file1');
$user->addFile('File2', 'https://google.com/file2');

A implementação de User::addFile é algo como:

class User
{
    public function addFile(
        string $fileName,
        string $fileUri
    ): void {

        $file = File::create(
            Id::generate(), // <-- ID generado
            $fileName,
            $fileUri,
            $this->id(), // <-- File precisa do Id ser User.
        );

        $this->files->add($file);
    }
}

Com esse approach a construção do Value Object File é responsabilidade do método user::addFile. O contrato fica simples e encapsulado nele.

O grande problema reside no gerador de ID (Id::generate()) que trará UUIDs aleatórios.

Possibilidades para resolver isso:

Abordagem 1

Inverter a dependência e fazer com que o ID seja passado para File::addFile .

class User
{
    public function addFile(
        Id $fileId,
        string $fileName,
        string $fileUri
    ): void { /* .. */ }
}

// código cliente
$user->addFile($aGivenId ?? Id::generate(), $fileId, $fileUri);

Abordagem 2

Fazer com que File::addFile receba uma instância de File pronta.

class User
{
    public function addFile(
        File $newFile
    ): void 
  {
    if (! $newFile->belongsTo($this->id)) {
      throw new InvalidArgumentException();
    }
  
    $this->files[] = $newFile;
  }
}

// código cliente
$aFile = new File(
  $aGivenId ?? Id::generate(), $fileId, $fileUri,
  $fileId,
  $fileUri,
  $user->id()
);
$user->addFile($aFile);

Abordagem 3

Alterar a implementação de Id::generate para considerar diferentes estratégias de geração de ID. Haveria um método para definir a estratégia a ser usada:

// Define uma estratégia de geração de ID. Se o método não for acionado, a estratégia padrão será usada (uuid).
Id::setGenerationStrategy(GenerationStrategy $strategy);

// Estratégias possíveis:

- `UUIDGenerator` - usa um gerador de UUID - padrão.
- `InMemorySequenceGenerator` - gera IDs inteiros positivos de forma sequencial
- `CallableGenerator`- recebe uma função responsável por gerar segundo seus próprios critérios

As 2 últimas estratégias são interessantes para testes. O teste original poderia ser refatorado para algo como:

Id::setGenerationStrategy(new InMemorySequenceGenerator());
// [..] setup dos dados
$firstPage = actionToFetchFirstPage();
$expectedFirstPage = <<<EOL
{
    "data":[
        {
            "name":"xthiago",
            "files":[
                {
                    "id":"1",
                    "name": "File 1",
                    "uri":"https://google.com/file1"
                },
                {
                    "id":"2",
                    "name": "File 2",
                    "uri":"https://google.com/file1"
                }
            ]
        }
    ]
}
EOL;

$this->assertEquals($expectedFirstPage, $firstPage);
@xthiago
Copy link
Author

xthiago commented Dec 23, 2020

Lembrando que o Id do agregado User já é recebido pelo construtor e totalmente controlado nos testes unitários. O cerne da questão aqui é sobre um value object do agregado (File).

Todas abordagens tem prós e contras. Qual seu feeling?

@Wikiko
Copy link

Wikiko commented Dec 23, 2020

Acho que a abordagem 3 é a mais interessante, estava pensando na segunda abordagem em usar a instância de File porém sem o user id e depois adiciona-la dentro do método addFile porém ai ele perde a questão de ser imutável e também provavelmente cria uma complexidade bem grande pra ficar verificando se tem user.id por que o campo passa a ser "nullable". A terceira abordagem acho boa, não é tão chata na questão de testes tipo algo que incomode e para o DX (experiencia do dev) no uso do método addFile acho que fica bem bacana, encapsulada a responsabilidade de como adicionar o File ao usuário e como que o usuário gerencia os ids desses Files.

@adrianuf22
Copy link

O valor do id não parece tão importante para esse teste e como a asserção é feita a sobre strings, eu usaria o assertStringMatchesFormat em vez de assertEquals e usaria %s em vez do valor da Id.

@dominguesguilherme
Copy link

dominguesguilherme commented Dec 29, 2020

Falando no padrão Value Object eu acredito ainda que possa haver uma 4ª abordagem, que é não levar o Id em consideração nos testes, já que um VO não deveria possuir uma identidade.

Claro que eu estou falando isso sem levar o domínio em consideração, mas no seu exemplo do teste você tem um User com 2 File onde:

  • Os 2 files possuem nomes diferentes apontando pra mesma URI.
    • A comparação em um VO necessariamente se dá por todos os valores que o compõe (Igualdade estrutural)

Então sobre esse ponto, é realmente relevante retornar o Id na resposta do método que está sendo testado (actionToFetchFirstPage)?

Se o Id é relevante para o domínio e/ou para o cliente, dentre as 3 abordagens, eu provavelmente optaria pela 2ª, onde é necessário receber o VO como argumento.
O lado ruim dessa abordagem é o "boierplate" gerado para construir o File nos serviços responsáveis.

Já a terceira abordagem me parece estar desconexa do domínio e para facilitar os testes é necessário incluir uma "complexidade" técnica.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment