Como Escrever Testes Unitários para Serviços Backend com Dependências de Banco de Dados Usando SQLite In-Memory
Tags: jest, sqlite, testing, database
Published: 2024-06-11 21:09:54

## Introdução

Ao desenvolver serviços backend, os testes unitários são cruciais para garantir a correção e a estabilidade do seu código. No entanto, escrever testes unitários para componentes que interagem com um banco de dados pode ser desafiador. Usar um banco de dados real para testes pode ser lento e complicado, além de introduzir efeitos colaterais que dificultam a reprodução dos testes. Uma solução eficaz é usar um banco de dados SQLite in-memory, que é rápido e fácil de configurar, permitindo que os testes sejam isolados e repetíveis.

Neste artigo, vamos explorar como configurar e escrever testes unitários para um serviço backend que interage com um banco de dados, usando TypeORM e SQLite in-memory.

## Instalação das Dependências

Primeiro, precisamos instalar as dependências necessárias:

```bash
npm install --save-dev typescript ts-jest ts-node @types/jest @types/node jest sqlite3 typeorm reflect-metadata
```

## Configuração do Ambiente de Testes

### Arquivo de Configuração do TypeORM para Testes

Crie um arquivo de configuração do TypeORM específico para testes. Este arquivo configura o TypeORM para usar um banco de dados SQLite em memória.

#### `jest.setup.ts`

```typescript
import 'reflect-metadata';
import {
  createConnection,
  getConnection,
} from 'typeorm';
import { User } from './src/entity/User';

beforeAll(() => {
  return createConnection({
    type: 'sqlite',
    database: ':memory:',
    dropSchema: true,
    entities: [User],
    synchronize: true,
    logging: false,
  });
});

afterAll(async () => {
  const connection = getConnection();
  await connection.close();
});

afterEach(async () => {
  const connection = getConnection();
  await connection.synchronize(true);
});
```

### Estrutura do Projeto

Suponha que você tenha a seguinte estrutura de projeto:

```
src/
  entity/
    User.ts
  repository/
    UserRepository.ts
  service/
    UserService.ts
  __tests__/
    UserService.test.ts
  jest.setup.ts
```

### Implementação dos Módulos

#### `src/entity/User.ts`

```typescript
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';

@Entity()
export class User {
  @PrimaryGeneratedColumn()
  id!: number;

  @Column()
  name!: string;

  @Column()
  email!: string;
}
```

#### `src/repository/UserRepository.ts`

```typescript
import { EntityRepository, Repository } from 'typeorm';
import { User } from '../entity/User';

@EntityRepository(User)
export class UserRepository extends Repository<User> {
  findByName(name: string): Promise<User | undefined> {
    return this.findOne({ name });
  }
}
```

#### `src/service/UserService.ts`

```typescript
import { getCustomRepository } from 'typeorm';
import { UserRepository } from '../repository/UserRepository';
import { User } from '../entity/User';

export class UserService {
  private userRepository = getCustomRepository(UserRepository);

  async findUserByName(name: string): Promise<User | undefined> {
    return this.userRepository.findByName(name);
  }

  async createUser(name: string, email: string): Promise<User> {
    const user = new User();
    user.name = name;
    user.email = email;
    return this.userRepository.save(user);
  }
}
```

### Configuração de Testes com Jest

Crie um arquivo de configuração do Jest para garantir que o ambiente de testes está configurado corretamente.

#### `jest.config.js`

```javascript
module.exports = {
  preset: 'ts-jest',
  testEnvironment: 'node',
  setupFilesAfterEnv: ['./jest.setup.ts'],
  moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
  testPathIgnorePatterns: ['/node_modules/', '/dist/'],
  transform: {
    '^.+\\.(ts|tsx)$': 'ts-jest',
  },
  globals: {
    'ts-jest': {
      tsconfig: 'tsconfig.json',
    },
  },
};
```

### Configuração do TypeScript

Certifique-se de que a configuração do TypeScript (`tsconfig.json`) permite o uso de decoradores e metadados de decoradores.

#### `tsconfig.json`

```json
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "CommonJS",
    "lib": ["ES2020"],
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "moduleResolution": "node",
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true
  },
  "include": ["src/**/*.ts"],
  "exclude": ["node_modules", "**/*.test.ts", "dist"]
}
```

### Escrita dos Testes

#### `src/__tests__/UserService.test.ts`

```typescript
import { UserService } from '../service/UserService';

describe('UserService', () => {
  let userService: UserService;

  beforeAll(() => {
    userService = new UserService();
  });

  test('should create a new user', async () => {
    const user = await userService.createUser('John Doe', 'john@example.com');
    expect(user).toHaveProperty('id');
    expect(user.name).toBe('John Doe');
    expect(user.email).toBe('john@example.com');
  });

  test('should find a user by name', async () => {
    const user = await userService.createUser('Jane Doe', 'jane@example.com');
    const foundUser = await userService.findUserByName('Jane Doe');
    expect(foundUser).toBeDefined();
    expect(foundUser?.name).toBe('Jane Doe');
    expect(foundUser?.email).toBe('jane@example.com');
  });

  test('should return undefined if user is not found', async () => {
    const foundUser = await userService.findUserByName('Non Existent');
    expect(foundUser).toBeUndefined();
  });
});
```

### Explicação

1. **Configuração do Banco de Dados de Teste**:
   - Configuramos o TypeORM para usar um banco de dados SQLite em memória para testes.
   - `jest.setup.ts` é usado para criar a conexão com o banco de dados antes de todos os testes e fechá-la após todos os testes.

2. **Sincronização do Banco de Dados**:
   - Após cada teste, sincronizamos o banco de dados para limpar os dados inseridos durante o teste (`await getConnection().synchronize(true);`).

3. **Escrita dos Testes**:
   - Criamos testes para verificar se o usuário é criado corretamente, se o usuário é encontrado pelo nome e se retorna `undefined` quando o usuário não é encontrado.

### Conclusão

Usar um banco de dados SQLite in-memory para testes é uma ótima maneira de testar funcionalidades que dependem de um banco de dados real sem a complexidade de configurar um banco de dados de teste separado. Isso garante que os testes sejam rápidos, isolados e confiáveis. Com esta abordagem, você pode garantir que seu código interaja corretamente com o banco de dados e que todas as funcionalidades críticas sejam verificadas.

### Repositório

https://github.com/vitorrios1001/tests-with-db