DEV Community

Gabriel de Luca
Gabriel de Luca

Posted on

AluraChallenges #2 ( Semana 2)

No post anterior, nós fizemos todas as implementações que tínhamos da semana 1, deixando nossa API de vídeos pronta para consumo.

Só que ainda temos desafios pela frente e nessa semana faremos a inserção de categorias, para classificarmos os nossos vídeos.
A história dessa semana é a seguinte:

Depois de alguns testes com usuários, foi definido que a próxima feature a ser desenvolvida nesse projeto é a divisão dos vídeos por categoria, para melhorar a experiência de organização da lista de vídeos pelo usuário.

Então, faremos nesse post as seguintes implementações :

  • Adicionar categorias e seus campos na base de dados;
  • Rotas CRUD para /categorias;
  • Incluir campo categoriaId no modelo video;
  • Escrever os testes unitários.

Vamos começar fazendo os dois primeiros pontos, e novamente usaremos o generate do Nest, para criamos o recurso Categorias.

nest generate resource categorias
Enter fullscreen mode Exit fullscreen mode

Após a criação, faremos a definição da nossa classe CreateCategoriaDto:

// src/categorias/dto/create-categoria.dto.ts

import { Video } from '../../videos/entities/video.entity';

export class CreateCategoriaDto {
  id: number;
  titulo: string;
  cor: string;
  videos: Video[];
}

Enter fullscreen mode Exit fullscreen mode

e alteraremos o nosso CreateVideoDto, adicionando a categoria:

// src/videos/dto/create-video.dto.ts

import { Categoria } from '../../categorias/entities/categoria.entity';

export class CreateVideoDto {
...

categoria: Categoria;
}
Enter fullscreen mode Exit fullscreen mode

Agora vamos na nossa "entity" e precisaremos fazer um pouco diferente.
Como será necessário ligar as tabelas de videos e categorias, utilizaremos o decorator @OneToMany(), que significa que teremos 1 categoria para vários vídeos:

// src/categorias/entities/categoria.entity.ts

import { IsNotEmpty, IsString } from 'class-validator';
import { Video } from '../../videos/entities/video.entity';
import { Column, Entity, OneToMany, PrimaryGeneratedColumn } from 'typeorm';

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

  @IsNotEmpty()
  @IsString()
  @Column()
  titulo: string;

  @IsNotEmpty()
  @IsString()
  @Column()
  cor: string;

  @OneToMany(() => Video, (video) => video.categoria)
  videos: Video[];
}
Enter fullscreen mode Exit fullscreen mode

Será necessário alterar nossa "entity" Video também, informando que teremos varios vídeos para cada categoria. Utilizaremos o decorator @ManyToOne() após todos os atributos que já temos:

// src/videos/entities/video.entity.ts

import { PrimaryGeneratedColumn, Column, Entity, ManyToOne } from 'typeorm';
import { IsNotEmpty, IsString, IsUrl } from 'class-validator';
import { Categoria } from '../../categorias/entities/categoria.entity';

@Entity()
export class Video {
  ...
  @ManyToOne(() => Categoria, (categoria) => categoria.id, { nullable: false })
  categoria: Categoria;
}
Enter fullscreen mode Exit fullscreen mode

Entidades ajustadas, vamos para o nosso "categoria.service" para implementar as funcionalidades dela.
A diferença nela está no findAll, que passamos o parâmetro indicando a relação (relations: ['videos']) e já pedindo para trazer os dados dessa relação (loadEagerRelations: true):

// src/categorias/categorias.service.ts

import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { CreateCategoriaDto } from './dto/create-categoria.dto';
import { UpdateCategoriaDto } from './dto/update-categoria.dto';
import { Categoria } from './entities/categoria.entity';

@Injectable()
export class CategoriasService {
  @InjectRepository(Categoria)
  private categoriaRepository: Repository<Categoria>;

  create(createCategoriaDto: CreateCategoriaDto) {
    return this.categoriaRepository.save(createCategoriaDto);
  }

  findAll() {
    return this.categoriaRepository.find({
      relations: ['videos'],
      loadEagerRelations: true,
    });
  }

  findOne(id: number) {
    return this.categoriaRepository.findOne(id);
  }

  update(id: number, updateCategoriaDto: UpdateCategoriaDto) {
    return this.categoriaRepository.update(id, updateCategoriaDto);
  }

  async remove(id: number) {
    const categoria = await this.findOne(id);
    return this.categoriaRepository.remove(categoria);
  }
}
Enter fullscreen mode Exit fullscreen mode

Novamente, temos que ajustar nosso "video.service" também, que no caso, só terá alteração no método "findAll()":

// src/videos/videos.service.ts

...
findAll() {
    return this.videoRepository.find({
      relations: ['categoria'],
      loadEagerRelations: true,
    });
  }
...
Enter fullscreen mode Exit fullscreen mode

Agora, para que a tabela de categorias seja criada no banco e identificada pelo Nest, precisamos ir no categorias.module e importar o TypeOrmModule passando a Categoria como parâmetro:

// src/categorias/categorias.module.ts

import { Module } from '@nestjs/common';
import { CategoriasService } from './categorias.service';
import { CategoriasController } from './categorias.controller';
import { Categoria } from './entities/categoria.entity';
import { TypeOrmModule } from '@nestjs/typeorm';

@Module({
  imports: [TypeOrmModule.forFeature([Categoria])],
  controllers: [CategoriasController],
  providers: [CategoriasService],
})
export class CategoriasModule {}

Enter fullscreen mode Exit fullscreen mode

Maravilha, implementamos a categoria para os nossos videos, porém, temos um card que pede para que ao acessar a rota "/categorias/:id/videos" retorne os vídeos de uma determinada categoria:
videos_por_categoria

Para que isso seja possível, vamos precisar de uma nova rota no nosso controller de categorias.
A nova rota vai ficar assim:

// src/categorias/categorias.controller.ts
...

@Get(':id/videos')
  findVideosByCategoryId(@Param('id') id: string) {
    return this.categoriasService.findVideoByCategory(+id);
  }

...
Enter fullscreen mode Exit fullscreen mode

Mas espera aí, nós não temos esse método "categoriasService.findVideoByCategory()".
Precisamos criar esse método lá no nosso serviço "categorias.service":

// src/categorias/categorias.service.ts

...

async findVideoByCategory(id: number): Promise<Video[]> {
    const categoria = await this.findOne(id);
    return categoria.videos;
  }

...
Enter fullscreen mode Exit fullscreen mode

Mas se tentarmos acessar a rota, veremos uma tela branca, sem retorno de nenhum dado. --'
Para que seja retornado, precisamos alterar também o nosso método findOne, passando um objeto de configuração, informando que queremos que ele faça o carregamento dos dados relacionados à essa tabela.
O método ficará assim:

// src/categorias/categorias.service.ts
...

findOne(id: number) {
    return this.categoriaRepository.findOne(id, {
      relations: ['videos'],
      loadEagerRelations: true,
    });
  }

...
Enter fullscreen mode Exit fullscreen mode

Agora sim, ao acessar nossa rota ".../categorias/1/videos", teremos nossos vídeos referentes à essa categoria.

O próximo card, tem a seguinte descrição:
videos_query_params

Para atender a esse requisito, precisaremos alterar nosso controller e nosso service de videos:

// src/videos/videos.controller.ts

...

@Get()
  findAll(@Query() query) {
    return this.videosService.findAll(query.search);
  }

...
Enter fullscreen mode Exit fullscreen mode
// src/videos/videos.service.ts

...

findAll(search = '') {
    return this.videoRepository.find({
      where: { titulo: ILike(`%${search}%`) },
      relations: ['categoria'],
    });
  }

...
Enter fullscreen mode Exit fullscreen mode

Próximo card:
categ_livre

Como já criamos uma categoria anteriormente, vamos enviar uma requisição de update para a categoria de ID 1, alterando o título para LIVRE
insomnia_patch
E após isso, vamos no videos.service e alteraremos a lógica do método create:

// src/videos/videos.service.ts
...

create(createVideoDto: CreateVideoDto) {
    if (!createVideoDto.categoria)
      return this.videoRepository.save({
        ...createVideoDto,
        categoria: { id: 1 },
      });
    return this.videoRepository.save(createVideoDto);
  }

...
Enter fullscreen mode Exit fullscreen mode

Pronto, requisito atendido!

(Faça uma requisição criando um vídeo sem informar a categoria e veja se está tudo funcionando como o esperado, o retorno deve ser o vídeo criado e com a "categoria":{"id": 1})

O próximo requisito é para que se crie os testes automatizados e aí que o negócio começa a ficar legal.

Implementando testes automatizados

Para facilitar nossos mocks, vamos inserir um construtor para as nossas entidades, deixando elas assim:

// src/categorias/entities/categoria.entity.ts

...

constructor(private categoria?: Partial<Categoria>) {}
Enter fullscreen mode Exit fullscreen mode
// src/categorias/entities/categoria.entity.ts

...

constructor(private video?: Partial<Categoria>) {}
Enter fullscreen mode Exit fullscreen mode

Vamos criar também uma pasta common dentro de src e nela criar uma pasta test, que ficarão os arquivos necessário e comum à todos os testes. Por agora, teremos dois stubs:
videos.stub.ts (crie esse arquivo)

// src/common/test/videos.stub.ts

import { Categoria } from '../../categorias/entities/categoria.entity';
import { Video } from '../../videos/entities/video.entity';
import { categoriasStub } from './categorias.stub';

export const videosStub: Video[] = [
  new Video({
    id: 1,
    titulo: 'título qualquer',
    descricao: 'descrição qualquer',
    url: 'http://url_qualquer.com',
    categoria: new Categoria({ id: 1, titulo: 'LIVRE', cor: 'verde' }),
  }),
  new Video({
    id: 2,
    titulo: 'outro título qualquer',
    descricao: 'outra descrição qualquer',
    url: 'http://outra_url_qualquer.com',
    categoria: categoriasStub[1],
  }),
  new Video({
    id: 3,
    titulo: 'titulo qualquer',
    descricao: 'descrição qualquer',
    url: 'http://url_qualquer.com',
    categoria: categoriasStub[1],
  }),
];

Enter fullscreen mode Exit fullscreen mode

e categorias.stub.ts (crie também)

// src/common/test/categorias.stub.ts

import { Categoria } from '../../categorias/entities/categoria.entity';

export const categoriasStub: Categoria[] = [
  new Categoria({ id: 1, titulo: 'LIVRE', cor: 'verde' }),
  new Categoria({ id: 2, titulo: 'Programação', cor: 'azul' }),
];


Enter fullscreen mode Exit fullscreen mode

Testando os controllers

Vamos rodas os testes e ver no que vai dar.

npm run test
Enter fullscreen mode Exit fullscreen mode

Que delícia, nenhum teste passou!
Isso acontece pois alteramos e implementamos tudo sem criar nenhum teste o que não é muito legal, mas enfim, vamos começar a ajustar as coisa...

Testando nosso categorias.controller

O Nestjs já deixa uma estrutura pré-montada, então, vamos até o nosso arquivo categorias.controller.spec.ts para trabalhar nele.
Nosso controller tem como dependência o service e para que ele seja disponibilizado no teste precisamos prover ele.
Faremos dessa forma:

// src/categorias/categorias.controller.spec.ts

import { Test, TestingModule } from '@nestjs/testing';
import { categoriasStub } from '../common/test/categorias.stub';
import { CategoriasController } from './categorias.controller';
import { CategoriasService } from './categorias.service';
import { videosStub } from '../common/test/videos.stub';

describe('CategoriasController', () => {
  let controller: CategoriasController;
  let service: CategoriasService;

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      controllers: [CategoriasController],
      providers: [
        CategoriasService,
        {
          provide: CategoriasService,
          useValue: {
            create: jest.fn().mockResolvedValue(categoriasStub[0]),
            findAll: jest.fn().mockResolvedValue(categoriasStub),
            findOne: jest.fn().mockResolvedValue(categoriasStub[0]),
            findVideoByCategory: jest.fn().mockResolvedValue(videosStub),
            update: jest.fn().mockResolvedValue(categoriasStub[0]),
            remove: jest.fn().mockResolvedValue(categoriasStub[0]),
          },
        },
      ],
    }).compile();

    controller = module.get<CategoriasController>(CategoriasController);
    service = module.get<CategoriasService>(CategoriasService);
  });

  it('should be defined', () => {
    expect(controller).toBeDefined();
    expect(service).toBeDefined();
  }); 
});
Enter fullscreen mode Exit fullscreen mode

repare que nós criamos um mock para cada função que temos no serviço e definimos o retorno delas com nosso stub da categoria.

com isso, podemos rodar novamente o comando de testes do npm, só que agora, deixaremos ele em modo watch para que ele fique observando as alterações, enquanto implementamos os testes, mas faremos somente para o arquivo que estamos trabalhando no momento:

npm run test:watch -t /home/gabriel/Documentos/alura-challenges-2/src/categorias/categorias.controller.spec.ts

# substitua essa parte '/home/gabriel/Documentos' pelo caminho do seu computador, é claro.
Enter fullscreen mode Exit fullscreen mode

Feito isso, o teste vai rodar e faremos a implementação do restante.
No final, esse arquivo ficará assim:

// src/categorias/categorias.controller.spec.ts

import { Test, TestingModule } from '@nestjs/testing';
import { categoriasStub } from '../common/test/categorias.stub';
import { CategoriasController } from './categorias.controller';
import { CategoriasService } from './categorias.service';
import { videosStub } from '../common/test/videos.stub';

describe('CategoriasController', () => {
  let controller: CategoriasController;
  let service: CategoriasService;

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      controllers: [CategoriasController],
      providers: [
        CategoriasService,
        {
          provide: CategoriasService,
          useValue: {
            create: jest.fn().mockResolvedValue(categoriasStub[0]),
            findAll: jest.fn().mockResolvedValue(categoriasStub),
            findOne: jest.fn().mockResolvedValue(categoriasStub[0]),
            findVideoByCategory: jest.fn().mockResolvedValue(videosStub),
            update: jest.fn().mockResolvedValue(categoriasStub[0]),
            remove: jest.fn().mockResolvedValue(categoriasStub[0]),
          },
        },
      ],
    }).compile();

    controller = module.get<CategoriasController>(CategoriasController);
    service = module.get<CategoriasService>(CategoriasService);
  });

  it('should be defined', () => {
    expect(controller).toBeDefined();
    expect(service).toBeDefined();
  }); 

  describe('create', () => {
    it('should create a category', async () => {
      const result = await service.create(categoriasStub[0]);
      expect(result).toEqual(categoriasStub[0]);
      expect(service.create).toHaveBeenCalledTimes(1);
    });

    it('should throw an exception', () => {
      jest.spyOn(service, 'create').mockRejectedValueOnce(new Error());
      expect(service.create(categoriasStub[0])).rejects.toThrowError();
      expect(service.create).toHaveBeenCalledTimes(1);
    });
  });

  describe('findAll', () => {
    it('should return a category list', async () => {
      const result = await service.findAll();
      expect(result).toEqual(categoriasStub);
      expect(service.findAll).toHaveBeenCalledTimes(1);
    });

    it('should throw an exception', () => {
      jest.spyOn(service, 'findAll').mockRejectedValueOnce(new Error());
      expect(service.findAll()).rejects.toThrowError();
      expect(service.findAll).toHaveBeenCalledTimes(1);
    });
  });

  describe('findOne', () => {
    it('should return a category', async () => {
      const result = await service.findOne(categoriasStub[0].id);
      expect(result).toEqual(categoriasStub[0]);
      expect(service.findOne).toHaveBeenCalledTimes(1);
    });

    it('should throw an exception', () => {
      jest.spyOn(service, 'findOne').mockRejectedValueOnce(new Error());
      expect(service.findOne(1)).rejects.toThrowError();
      expect(service.findOne).toHaveBeenCalledTimes(1);
    });
  });

  describe('findVideosByCategoryId', () => {
    it('should return videos from a category', async () => {
      const result = await service.findVideoByCategory(categoriasStub[0].id);
      expect(result).toEqual(videosStub);
      expect(service.findVideoByCategory).toHaveBeenCalledTimes(1);
    });

    it('should throw an exception', () => {
      jest
        .spyOn(service, 'findVideoByCategory')
        .mockRejectedValueOnce(new Error());
      expect(service.findVideoByCategory(1)).rejects.toThrowError();
      expect(service.findVideoByCategory).toHaveBeenCalledTimes(1);
    });
  });

  describe('update', () => {
    it('should return a updated category', async () => {
      const result = await service.update(
        categoriasStub[0].id,
        categoriasStub[0],
      );
      expect(result).toEqual(categoriasStub[0]);
      expect(service.update).toHaveBeenCalledTimes(1);
    });

    it('should throw an exception', () => {
      jest.spyOn(service, 'update').mockRejectedValueOnce(new Error());
      expect(service.update(1, categoriasStub[0])).rejects.toThrowError();
      expect(service.update).toHaveBeenCalledTimes(1);
    });
  });

  describe('remove', () => {
    it('should return a removed category', async () => {
      const result = await service.remove(categoriasStub[0].id);
      expect(result).toEqual(categoriasStub[0]);
      expect(service.remove).toHaveBeenCalledTimes(1);
    });

    it('should throw an exception', () => {
      jest.spyOn(service, 'remove').mockRejectedValueOnce(new Error());
      expect(service.remove(1)).rejects.toThrowError();
      expect(service.remove).toHaveBeenCalledTimes(1);
    });
  });
});

Enter fullscreen mode Exit fullscreen mode

Passou todos os testes? vamos para o próximo!

Testando nosso videos.controller

npm run test:watch -t /home/gabriel/Documentos/alura-challenges-2/src/videos/videos.controller.spec.ts

# substitua essa parte '/home/gabriel/Documentos' pelo caminho do seu computador, é claro.
Enter fullscreen mode Exit fullscreen mode

A ideia é a mesma para cá, então, esse arquivo fica assim:

// src/videos/videos.controller.spec.ts

import { Test, TestingModule } from '@nestjs/testing';
import { VideosController } from './videos.controller';
import { VideosService } from './videos.service';
import { videosStub } from '../common/test/videos.stub';

describe('VideosController', () => {
  let controller: VideosController;
  let service: VideosService;

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      controllers: [VideosController],
      providers: [
        VideosService,
        {
          provide: VideosService,
          useValue: {
            create: jest.fn().mockResolvedValue(videosStub[0]),
            findAll: jest.fn().mockResolvedValue(videosStub),
            findOne: jest.fn().mockResolvedValue(videosStub[0]),
            update: jest.fn().mockResolvedValue(videosStub[0]),
            remove: jest.fn().mockResolvedValue(videosStub[0]),
          },
        },
      ],
    }).compile();

    controller = module.get<VideosController>(VideosController);
    service = module.get<VideosService>(VideosService);
  });

  it('should be defined', () => {
    expect(controller).toBeDefined();
    expect(service).toBeDefined();
  });

  describe('create', () => {
    it('should create a video', async () => {
      const result = await service.create(videosStub[0]);
      expect(result).toEqual(videosStub[0]);
      expect(service.create).toHaveBeenCalledTimes(1);
    });

    it('should throw an exception', () => {
      jest.spyOn(service, 'create').mockRejectedValueOnce(new Error());
      expect(service.create(videosStub[0])).rejects.toThrowError();
      expect(service.create).toHaveBeenCalledTimes(1);
    });
  });

  describe('findAll', () => {
    it('should return a video list', async () => {
      const result = await service.findAll();
      expect(result).toEqual(videosStub);
      expect(service.findAll).toHaveBeenCalledTimes(1);
    });

    it('should throw an exception', () => {
      jest.spyOn(service, 'findAll').mockRejectedValueOnce(new Error());
      expect(service.findAll()).rejects.toThrowError();
      expect(service.findAll).toHaveBeenCalledTimes(1);
    });
  });

  describe('findOne', () => {
    it('should return a video', async () => {
      const result = await service.findOne(videosStub[0].id);
      expect(result).toEqual(videosStub[0]);
      expect(service.findOne).toHaveBeenCalledTimes(1);
    });

    it('should throw an exception', () => {
      jest.spyOn(service, 'findOne').mockRejectedValueOnce(new Error());
      expect(service.findOne(1)).rejects.toThrowError();
      expect(service.findOne).toHaveBeenCalledTimes(1);
    });
  });

  describe('update', () => {
    it('should return a updated video', async () => {
      const result = await service.update(videosStub[0].id, videosStub[0]);
      expect(result).toEqual(videosStub[0]);
      expect(service.update).toHaveBeenCalledTimes(1);
    });

    it('should throw an exception', () => {
      jest.spyOn(service, 'update').mockRejectedValueOnce(new Error());
      expect(service.update(1, videosStub[0])).rejects.toThrowError();
      expect(service.update).toHaveBeenCalledTimes(1);
    });
  });

  describe('remove', () => {
    it('should return a removed video', async () => {
      const result = await service.remove(videosStub[0].id);
      expect(result).toEqual(videosStub[0]);
      expect(service.remove).toHaveBeenCalledTimes(1);
    });

    it('should throw an exception', () => {
      jest.spyOn(service, 'remove').mockRejectedValueOnce(new Error());
      expect(service.remove(1)).rejects.toThrowError();
      expect(service.remove).toHaveBeenCalledTimes(1);
    });
  });
});

Enter fullscreen mode Exit fullscreen mode

Testes dos controllers ok, vamos para os services.

Testando os services

Sem muitas mudanças para cá também, faremos o mesmo que nos controllers, exceto pelo fato de que a dependência deixa de ser o serviço (óbvio, pois estamos no serviço rs) e passa a ser o nosso repository.
Trecho para prestar atenção:

// src/categorias/categorias.service.spec.ts

...

beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        CategoriasService,
        {
          provide: getRepositoryToken(Categoria),
          useValue: {
            save: jest.fn().mockResolvedValue(categoriasStub[0]),
            find: jest.fn().mockResolvedValue(categoriasStub),
            findOne: jest.fn().mockResolvedValue(categoriasStub[0]),
            update: jest.fn().mockResolvedValue(categoriasStub[0]),
            remove: jest.fn().mockResolvedValue(categoriasStub[0]),
          },
        },
      ],
    }).compile();

    service = module.get<CategoriasService>(CategoriasService);
    repository = module.get(getRepositoryToken(Categoria));
  });

...
Enter fullscreen mode Exit fullscreen mode

Dada a devida atenção para o que muda dos controllers, vamos para as implementações dos services.

Testando nosso categorias.service

npm run test:watch -t /home/gabriel/Documentos/alura-challenges-2/src/categorias/categorias.service.spec.ts

# substitua essa parte '/home/gabriel/Documentos' pelo caminho do seu computador, é claro.
Enter fullscreen mode Exit fullscreen mode

O código para testar nosso service de categorias ficará assim:

// src/categorias/categorias.service.spec.ts

import { Test, TestingModule } from '@nestjs/testing';
import { CategoriasService } from './categorias.service';
import { getRepositoryToken } from '@nestjs/typeorm';
import { Categoria } from './entities/categoria.entity';
import { categoriasStub } from '../common/test/categorias.stub';
import { Repository } from 'typeorm';

describe('CategoriasService', () => {
  let service: CategoriasService;
  let repository: Repository<Categoria>;

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        CategoriasService,
        {
          provide: getRepositoryToken(Categoria),
          useValue: {
            save: jest.fn().mockResolvedValue(categoriasStub[0]),
            find: jest.fn().mockResolvedValue(categoriasStub),
            findOne: jest.fn().mockResolvedValue(categoriasStub[0]),
            update: jest.fn().mockResolvedValue(categoriasStub[0]),
            remove: jest.fn().mockResolvedValue(categoriasStub[0]),
          },
        },
      ],
    }).compile();

    service = module.get<CategoriasService>(CategoriasService);
    repository = module.get(getRepositoryToken(Categoria));
  });

  it('should be defined', () => {
    expect(service).toBeDefined();
    expect(repository).toBeDefined();
  });

  describe('save', () => {
    it('should create a category', async () => {
      const newCategory: Omit<Categoria, 'id'> = categoriasStub[0];

      const result = await service.create(newCategory);

      expect(result).toEqual(categoriasStub[0]);
      expect(repository.save).toHaveBeenCalledTimes(1);
    });

    it('should throw an exception', () => {
      jest.spyOn(repository, 'save').mockRejectedValueOnce(new Error());
      expect(service.create(categoriasStub[0])).rejects.toThrowError();
      expect(repository.save).toHaveBeenCalledTimes(1);
    });
  });

  describe('findAll', () => {
    it('should return a categories list', async () => {
      const result = await service.findAll();
      expect(result).toEqual(categoriasStub);
      expect(repository.find).toHaveBeenCalledTimes(1);
    });

    it('should throw an exception', () => {
      jest.spyOn(repository, 'find').mockRejectedValueOnce(new Error());
      expect(service.findAll()).rejects.toThrowError();
      expect(repository.find).toHaveBeenCalledTimes(1);
    });
  });

  describe('findOne', () => {
    it('should return a category', async () => {
      const result = await service.findOne(categoriasStub[0].id);
      expect(result).toEqual(categoriasStub[0]);
      expect(repository.findOne).toHaveBeenCalledTimes(1);
    });

    it('should throw an exception', () => {
      jest.spyOn(repository, 'findOne').mockRejectedValueOnce(new Error());
      expect(service.findOne(1)).rejects.toThrowError();
      expect(repository.findOne).toHaveBeenCalledTimes(1);
    });
  });

  describe('update', () => {
    it('should return a updated category', async () => {
      const result = await service.update(
        categoriasStub[0].id,
        categoriasStub[0],
      );
      expect(result).toEqual(categoriasStub[0]);
      expect(repository.update).toHaveBeenCalledTimes(1);
    });

    it('should throw an exception', () => {
      jest.spyOn(repository, 'update').mockRejectedValueOnce(new Error());
      expect(service.update(1, categoriasStub[0])).rejects.toThrowError();
      expect(repository.update).toHaveBeenCalledTimes(1);
    });
  });

  describe('remove', () => {
    it('should return a removed category', async () => {
      const result = await service.remove(categoriasStub[0].id);
      expect(result).toEqual(categoriasStub[0]);
      expect(repository.remove).toHaveBeenCalledTimes(1);
      expect(repository.findOne).toHaveBeenCalledTimes(1);
    });

    it('should throw an exception', () => {
      jest.spyOn(repository, 'remove').mockRejectedValueOnce(new Error());
      expect(service.remove(1)).rejects.toThrowError();
      expect(repository.findOne).toHaveBeenCalledTimes(1);
    });
  });
});
Enter fullscreen mode Exit fullscreen mode

Testando nosso videos.service

E para o nossos testes do service de videos:

npm run test:watch -t /home/gabriel/Documentos/alura-challenges-2/src/videos/videos.service.spec.ts

# substitua essa parte '/home/gabriel/Documentos' pelo caminho do seu computador, é claro.
Enter fullscreen mode Exit fullscreen mode
// src/videos/videos.service.spec.ts

import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { videosStub } from '../common/test/videos.stub';
import { Video } from './entities/video.entity';
import { VideosService } from './videos.service';

describe('VideosService', () => {
  let service: VideosService;
  let repository: Repository<Video>;

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [
        VideosService,
        {
          provide: getRepositoryToken(Video),
          useValue: {
            save: jest.fn().mockResolvedValue(videosStub[0]),
            find: jest.fn().mockResolvedValue(videosStub),
            findOne: jest.fn().mockResolvedValue(videosStub[0]),
            update: jest.fn().mockResolvedValue(videosStub[0]),
            remove: jest.fn().mockResolvedValue(videosStub[0]),
          },
        },
      ],
    }).compile();

    service = module.get<VideosService>(VideosService);
    repository = module.get(getRepositoryToken(Video));
  });

  it('should be defined', () => {
    expect(service).toBeDefined();
    expect(repository).toBeDefined();
  });

  describe('save', () => {
    const newVideo: Omit<Video, 'id'> = videosStub[0];

    it('should create a video', async () => {
      const result = await service.create(newVideo);
      expect(result).toEqual(videosStub[0]);
      expect(repository.save).toHaveBeenCalledTimes(1);
    });

    it('should throw an exception', () => {
      jest.spyOn(repository, 'save').mockRejectedValueOnce(new Error());
      expect(service.create(videosStub[0])).rejects.toThrowError();
      expect(repository.save).toHaveBeenCalledTimes(1);
    });
  });

  describe('findAll', () => {
    it('should return a videos list if search is not informed', async () => {
      const result = await service.findAll();
      expect(result).toEqual(videosStub);
      expect(repository.find).toHaveBeenCalledTimes(1);
    });

    it('should return a videos list if search is informed', async () => {
      //Arrange
      const expectedResult = videosStub.filter((video) =>
        video.titulo?.includes('teste'),
      );
      jest.spyOn(repository, 'find').mockResolvedValue(expectedResult);

      //Act
      const result = await service.findAll('teste');

      //Assert
      expect(result).toEqual([]);
      expect(repository.find).toHaveBeenCalledTimes(1);
    });

    it('should throw an exception', () => {
      jest.spyOn(repository, 'find').mockRejectedValueOnce(new Error());
      expect(service.findAll()).rejects.toThrowError();
      expect(repository.find).toHaveBeenCalledTimes(1);
    });
  });

  describe('findOne', () => {
    it('should return a video', async () => {
      const result = await service.findOne(videosStub[0].id);
      expect(result).toEqual(videosStub[0]);
      expect(repository.findOne).toHaveBeenCalledTimes(1);
    });

    it('should throw an exception', () => {
      jest.spyOn(repository, 'findOne').mockRejectedValueOnce(new Error());
      expect(service.findOne(1)).rejects.toThrowError();
      expect(repository.findOne).toHaveBeenCalledTimes(1);
    });
  });

  describe('update', () => {
    it('should return a updated video', async () => {
      const result = await service.update(videosStub[0].id, videosStub[0]);
      expect(result).toEqual(videosStub[0]);
      expect(repository.update).toHaveBeenCalledTimes(1);
    });

    it('should throw an exception', () => {
      jest.spyOn(repository, 'update').mockRejectedValueOnce(new Error());
      expect(service.update(1, videosStub[0])).rejects.toThrowError();
      expect(repository.update).toHaveBeenCalledTimes(1);
    });
  });

  describe('remove', () => {
    it('should return a removed video', async () => {
      const result = await service.remove(videosStub[0].id);
      expect(result).toEqual(videosStub[0]);
      expect(repository.remove).toHaveBeenCalledTimes(1);
      expect(repository.findOne).toHaveBeenCalledTimes(1);
    });

    it('should throw an exception', () => {
      jest.spyOn(repository, 'remove').mockRejectedValueOnce(new Error());
      expect(service.remove(1)).rejects.toThrowError();
      expect(repository.findOne).toHaveBeenCalledTimes(1);
    });
  });
});
Enter fullscreen mode Exit fullscreen mode

E com isso, fechamos as implementações da segunda semana.

Aaaah, estou fazendo meus commits conforme vou implementando as coisas... (Padronizadinho, conforme configuramos no início)
Está lá no meu Github.

Abraços e até a próxima semana!

Top comments (0)