TypeScript

Boas Práticas de Mocks com TypeScript: jest-mock-extended e Tipagem de Dependências para Times Ágeis

16 min de leitura

Boas Práticas de Mocks com TypeScript: jest-mock-extended e Tipagem de Dependências para Times Ágeis

O Problema: Por Que Mocks São Essenciais em Testes Quando você escreve testes unitários, precisa isolar a unidade de código sob teste. Isso significa que dependências externas — serviços HTTP, bancos de dados, APIs terceirizadas — não devem ser executadas durante o teste. Se você deixar essas dependências reais rodarem, seu teste se torna frágil, lento e dependente de fatores externos que podem falhar por razões não relacionadas ao seu código. Mocks são objetos que simulam o comportamento de dependências reais. Eles permitem que você controle exatamente o que é retornado, qual exceção é lançada e quantas vezes uma função foi chamada. TypeScript torna isso ainda mais poderoso porque você pode ter mocks que respeitam a tipagem original, garantindo que seu código de teste também seja seguro em tempo de compilação. Introdução ao jest-mock-extended O é uma biblioteca que estende as capacidades nativas do Jest para criar mocks tipados e inteligentes. Enquanto o Jest oferece básico, o fornece , que

<h2>O Problema: Por Que Mocks São Essenciais em Testes</h2>

<p>Quando você escreve testes unitários, precisa isolar a unidade de código sob teste. Isso significa que dependências externas — serviços HTTP, bancos de dados, APIs terceirizadas — não devem ser executadas durante o teste. Se você deixar essas dependências reais rodarem, seu teste se torna frágil, lento e dependente de fatores externos que podem falhar por razões não relacionadas ao seu código.</p>

<p>Mocks são objetos que simulam o comportamento de dependências reais. Eles permitem que você controle exatamente o que é retornado, qual exceção é lançada e quantas vezes uma função foi chamada. TypeScript torna isso ainda mais poderoso porque você pode ter mocks que respeitam a tipagem original, garantindo que seu código de teste também seja seguro em tempo de compilação.</p>

<h2>Introdução ao jest-mock-extended</h2>

<p>O <code>jest-mock-extended</code> é uma biblioteca que estende as capacidades nativas do Jest para criar mocks tipados e inteligentes. Enquanto o Jest oferece <code>jest.fn()</code> básico, o <code>jest-mock-extended</code> fornece <code>mock&lt;T&gt;()</code>, que cria um mock totalmente tipado de qualquer interface ou classe.</p>

<p>Instale a biblioteca com:</p>

<pre><code class="language-bash">npm install --save-dev jest-mock-extended</code></pre>

<p>A grande vantagem é que você obtém autocompletar e verificação de tipos em tempo de desenvolvimento. Você não consegue chamar um método que não existe na interface original, e o TypeScript te avisa imediatamente se tentar acessar uma propriedade inexistente.</p>

<h3>Exemplo Básico: Criando um Mock Tipado</h3>

<p>Imagine que você tem um serviço de autenticação:</p>

<pre><code class="language-typescript">// auth.service.ts

export interface IAuthService {

login(email: string, password: string): Promise&lt;string&gt;;

logout(): Promise&lt;void&gt;;

isAuthenticated(): boolean;

}

export class AuthService implements IAuthService {

async login(email: string, password: string): Promise&lt;string&gt; {

// implementação real

return &quot;token123&quot;;

}

async logout(): Promise&lt;void&gt; {

// implementação real

}

isAuthenticated(): boolean {

// implementação real

return true;

}

}</code></pre>

<p>Agora você quer testar um componente que depende desse serviço:</p>

<pre><code class="language-typescript">// user.component.ts

export class UserComponent {

constructor(private authService: IAuthService) {}

async handleLogin(email: string, password: string): Promise&lt;void&gt; {

const token = await this.authService.login(email, password);

if (token) {

console.log(&quot;Login bem-sucedido&quot;);

}

}

}</code></pre>

<p>Com <code>jest-mock-extended</code>, o teste fica assim:</p>

<pre><code class="language-typescript">// user.component.test.ts

import { mock } from &quot;jest-mock-extended&quot;;

import { UserComponent } from &quot;./user.component&quot;;

import { IAuthService } from &quot;./auth.service&quot;;

describe(&quot;UserComponent&quot;, () =&gt; {

it(&quot;deve realizar login com sucesso&quot;, async () =&gt; {

// Criar um mock totalmente tipado

const authServiceMock = mock&lt;IAuthService&gt;();

// Configurar o comportamento esperado

authServiceMock.login.mockResolvedValue(&quot;token-valido&quot;);

// Injetar o mock no componente

const component = new UserComponent(authServiceMock);

// Executar a lógica

await component.handleLogin(&quot;user@example.com&quot;, &quot;senha123&quot;);

// Verificar se a função foi chamada com os argumentos corretos

expect(authServiceMock.login).toHaveBeenCalledWith(

&quot;user@example.com&quot;,

&quot;senha123&quot;

);

});

});</code></pre>

<p>O <code>mock&lt;IAuthService&gt;()</code> já sabe que <code>login</code> existe, que retorna uma Promise, e que <code>mockResolvedValue</code> é o método correto para um método assíncrono. Se você tentasse chamar um método inexistente, TypeScript reclamaria ainda na escrita do código.</p>

<h2>Tipagem de Dependências e Padrões de Injeção</h2>

<p>Tipar corretamente suas dependências é o alicerce para criar mocks eficientes. Existem diferentes abordagens, cada uma com seus trade-offs.</p>

<h3>Interfaces vs Classes: Qual Usar?</h3>

<p>Use <strong>interfaces</strong> quando a dependência é um contrato (o &quot;quê&quot; ela faz). Use <strong>classes abstratas</strong> quando você precisa de lógica compartilhada. Para testes, interfaces são preferíveis porque são mais leves e não carregam implementação.</p>

<pre><code class="language-typescript"></code></pre>

<p>Por que isso importa? Porque quando você precisa testar <code>OrderService</code>, pode passar um mock que implementa <code>IDatabase</code> sem se preocupar com conexões reais com Postgres.</p>

<h3>Constructor Injection vs Property Injection</h3>

<p><strong>Constructor Injection</strong> é o padrão recomendado. As dependências são explícitas, imutáveis após a construção e fáceis de testar:</p>

<pre><code class="language-typescript"></code></pre>

<p>Isso é muito mais testável do que injetar dependências via setters ou diretamente em propriedades públicas.</p>

<h3>Exemplo Real: Serviço de Pagamento com Múltiplas Dependências</h3>

<p>Aqui está um exemplo mais próximo da realidade, onde você tem várias dependências e precisa fazer testes mais sofisticados:</p>

<pre><code class="language-typescript">// payment.interfaces.ts

export interface IPaymentGateway {

charge(amount: number, token: string): Promise&lt;{ transactionId: string }&gt;;

}

export interface INotificationService {

sendConfirmation(email: string, transactionId: string): Promise&lt;void&gt;;

}

export interface IOrderRepository {

updateOrderStatus(orderId: string, status: string): Promise&lt;void&gt;;

}

// payment.service.ts

export class PaymentService {

constructor(

private gateway: IPaymentGateway,

private notifier: INotificationService,

private orderRepo: IOrderRepository

) {}

async processPayment(

orderId: string,

amount: number,

token: string,

email: string

): Promise&lt;void&gt; {

const result = await this.gateway.charge(amount, token);

await this.orderRepo.updateOrderStatus(

orderId,

&quot;payment_confirmed&quot;

);

await this.notifier.sendConfirmation(email, result.transactionId);

}

}

// payment.service.test.ts

import { mock } from &quot;jest-mock-extended&quot;;

import { PaymentService } from &quot;./payment.service&quot;;

import {

IPaymentGateway,

INotificationService,

IOrderRepository,

} from &quot;./payment.interfaces&quot;;

describe(&quot;PaymentService&quot;, () =&gt; {

it(&quot;deve processar pagamento, atualizar pedido e notificar cliente&quot;, async () =&gt; {

const gatewayMock = mock&lt;IPaymentGateway&gt;();

const notifierMock = mock&lt;INotificationService&gt;();

const repositoryMock = mock&lt;IOrderRepository&gt;();

// Configurar os retornos esperados

gatewayMock.charge.mockResolvedValue({

transactionId: &quot;txn-12345&quot;,

});

notifierMock.sendConfirmation.mockResolvedValue(undefined);

repositoryMock.updateOrderStatus.mockResolvedValue(undefined);

const service = new PaymentService(

gatewayMock,

notifierMock,

repositoryMock

);

// Executar

await service.processPayment(

&quot;order-456&quot;,

99.99,

&quot;tok_visa&quot;,

&quot;customer@example.com&quot;

);

// Verificar as chamadas e a ordem

expect(gatewayMock.charge).toHaveBeenCalledWith(99.99, &quot;tok_visa&quot;);

expect(repositoryMock.updateOrderStatus).toHaveBeenCalledWith(

&quot;order-456&quot;,

&quot;payment_confirmed&quot;

);

expect(notifierMock.sendConfirmation).toHaveBeenCalledWith(

&quot;customer@example.com&quot;,

&quot;txn-12345&quot;

);

});

it(&quot;deve lançar erro se o gateway falhar&quot;, async () =&gt; {

const gatewayMock = mock&lt;IPaymentGateway&gt;();

const notifierMock = mock&lt;INotificationService&gt;();

const repositoryMock = mock&lt;IOrderRepository&gt;();

// Simular uma falha no gateway

gatewayMock.charge.mockRejectedValue(

new Error(&quot;Cartão recusado&quot;)

);

const service = new PaymentService(

gatewayMock,

notifierMock,

repositoryMock

);

// Esperar por uma exceção

await expect(

service.processPayment(

&quot;order-456&quot;,

99.99,

&quot;tok_invalid&quot;,

&quot;customer@example.com&quot;

)

).rejects.toThrow(&quot;Cartão recusado&quot;);

// Verificar que repositório e notificador NÃO foram chamados

expect(repositoryMock.updateOrderStatus).not.toHaveBeenCalled();

expect(notifierMock.sendConfirmation).not.toHaveBeenCalled();

});

});</code></pre>

<h2>Técnicas Avançadas de Mock com jest-mock-extended</h2>

<p>Agora que você entende o básico, vamos explorar funcionalidades mais sofisticadas que tornam seus testes ainda mais robustos.</p>

<h3>Mockando Métodos Específicos com Comportamentos Diferentes</h3>

<p>Às vezes você quer que diferentes chamadas ao mesmo método retornem valores diferentes:</p>

<pre><code class="language-typescript">interface IUserRepository {

findById(id: string): Promise&lt;User | null&gt;;

}

describe(&quot;UserService&quot;, () =&gt; {

it(&quot;deve lidar com usuários existentes e inexistentes&quot;, async () =&gt; {

const repoMock = mock&lt;IUserRepository&gt;();

// Primeira chamada retorna um usuário, segunda retorna null

repoMock.findById

.mockResolvedValueOnce({ id: &quot;1&quot;, name: &quot;Alice&quot; })

.mockResolvedValueOnce(null);

const service = new UserService(repoMock);

const user1 = await service.findUser(&quot;1&quot;);

const user2 = await service.findUser(&quot;2&quot;);

expect(user1?.name).toBe(&quot;Alice&quot;);

expect(user2).toBeNull();

});

});</code></pre>

<h3>Usando <code>mockImplementation</code> para Lógica Customizada</h3>

<p>Quando o comportamento é complexo demais para um simples <code>mockResolvedValue</code>, use <code>mockImplementation</code>:</p>

<pre><code class="language-typescript">interface ICalculator {

calculate(expression: string): number;

}

it(&quot;deve validar expressões matemáticas&quot;, () =&gt; {

const calcMock = mock&lt;ICalculator&gt;();

calcMock.calculate.mockImplementation((expr: string) =&gt; {

if (expr === &quot;2+2&quot;) return 4;

if (expr === &quot;10-5&quot;) return 5;

throw new Error(&quot;Expressão inválida&quot;);

});

expect(calcMock.calculate(&quot;2+2&quot;)).toBe(4);

expect(() =&gt; calcMock.calculate(&quot;invalid&quot;)).toThrow();

});</code></pre>

<h3>Capturando Argumentos com <code>mockCalls</code></h3>

<p>Às vezes você precisa verificar não apenas se uma função foi chamada, mas com quais argumentos exatos:</p>

<pre><code class="language-typescript">interface ILogger {

log(level: string, message: string, data?: any): void;

}

it(&quot;deve registrar erros com dados contextuais&quot;, () =&gt; {

const loggerMock = mock&lt;ILogger&gt;();

const service = new Service(loggerMock);

service.doSomethingThatLogs();

// Verificar a última chamada

const lastCall = loggerMock.log.mock.calls[loggerMock.log.mock.calls.length - 1];

expect(lastCall[0]).toBe(&quot;error&quot;);

expect(lastCall[1]).toContain(&quot;falha&quot;);

expect(lastCall[2]).toEqual({ code: 500 });

});</code></pre>

<h3>Mockando Propriedades, Não Apenas Métodos</h3>

<p>Interfaces também podem ter propriedades. <code>jest-mock-extended</code> permite mockear isso também:</p>

<pre><code class="language-typescript">interface IConfig {

apiUrl: string;

timeout: number;

retries: number;

}

it(&quot;deve usar configurações do mock&quot;, () =&gt; {

const configMock = mock&lt;IConfig&gt;({

apiUrl: &quot;https://test-api.example.com&quot;,

timeout: 5000,

retries: 2,

});

const client = new ApiClient(configMock);

expect(client.getApiUrl()).toBe(&quot;https://test-api.example.com&quot;);

expect(client.getTimeout()).toBe(5000);

});</code></pre>

<h3>Mockando Métodos em Cadeia (Fluent API)</h3>

<p>Se seu código usa uma API fluente (método retorna <code>this</code>), você pode mockear isso:</p>

<pre><code class="language-typescript">interface IQueryBuilder {

select(fields: string[]): IQueryBuilder;

where(condition: string): IQueryBuilder;

orderBy(field: string): IQueryBuilder;

build(): string;

}

it(&quot;deve construir query com métodos encadeados&quot;, () =&gt; {

const queryMock = mock&lt;IQueryBuilder&gt;();

// Configurar encadeamento

queryMock.select.mockReturnValue(queryMock);

queryMock.where.mockReturnValue(queryMock);

queryMock.orderBy.mockReturnValue(queryMock);

queryMock.build.mockReturnValue(&quot;SELECT * FROM users WHERE id = 1 ORDER BY name&quot;);

const query = queryMock

.select([&quot;id&quot;, &quot;name&quot;])

.where(&quot;id = 1&quot;)

.orderBy(&quot;name&quot;)

.build();

expect(query).toContain(&quot;SELECT&quot;);

expect(queryMock.select).toHaveBeenCalled();

expect(queryMock.where).toHaveBeenCalled();

});</code></pre>

<h2>Integração com Padrões de Arquitetura</h2>

<p>Até agora vimos exemplos simples. Em aplicações reais, você usa injeção de dependência em escala, frequentemente com containers de IoC.</p>

<h3>Usando Mocks com Decorators (NestJS Example)</h3>

<p>Se você usa NestJS, pode integrar mocks perfeitamente com os providers:</p>

<pre><code class="language-typescript">// user.service.ts

import { Injectable } from &quot;@nestjs/common&quot;;

export interface IUserRepository {

findById(id: string): Promise&lt;any&gt;;

}

@Injectable()

export class UserService {

constructor(private userRepo: IUserRepository) {}

async getUser(id: string) {

return this.userRepo.findById(id);

}

}

// user.service.spec.ts

import { Test, TestingModule } from &quot;@nestjs/testing&quot;;

import { mock } from &quot;jest-mock-extended&quot;;

import { UserService } from &quot;./user.service&quot;;

import { IUserRepository } from &quot;./user.repository.interface&quot;;

describe(&quot;UserService&quot;, () =&gt; {

let service: UserService;

let userRepoMock: jest.Mocked&lt;IUserRepository&gt;;

beforeEach(async () =&gt; {

userRepoMock = mock&lt;IUserRepository&gt;();

const module: TestingModule = await Test.createTestingModule({

providers: [

UserService,

{

provide: &quot;IUserRepository&quot;,

useValue: userRepoMock,

},

],

}).compile();

service = module.get&lt;UserService&gt;(UserService);

});

it(&quot;deve buscar usuário pelo ID&quot;, async () =&gt; {

userRepoMock.findById.mockResolvedValue({ id: &quot;1&quot;, name: &quot;Bob&quot; });

const result = await service.getUser(&quot;1&quot;);

expect(result.name).toBe(&quot;Bob&quot;);

expect(userRepoMock.findById).toHaveBeenCalledWith(&quot;1&quot;);

});

});</code></pre>

<h3>Testando Comportamento com Estados Múltiplos</h3>

<p>Em sistemas mais complexos, uma dependência pode passar por múltiplos estados. Você pode simular isso:</p>

<pre><code class="language-typescript">interface IAuthenticator {

authenticate(credentials: any): Promise&lt;boolean&gt;;

getUser(): { id: string; role: string } | null;

logout(): void;

}

describe(&quot;ProtectedResource&quot;, () =&gt; {

it(&quot;deve negar acesso antes da autenticação e permitir depois&quot;, async () =&gt; {

const authMock = mock&lt;IAuthenticator&gt;();

// Simular estado não autenticado

authMock.getUser.mockReturnValue(null);

let resource = new ProtectedResource(authMock);

expect(() =&gt; resource.getData()).toThrow(&quot;Não autenticado&quot;);

// Simular autenticação bem-sucedida

authMock.authenticate.mockResolvedValue(true);

authMock.getUser.mockReturnValue({ id: &quot;123&quot;, role: &quot;admin&quot; });

await authMock.authenticate({});

expect(resource.getData()).toEqual({ sensitive: &quot;data&quot; });

});

});</code></pre>

<h2>Conclusão</h2>

<p>Você aprendeu três pilares fundamentais para dominar testes em TypeScript:</p>

<ol>

<li><strong>jest-mock-extended fornece segurança de tipos</strong>: Diferente de <code>jest.fn()</code> básico, <code>mock&lt;T&gt;()</code> integra-se perfeitamente com TypeScript, oferecendo autocompletar e detecção de erros em tempo de compilação. Isso não é apenas conforto — é precisão.</li>

</ol>

<ol>

<li><strong>Tipar dependências com interfaces é o padrão</strong>: Depender de abstrações (interfaces) em vez de implementações concretas torna seu código testável por design. Constructor injection é o padrão ouro. Quando suas dependências são bem tipadas, criar mocks delas torna-se uma tarefa mecânica e segura.</li>

</ol>

<ol>

<li><strong>Mocks sofisticados simulam comportamentos realistas</strong>: Você vai além de simples <code>mockResolvedValue</code>. Use <code>mockImplementation</code> para lógica complexa, <code>mockRejectedValue</code> para simular erros, e combine múltiplos mocks para testar fluxos inteiros. Seus testes passam a ser documentação viva do comportamento esperado.</li>

</ol>

<h2>Referências</h2>

<ul>

<li><a href="https://github.com/marchaos/jest-mock-extended" target="_blank" rel="noopener noreferrer">jest-mock-extended - Documentação Oficial</a></li>

<li><a href="https://jestjs.io/docs/mock-functions" target="_blank" rel="noopener noreferrer">Jest - Testing Library Documentation</a></li>

<li><a href="https://www.typescriptlang.org/docs/handbook/2/objects.html" target="_blank" rel="noopener noreferrer">TypeScript Handbook - Interfaces</a></li>

<li><a href="https://docs.nestjs.com/fundamentals/testing" target="_blank" rel="noopener noreferrer">NestJS Testing Documentation</a></li>

<li><a href="https://martinfowler.com/articles/mocksArentStubs.html" target="_blank" rel="noopener noreferrer">Martin Fowler - Mocks Aren&#039;t Stubs</a></li>

</ul>

<p>&lt;!-- FIM --&gt;</p>

Comentários

Mais em TypeScript

TypeScript Compiler API: Parsear, Transformar e Gerar Código na Prática
TypeScript Compiler API: Parsear, Transformar e Gerar Código na Prática

Introdução à TypeScript Compiler API A TypeScript Compiler API é um conjunto...

O que Todo Dev Deve Saber sobre Herança e Polimorfismo em TypeScript com Classes e Interfaces
O que Todo Dev Deve Saber sobre Herança e Polimorfismo em TypeScript com Classes e Interfaces

Entendendo Herança em TypeScript A herança é um dos pilares da Programação Or...

Dominando Segurança em TypeScript: Tipos que Previnem Vulnerabilidades Comuns em Projetos Reais
Dominando Segurança em TypeScript: Tipos que Previnem Vulnerabilidades Comuns em Projetos Reais

O Poder do Sistema de Tipos do TypeScript na Prevenção de Vulnerabilidades O...