<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<T>()</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<string>;
logout(): Promise<void>;
isAuthenticated(): boolean;
}
export class AuthService implements IAuthService {
async login(email: string, password: string): Promise<string> {
// implementação real
return "token123";
}
async logout(): Promise<void> {
// 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<void> {
const token = await this.authService.login(email, password);
if (token) {
console.log("Login bem-sucedido");
}
}
}</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 "jest-mock-extended";
import { UserComponent } from "./user.component";
import { IAuthService } from "./auth.service";
describe("UserComponent", () => {
it("deve realizar login com sucesso", async () => {
// Criar um mock totalmente tipado
const authServiceMock = mock<IAuthService>();
// Configurar o comportamento esperado
authServiceMock.login.mockResolvedValue("token-valido");
// Injetar o mock no componente
const component = new UserComponent(authServiceMock);
// Executar a lógica
await component.handleLogin("user@example.com", "senha123");
// Verificar se a função foi chamada com os argumentos corretos
expect(authServiceMock.login).toHaveBeenCalledWith(
"user@example.com",
"senha123"
);
});
});</code></pre>
<p>O <code>mock<IAuthService>()</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 "quê" 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<{ transactionId: string }>;
}
export interface INotificationService {
sendConfirmation(email: string, transactionId: string): Promise<void>;
}
export interface IOrderRepository {
updateOrderStatus(orderId: string, status: string): Promise<void>;
}
// 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<void> {
const result = await this.gateway.charge(amount, token);
await this.orderRepo.updateOrderStatus(
orderId,
"payment_confirmed"
);
await this.notifier.sendConfirmation(email, result.transactionId);
}
}
// payment.service.test.ts
import { mock } from "jest-mock-extended";
import { PaymentService } from "./payment.service";
import {
IPaymentGateway,
INotificationService,
IOrderRepository,
} from "./payment.interfaces";
describe("PaymentService", () => {
it("deve processar pagamento, atualizar pedido e notificar cliente", async () => {
const gatewayMock = mock<IPaymentGateway>();
const notifierMock = mock<INotificationService>();
const repositoryMock = mock<IOrderRepository>();
// Configurar os retornos esperados
gatewayMock.charge.mockResolvedValue({
transactionId: "txn-12345",
});
notifierMock.sendConfirmation.mockResolvedValue(undefined);
repositoryMock.updateOrderStatus.mockResolvedValue(undefined);
const service = new PaymentService(
gatewayMock,
notifierMock,
repositoryMock
);
// Executar
await service.processPayment(
"order-456",
99.99,
"tok_visa",
"customer@example.com"
);
// Verificar as chamadas e a ordem
expect(gatewayMock.charge).toHaveBeenCalledWith(99.99, "tok_visa");
expect(repositoryMock.updateOrderStatus).toHaveBeenCalledWith(
"order-456",
"payment_confirmed"
);
expect(notifierMock.sendConfirmation).toHaveBeenCalledWith(
"customer@example.com",
"txn-12345"
);
});
it("deve lançar erro se o gateway falhar", async () => {
const gatewayMock = mock<IPaymentGateway>();
const notifierMock = mock<INotificationService>();
const repositoryMock = mock<IOrderRepository>();
// Simular uma falha no gateway
gatewayMock.charge.mockRejectedValue(
new Error("Cartão recusado")
);
const service = new PaymentService(
gatewayMock,
notifierMock,
repositoryMock
);
// Esperar por uma exceção
await expect(
service.processPayment(
"order-456",
99.99,
"tok_invalid",
"customer@example.com"
)
).rejects.toThrow("Cartão recusado");
// 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<User | null>;
}
describe("UserService", () => {
it("deve lidar com usuários existentes e inexistentes", async () => {
const repoMock = mock<IUserRepository>();
// Primeira chamada retorna um usuário, segunda retorna null
repoMock.findById
.mockResolvedValueOnce({ id: "1", name: "Alice" })
.mockResolvedValueOnce(null);
const service = new UserService(repoMock);
const user1 = await service.findUser("1");
const user2 = await service.findUser("2");
expect(user1?.name).toBe("Alice");
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("deve validar expressões matemáticas", () => {
const calcMock = mock<ICalculator>();
calcMock.calculate.mockImplementation((expr: string) => {
if (expr === "2+2") return 4;
if (expr === "10-5") return 5;
throw new Error("Expressão inválida");
});
expect(calcMock.calculate("2+2")).toBe(4);
expect(() => calcMock.calculate("invalid")).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("deve registrar erros com dados contextuais", () => {
const loggerMock = mock<ILogger>();
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("error");
expect(lastCall[1]).toContain("falha");
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("deve usar configurações do mock", () => {
const configMock = mock<IConfig>({
apiUrl: "https://test-api.example.com",
timeout: 5000,
retries: 2,
});
const client = new ApiClient(configMock);
expect(client.getApiUrl()).toBe("https://test-api.example.com");
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("deve construir query com métodos encadeados", () => {
const queryMock = mock<IQueryBuilder>();
// Configurar encadeamento
queryMock.select.mockReturnValue(queryMock);
queryMock.where.mockReturnValue(queryMock);
queryMock.orderBy.mockReturnValue(queryMock);
queryMock.build.mockReturnValue("SELECT * FROM users WHERE id = 1 ORDER BY name");
const query = queryMock
.select(["id", "name"])
.where("id = 1")
.orderBy("name")
.build();
expect(query).toContain("SELECT");
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 "@nestjs/common";
export interface IUserRepository {
findById(id: string): Promise<any>;
}
@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 "@nestjs/testing";
import { mock } from "jest-mock-extended";
import { UserService } from "./user.service";
import { IUserRepository } from "./user.repository.interface";
describe("UserService", () => {
let service: UserService;
let userRepoMock: jest.Mocked<IUserRepository>;
beforeEach(async () => {
userRepoMock = mock<IUserRepository>();
const module: TestingModule = await Test.createTestingModule({
providers: [
UserService,
{
provide: "IUserRepository",
useValue: userRepoMock,
},
],
}).compile();
service = module.get<UserService>(UserService);
});
it("deve buscar usuário pelo ID", async () => {
userRepoMock.findById.mockResolvedValue({ id: "1", name: "Bob" });
const result = await service.getUser("1");
expect(result.name).toBe("Bob");
expect(userRepoMock.findById).toHaveBeenCalledWith("1");
});
});</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<boolean>;
getUser(): { id: string; role: string } | null;
logout(): void;
}
describe("ProtectedResource", () => {
it("deve negar acesso antes da autenticação e permitir depois", async () => {
const authMock = mock<IAuthenticator>();
// Simular estado não autenticado
authMock.getUser.mockReturnValue(null);
let resource = new ProtectedResource(authMock);
expect(() => resource.getData()).toThrow("Não autenticado");
// Simular autenticação bem-sucedida
authMock.authenticate.mockResolvedValue(true);
authMock.getUser.mockReturnValue({ id: "123", role: "admin" });
await authMock.authenticate({});
expect(resource.getData()).toEqual({ sensitive: "data" });
});
});</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<T>()</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't Stubs</a></li>
</ul>
<p><!-- FIM --></p>