๐Ÿคž Nestjs ์—ฐ์Šต with Nomadcoder

Jake_Youngยท2020๋…„ 12์›” 25์ผ
0

์ฝ”๋“œ ์ผ์ง€, Code Archive

๋ชฉ๋ก ๋ณด๊ธฐ
2/10

๊ฐ•์˜ ๋งํฌ๋Š” ์—ฌ๊ธฐ๋ฅผ ํด๋ฆญํ•˜์„ธ์š”๐Ÿ‘†

๐Ÿง‘ Nestjs ๋ฐฐ์šฐ๊ธฐ

  • Nestjs๋ž€ Express ๊ธฐ๋ฐ˜์˜ ์„œ๋ฒ„๊ฐœ๋ฐœ์šฉ Framework์ด๋‹ค.
  • Nestjs๊ฐ€ ํ•ซํ•˜๋‹ค๊ณ  ํ•˜์—ฌ ํ•ญ์ƒ ๊ณต๋ถ€ํ•˜๊ณ  ์‹ถ์—ˆ๋‹ค.
  • Express๋กœ ์„œ๋ฒ„๋ฅผ ๊ฐœ๋ฐœํ•˜๋ฉด ํ•ญ์ƒ ๋ฌด์—์„œ ์œ ๋ฅผ ์ฐฝ์กฐํ•ด์•ผ ํ–ˆ๋‹ค.
  • ๋ฉ‹์žˆ๋Š” ์ž‘์—…์ด๋ผ๊ณ  ์ƒ๊ฐํ•˜๊ธด ํ–ˆ์ง€๋งŒ, ํ•ญ์ƒ ๋ง‰๋ง‰ํ•˜๊ณ  ๋ญ”๊ฐ€ ๋‘๋ ค์šด ๋Š๋‚Œ์ด ๋“ค์—ˆ๋‹ค.
  • Java ์ง„์˜์˜ Spring Boot์™€ ์œ ์‚ฌํ•˜๋‹ค๊ณ  ํ•˜์—ฌ ๋ฐฐ์›Œ๋ณด๊ณ  ์‹ถ์€ ๋งˆ์Œ๋„ ์žˆ์—ˆ๋‹ค.
  • Python์˜ Django ์ฒ˜๋Ÿผ ํด๋” ์ด๋ฆ„๊ณผ ๊ธฐ๋ณธ์ ์ธ ์„ธํŒ…๊นŒ์ง€ ๋‹ค ์ œ๊ณต๋˜์—ˆ๋‹ค.
  • ์‚ฌ์šฉํ•ด๋ณด๋‹ˆ ๋ฉ‹์ง„ ๋ถ€๋ถ„์ด ๋งŽ์ด ์žˆ์–ด์„œ ์ข‹์•˜๋‹ค.

๐Ÿ‘ณโ€โ™‚๏ธ Jest ์‚ฌ์šฉํ•ด๋ณด๊ธฐ

  • ๊ฐœ๋ฐœ ํ…Œ์ŠคํŠธ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ํ•ญ์ƒ ์‚ฌ์šฉํ•ด์•ผํ•œ๋‹ค๊ณ .. ์—ญ์‹œ๋‚˜ ์ƒ๊ฐ๋งŒ ํ•˜๊ณ  ์žˆ์—ˆ๋‹ค.
  • ์ด๋ฒˆ ๊ฐ•์˜๋ฅผ ํ†ตํ•ด ๊ทธ ๊ธฐ๋ณธ์ ์ธ ํ™œ์šฉ์— ๋Œ€ํ•ด ๊ณต๋ถ€ํ•  ์ˆ˜ ์žˆ์—ˆ๋‹ค.
  • ์ฝ”๋“œ๋ฅผ ์—…๋ฐ์ดํŠธ ํ•˜๋ฉด ํ•ญ์ƒ ๋‘๋ ค์›€์ด ์•ž์„ฐ์—ˆ๋‹ค.
  • ์ด์ œ๋Š” ๋งˆ์Œ๊ป ์ˆ˜์ •ํ•˜๊ณ  ํ…Œ์ŠคํŠธ๋งŒ ๋Œ๋ฆฌ๋ฉด ๋œ๋‹ค!! (๊ทธ๋ž˜๋„ ๋ถ€์กฑํ•œ ๋ถ€๋ถ„์ด ์žˆ๊ฒ ์ง€๋งŒ..)

๐Ÿ‘ฑโ€โ™€๏ธ ํด๋” ๊ตฌ์กฐ

๐Ÿ‘ฎโ€โ™‚๏ธ ์ž‘์„ฑ ์ฝ”๋“œ

// main.ts
import { ValidationPipe } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalPipes(
    new ValidationPipe({
      whitelist: true,
      forbidNonWhitelisted: true,
      transform: true,
    }),
  );
  await app.listen(3000);
}
bootstrap();


// app.module.ts
import { Module } from '@nestjs/common';
import { MoviesModule } from './movies/movies.module';
import { AppController } from './app.controller';

@Module({
  imports: [MoviesModule],
  controllers: [AppController],
  providers: [],
})
export class AppModule {}


//app.controller.js
import { Controller, Get } from '@nestjs/common';

@Controller('')
export class AppController {
  @Get()
  home() {
    return 'Welcome to my Movie API';
  }
}


//movies.modules.ts
import { Module } from '@nestjs/common';
import { MoviesController } from './movies.controller';
import { MoviesService } from './movies.service';

@Module({
  controllers: [MoviesController],
  providers: [MoviesService],
})
export class MoviesModule {}


//movies.service.ts
import { Injectable, NotFoundException } from '@nestjs/common';
import { CreateMovieDto } from './dto/create-movie.dto';
import { UpdateMovieDto } from './dto/update-movie.dto';
import { Movie } from './entities/movie.entity';

@Injectable()
export class MoviesService {
  private movies: Movie[] = [];

  getAll(): Movie[] {
    return this.movies;
  }

  getOne(id: number): Movie {
    const movie = this.movies.find((movie) => movie.id === id);
    if (!movie) {
      throw new NotFoundException(`Movie with ID ${id} not found.`);
    }
    return movie;
  }

  deleteOne(id: number) {
    this.getOne(id);
    this.movies = this.movies.filter((movie) => movie.id !== id);
  }

  create(movieData: CreateMovieDto) {
    this.movies.push({
      id: this.movies.length + 1,
      ...movieData,
    });
  }

  update(id: number, updateData: UpdateMovieDto) {
    const movie = this.getOne(id);
    this.deleteOne(id);
    this.movies.push({ ...movie, ...updateData });
  }
}


//movies.controller.ts
import {
  Body,
  Controller,
  Delete,
  Get,
  Param,
  Patch,
  Post,
} from '@nestjs/common';
import { CreateMovieDto } from './dto/create-movie.dto';
import { UpdateMovieDto } from './dto/update-movie.dto';
import { Movie } from './entities/movie.entity';
import { MoviesService } from './movies.service';

@Controller('movies')
export class MoviesController {
  constructor(private readonly moviesService: MoviesService) {}

  @Get()
  getAll(): Movie[] {
    return this.moviesService.getAll();
  }

  @Get('/:id')
  getOne(@Param('id') movieId: number): Movie {
    return this.moviesService.getOne(movieId);
  }

  @Post()
  create(@Body() movieData: CreateMovieDto) {
    return this.moviesService.create(movieData);
  }

  @Delete('/:id')
  delete(@Param('id') movieId: number) {
    return this.moviesService.deleteOne(movieId);
  }

  @Patch('/:id')
  update(@Param('id') movieId: number, @Body() updateData: UpdateMovieDto) {
    return this.moviesService.update(movieId, updateData);
  }
}


//movie.entity.ts
export class Movie {
  id: number;
  title: string;
  year: number;
  genres: string[];
}


//create-movie.dto.ts
import { IsNumber, IsString, IsOptional } from 'class-validator';

export class CreateMovieDto {
  @IsString()
  readonly title: string;
  @IsNumber()
  readonly year: number;
  @IsOptional()
  @IsString({ each: true })
  readonly genres: string[];
}


//update-movie.dto.ts
import { PartialType } from '@nestjs/mapped-types';
import { CreateMovieDto } from './create-movie.dto';

export class UpdateMovieDto extends PartialType(CreateMovieDto) {}


//movies.service.spec.ts
import { NotFoundException } from '@nestjs/common';
import { Test, TestingModule } from '@nestjs/testing';
import { MoviesService } from './movies.service';

describe('MoviesService', () => {
  let service: MoviesService;

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [MoviesService],
    }).compile();

    service = module.get<MoviesService>(MoviesService);
  });

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

  describe('getAll', () => {
    it('should return an array', () => {
      const result = service.getAll();
      expect(result).toBeInstanceOf(Array);
    });
  });

  describe('getOne', () => {
    it('should return a movie', () => {
      service.create({
        title: 'Test Movie',
        genres: ['test'],
        year: 2000,
      });
      const movie = service.getOne(1);
      expect(movie).toBeDefined();
      expect(movie.id).toEqual(1);
    });
    it('should throw 404 error', () => {
      try {
        service.getOne(999);
      } catch (e) {
        expect(e).toBeInstanceOf(NotFoundException);
        expect(e.message).toEqual(`Movie with ID 999 not found.`);
      }
    });
  });

  describe('deleteOne', () => {
    it('deletes a movie', () => {
      service.create({
        title: 'Test Movie',
        genres: ['Test'],
        year: 2000,
      });
      const allMovies = service.getAll();
      service.deleteOne(1);
      const afterDelete = service.getAll();
      expect(afterDelete.length).toEqual(allMovies.length - 1);
    });
    it('should return a 404', () => {
      try {
        service.deleteOne(999);
      } catch (e) {
        expect(e).toBeInstanceOf(NotFoundException);
        expect(e.message).toEqual(`Movie with ID 999 not found.`);
      }
    });
  });

  describe('create', () => {
    it('should create a movie', () => {
      const beforeCreate = service.getAll().length;
      service.create({
        title: 'Test Movie',
        genres: ['Test'],
        year: 2000,
      });
      const afterCreate = service.getAll().length;
      expect(afterCreate).toEqual(beforeCreate + 1);
    });
  });

  describe('dupdate', () => {
    it('should update a movie', () => {
      service.create({
        title: 'Test Movie',
        genres: ['Test'],
        year: 2000,
      });
      service.update(1, { title: 'Updated Test' });
      const movie = service.getOne(1);
      expect(movie.title).toEqual('Updated Test');
    });
    it('should throw a NotFoundExe', () => {
      try {
        service.update(999, {});
      } catch (e) {
        expect(e).toBeInstanceOf(NotFoundException);
        expect(e.message).toEqual(`Movie with ID 999 not found.`);
      }
    });
  });
});


//app.e2e-spec.ts
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication, ValidationPipe } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from './../src/app.module';

describe('AppController (e2e)', () => {
  let app: INestApplication;

  beforeAll(async () => {
    const moduleFixture: TestingModule = await Test.createTestingModule({
      imports: [AppModule],
    }).compile();

    app = moduleFixture.createNestApplication();
    app.useGlobalPipes(
      new ValidationPipe({
        whitelist: true,
        forbidNonWhitelisted: true,
        transform: true,
      }),
    );
    await app.init();
  });

  it('/ (GET)', () => {
    return request(app.getHttpServer())
      .get('/')
      .expect(200)
      .expect('Welcome to my Movie API');
  });

  describe('/movies', () => {
    it('GET', () => {
      return request(app.getHttpServer()).get('/movies').expect(200).expect([]);
    });
    it('POST 201', () => {
      return request(app.getHttpServer())
        .post('/movies')
        .send({
          title: 'Test',
          year: 2000,
          genres: ['test'],
        })
        .expect(201);
    });
    it('POST 400', () => {
      return request(app.getHttpServer())
        .post('/movies')
        .send({
          title: 'Test',
          year: 2000,
          genres: ['test'],
          other: 'thing',
        })
        .expect(400);
    });
    it('DELETE', () => {
      return request(app.getHttpServer()).delete('/movies').expect(404);
    });
  });

  describe('/movies/:id', () => {
    it('GET 200', () => {
      return request(app.getHttpServer()).get('/movies/1').expect(200);
    });
    it('GET 404', () => {
      return request(app.getHttpServer()).get('/movies/999').expect(404);
    });
    it('PATCH 200', () => {
      return request(app.getHttpServer())
        .patch('/movies/1')
        .send({ title: 'Updated Test' })
        .expect(200);
    });
    it('DELETE 200', () => {
      return request(app.getHttpServer()).delete('/movies/1').expect(200);
    });
  });
});
profile
์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ์™€ ํŒŒ์ด์ฌ ๊ทธ๋ฆฌ๊ณ  ์ปดํ“จํ„ฐ์™€ ๋„คํŠธ์›Œํฌ

0๊ฐœ์˜ ๋Œ“๊ธ€