> For the complete documentation index, see [llms.txt](https://docs.skail.dev/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.skail.dev/construa/como-fazer/testar-seus-fluxos.md).

# Testar seus fluxos

Como escrever testes unitários de uma function e seus commands sem conectar ao skail: o pacote de testes simula o skail em memória, executa a function e deixa você inspecionar o que foi emitido e gravado.

## Pré-requisitos

{% tabs %}
{% tab title="C#" %}
No projeto de testes:

```xml
<PackageReference Include="Skail.Platform.Runtime.Testing" Version="2.2.*" />
<PackageReference Include="xunit" Version="2.5.3" />
```

Referência ao projeto que contém as functions.
{% endtab %}

{% tab title="TypeScript" %}
O pacote `@skail/testing`, que traz a `InMemorySkailPlatform` para testes em memória. O exemplo usa vitest.
{% endtab %}
{% endtabs %}

## A function sob teste

{% tabs %}
{% tab title="C#" %}

```csharp
public class FaturamentoService
{
    private readonly IGatewayPagamento _gateway;
    private readonly IRepositorioFaturas _faturas;

    public FaturamentoService(IGatewayPagamento gateway, IRepositorioFaturas faturas)
    {
        _gateway = gateway;
        _faturas = faturas;
    }

    [SkailFunction]
    public async SkailTask EmitirFatura(Guid faturaId)
    {
        var valor = await CarregarValor(faturaId);
        var resultado = await CobrarCartao(faturaId, valor);
        if (resultado == ResultadoCobranca.Aprovado) await MarcarComoPaga(faturaId);
    }

    [SkailCommand]
    public async SkailTask<decimal> CarregarValor(Guid faturaId) => await _faturas.ObterValorAsync(faturaId);

    [SkailCommand(retryCount: 8)]
    public async SkailTask<ResultadoCobranca> CobrarCartao(Guid faturaId, decimal valor) => await _gateway.CobrarAsync(faturaId, valor);

    [SkailCommand]
    public async SkailTask MarcarComoPaga(Guid faturaId) => await _faturas.MarcarPagaAsync(faturaId);
}
```

{% endtab %}

{% tab title="TypeScript" %}

```typescript
import { SkailCommand, SkailFunction } from "@skail/runtime";

export type ResultadoCobranca = "Aprovado" | "Recusado";

export interface GatewayPagamento {
  cobrar(faturaId: string, valor: number): Promise<ResultadoCobranca>;
}

export interface RepositorioFaturas {
  obterValor(faturaId: string): Promise<number>;
  marcarPaga(faturaId: string): Promise<void>;
}

export class FaturamentoService {
  constructor(
    private readonly gateway: GatewayPagamento,
    private readonly faturas: RepositorioFaturas,
  ) {}

  @SkailFunction()
  async EmitirFatura(faturaId: string): Promise<void> {
    const valor = await this.CarregarValor(faturaId);
    const resultado = await this.CobrarCartao(faturaId, valor);
    if (resultado === "Aprovado") await this.MarcarComoPaga(faturaId);
  }

  @SkailCommand()
  async CarregarValor(faturaId: string): Promise<number> {
    return await this.faturas.obterValor(faturaId);
  }

  @SkailCommand({ retryCount: 8 })
  async CobrarCartao(faturaId: string, valor: number): Promise<ResultadoCobranca> {
    return await this.gateway.cobrar(faturaId, valor);
  }

  @SkailCommand()
  async MarcarComoPaga(faturaId: string): Promise<void> {
    await this.faturas.marcarPaga(faturaId);
  }
}
```

{% endtab %}
{% endtabs %}

As dependências entram pelo construtor como interfaces, para que o teste substitua por dublês. Essa é a regra de desenho que torna functions testáveis: a function decide, os commands fazem as operações de I/O por interfaces.

## O teste

{% tabs %}
{% tab title="C#" %}

```csharp
using Skail.Platform.Runtime.Testing;
using Microsoft.Extensions.DependencyInjection;
using Xunit;

public class EmitirFaturaTests
{
    [Fact]
    public async Task Cobranca_aprovada_marca_fatura_como_paga()
    {
        // Arrange
        var platform = UnitTestSkailPlatform.Create();
        var broker   = platform.ServiceProvider.GetRequiredService<UnitTestMessageBroker>();
        var ids      = platform.ServiceProvider.GetRequiredService<ITaskIdProvider>();

        // dublês das dependências
        var gateway = new GatewayFake(resultado: ResultadoCobranca.Aprovado);
        var faturas = new RepositorioFaturasFake(valor: 149.90m);

        var faturaId = Guid.NewGuid();
        var trigger = EventFrameHelper.CreateTrigger<FaturamentoService>(s => s.EmitirFatura(faturaId), ids);

        await broker.Produce(trigger, new Grpc.Core.Metadata(), new MessageId());

        // Act
        await platform.SimulateConsumeAndWaitTaskCompletion();

        // Assert: o efeito aconteceu
        Assert.True(faturas.FoiMarcadaComoPaga(faturaId));

        // Assert: o que o runtime emitiu
        var comandos = broker.Commands.ToList();
        Assert.Contains(comandos, c => c.Kind == MessageBrokerCommandKind.SaveState);

        // Assert: os passos registrados
        var estados = broker.GetAllStates(trigger.TargetTaskId);
        Assert.Equal(EventType.Rpc, estados[0].EventType);       // início da function
        Assert.True(estados.Count >= 4);                          // início + três commands
    }

    [Fact]
    public async Task Cobranca_recusada_nao_marca_como_paga()
    {
        var platform = UnitTestSkailPlatform.Create();
        var broker   = platform.ServiceProvider.GetRequiredService<UnitTestMessageBroker>();
        var ids      = platform.ServiceProvider.GetRequiredService<ITaskIdProvider>();
        var gateway  = new GatewayFake(resultado: ResultadoCobranca.Recusado);
        var faturas  = new RepositorioFaturasFake(valor: 149.90m);

        var faturaId = Guid.NewGuid();
        var trigger = EventFrameHelper.CreateTrigger<FaturamentoService>(s => s.EmitirFatura(faturaId), ids);
        await broker.Produce(trigger, new Grpc.Core.Metadata(), new MessageId());

        await platform.SimulateConsumeAndWaitTaskCompletion();

        Assert.False(faturas.FoiMarcadaComoPaga(faturaId));
    }
}
```

{% endtab %}

{% tab title="TypeScript" %}

```typescript
import { describe, expect, it } from "vitest";
import { EventType, SkailCommand, SkailFunction } from "@skail/runtime";
import { createSkailWorker } from "@skail/runtime/testing";
import { InMemorySkailPlatform } from "@skail/testing";

type ResultadoCobranca = "Aprovado" | "Recusado";

interface GatewayPagamento {
  cobrar(faturaId: string, valor: number): Promise<ResultadoCobranca>;
}

interface RepositorioFaturas {
  obterValor(faturaId: string): Promise<number>;
  marcarPaga(faturaId: string): Promise<void>;
}

class FaturamentoService {
  constructor(
    private readonly gateway: GatewayPagamento,
    private readonly faturas: RepositorioFaturas,
  ) {}

  @SkailFunction()
  async EmitirFatura(faturaId: string): Promise<void> {
    const valor = await this.CarregarValor(faturaId);
    const resultado = await this.CobrarCartao(faturaId, valor);
    if (resultado === "Aprovado") await this.MarcarComoPaga(faturaId);
  }

  @SkailCommand()
  async CarregarValor(faturaId: string): Promise<number> {
    return await this.faturas.obterValor(faturaId);
  }

  @SkailCommand({ retryCount: 8 })
  async CobrarCartao(faturaId: string, valor: number): Promise<ResultadoCobranca> {
    return await this.gateway.cobrar(faturaId, valor);
  }

  @SkailCommand()
  async MarcarComoPaga(faturaId: string): Promise<void> {
    await this.faturas.marcarPaga(faturaId);
  }
}

// dublês das dependências
class GatewayFake implements GatewayPagamento {
  constructor(private readonly resultado: ResultadoCobranca) {}
  async cobrar(): Promise<ResultadoCobranca> {
    return this.resultado;
  }
}

class RepositorioFaturasFake implements RepositorioFaturas {
  readonly pagas = new Set<string>();
  constructor(private readonly valor: number) {}
  async obterValor(): Promise<number> {
    return this.valor;
  }
  async marcarPaga(faturaId: string): Promise<void> {
    this.pagas.add(faturaId);
  }
  foiMarcadaComoPaga(faturaId: string): boolean {
    return this.pagas.has(faturaId);
  }
}

function criarAmbiente(resultado: ResultadoCobranca) {
  const platform = new InMemorySkailPlatform();
  const faturas = new RepositorioFaturasFake(149.9);
  const gateway = new GatewayFake(resultado);
  const worker = createSkailWorker({
    workload: "faturamento:1.0.0",
    types: [FaturamentoService],
    // dublês das dependências
    providers: [{ token: FaturamentoService, useFactory: () => new FaturamentoService(gateway, faturas) }],
    sidecar: platform,
  });
  return { platform, faturas, worker };
}

describe("EmitirFatura", () => {
  it("cobranca aprovada marca fatura como paga", async () => {
    // Arrange
    const { platform, faturas, worker } = criarAmbiente("Aprovado");
    const faturaId = crypto.randomUUID();
    const taskId = platform.start("faturamento:1.0.0/EmitirFatura", [faturaId]);

    // Act
    const outcome = await worker.runActivation((await platform.consume())!);

    // Assert: o efeito aconteceu
    expect(outcome.status).toBe("completed");
    expect(faturas.foiMarcadaComoPaga(faturaId)).toBe(true);

    // Assert: os passos registrados
    const estados = platform.history(taskId);
    expect(estados[0]!.eventType).toBe(EventType.Rpc);         // início da function
    expect(estados).toHaveLength(4);                            // início + três commands
    expect(estados.slice(1).every((e) => e.eventType === EventType.Durable)).toBe(true);

    // Assert: o que o runtime confirmou à plataforma
    expect(platform.acknowledgements).toHaveLength(1);
  });

  it("cobranca recusada nao marca como paga", async () => {
    const { platform, faturas, worker } = criarAmbiente("Recusado");
    const faturaId = crypto.randomUUID();
    platform.start("faturamento:1.0.0/EmitirFatura", [faturaId]);

    await worker.runActivation((await platform.consume())!);

    expect(faturas.foiMarcadaComoPaga(faturaId)).toBe(false);
  });
});
```

{% endtab %}
{% endtabs %}

Veja o que cada parte faz:

{% tabs %}
{% tab title="C#" %}
`UnitTestSkailPlatform.Create()` monta um runtime em memória, sem conexão com o skail. `EventFrameHelper.CreateTrigger<T>(...)` cria a mensagem de início da function, do mesmo jeito que um trigger real. `broker.Produce(...)` entrega a mensagem ao runtime de teste. `SimulateConsumeAndWaitTaskCompletion()` consome, executa a function e todos os commands, e espera terminar. Depois, `broker.Commands` tem cada interação do runtime com a plataforma de teste (Produce, SaveState, Consume, Ack, Nak) e `broker.GetAllStates(taskId)` tem os passos registrados, na ordem.
{% endtab %}

{% tab title="TypeScript" %}
`new InMemorySkailPlatform()` monta um runtime em memória, sem conexão com o skail. `platform.start(...)` cria o trigger da function. `platform.consume()` entrega a ativação, e `worker.runActivation(...)` executa a function e os commands dela. Depois, `platform.history(taskId)` devolve os passos registrados, na ordem.
{% endtab %}
{% endtabs %}

## O que dá e o que não dá para testar

Em teste unitário, você consegue testar:

* o caminho da function (quais commands rodaram, em que ordem, com que argumentos);
* o efeito nos dublês;
* os passos registrados.

O resto depende do pacote de testes de cada linguagem:

{% tabs %}
{% tab title="C#" %}
Dá para testar também o comportamento sob falha injetada em um command com `FaultInjection`, mas não a hibernação.

Uma function que alcança um delay ou uma espera por evento não satisfeita lança "Hibernation is not allowed in the unit test.". Para esses fluxos, teste até o ponto de espera e extraia a lógica de decisão para métodos puros, testáveis sem a plataforma; a espera em si fica para um teste de integração contra um environment de desenvolvimento.
{% endtab %}

{% tab title="TypeScript" %}
Uma function que chega a um delay ou a uma espera não satisfeita termina a ativação com `status: "hibernated"`, e o teste avança com `platform.complete(...)` ou `platform.signal(...)`.
{% endtab %}
{% endtabs %}

{% hint style="info" %}
Teste os commands como código comum: são métodos que chamam interfaces; um teste direto com o dublê basta, sem a plataforma de testes.
{% endhint %}

## Erros comuns

| Sintoma                                                 | Causa                                                                     | Correção                                                                      |
| ------------------------------------------------------- | ------------------------------------------------------------------------- | ----------------------------------------------------------------------------- |
| (C#) "Hibernation is not allowed in the unit test."     | A function chegou a um delay ou a uma espera por evento                   | Teste até o ponto de espera, ou em integração                                 |
| (C#) "The Builder Factory can only be registered once!" | Dois `UnitTestSkailPlatform.Create()` no mesmo processo sem isolamento    | Uma plataforma por processo de teste; compartilhe a instância entre os testes |
| (C#) Function não é encontrada                          | Assembly de teste ou de produção sem `[assembly: VisibleToSkailPlatform]` | Adicione o atributo                                                           |

## Próximos passos

<table data-view="cards"><thead><tr><th></th><th></th><th></th><th data-hidden data-card-target data-type="content-ref"></th></tr></thead><tbody><tr><td><i class="fa-bug">:bug:</i></td><td><strong>Como depurar com Time Travel Debug</strong></td><td>Para reproduzir uma execução real.</td><td><a href="/construa/como-fazer/depurar-com-time-travel-debug.md">Depurar com Time Travel Debug</a></td></tr><tr><td><i class="fa-diagram-project">:diagram-project:</i></td><td><strong>Modelo de programação</strong></td><td>Para a divisão que torna a function testável.</td><td><a href="/construa/conceitos/functions-e-commands.md">Functions e commands</a></td></tr><tr><td><i class="fa-box">:box:</i></td><td><strong>Pacotes</strong></td><td></td><td><a href="/referencia/sdk-csharp/pacotes.md">Pacotes</a></td></tr></tbody></table>
