728x90
반응형

계속해서 이번엔 Delete와 Create를 테스트 해보자.

 

  describe("deleteOne", () => {

    it("deletes a movie", () => {
      service.create({
        title:"Test Movie",
        genres: ['test'],
        year: 2000
      });
      const allMovies = service.getAll().length;
      service.deleteOne(1);
      const afterDelete = service.getAll().length;
      expect(afterDelete).toBeLessThan(allMovies);
    });

  });

 

한 개의 데이터를 삭제 했으니 기존의 것보다 개수가 줄었다는 것을 테스트한다.

404 error 까지 코드를 추가하고 테스트를 실행해보자.

 

  describe("deleteOne", () => {

    it("deletes a movie", () => {
      service.create({
        title:"Test Movie",
        genres: ['test'],
        year: 2000
      });
      const allMovies = service.getAll().length;
      service.deleteOne(1);
      const afterDelete = service.getAll().length;
      expect(afterDelete).toBeLessThan(allMovies);
    });
    it("should throw 404 error", () => {
      try {
        service.deleteOne(999);
      }
      catch(e){
        expect(e).toBeInstanceOf(NotFoundException)
      }
    })

  });

 

 

이처럼 테스트가 정상 작동하는 것을 볼 수 있다.

 

Create도 마찬가지로 추가하고 테스트를 진행해보자.

  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).toBeGreaterThan(beforeCreate)
    })

  })

 

728x90
반응형
728x90
반응형

이제 function을 테스트해보자.

 

movies.service.spec.ts

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 be return an array", () => {
      const result = service.getAll();
      expect(result).toBeInstanceOf(Array);
    })
  })

});

 

이런 식으로 하단 부분에 테스트 코드를 추가해보면, getAll 함수가 array를 반환하는지에 대해 테스트를 해볼 수 있다.

 

getOne부분도 추가해보자

  describe("getOne", () => {
    it("should be return a movie", () => {
      service.create({
        title:"Test Movie",
        genres: ['test'],
        year: 2000
      });
      
      const movie = service.getOne(1);
      expect(movie).toBeDefined()
    })
  })

 

 

자 이제 npm run test를 해보면, 

 

테스트가 정상적으로 수행되는 것을 확인할 수 있다.

 

 

이번엔 404 error를 테스트해보자.

  it("should throw 404 error", () => {
    try {
      service.getOne(999);
    }
    catch(e){
      expect(e).toBeInstanceOf(NotFoundException)
    }
  })

코드를 추가하고 npm run test를 실행한다.

 

 

테스트가 정상 작동하는 것을 확인할 수 있다.

728x90
반응형
728x90
반응형

 

우리가 만든 movie.service.ts를 테스트 해보자.

 

movies.service.spec.ts

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 부분은 테스트를 묘사한다는 뜻이며, beforeEach 부분은 테스트를 하기 전에 실행되는 것이다.

 

하단에 it 부분이 테스트를 실행하는 부분인데, 여기서 추가해서 테스트를 진행할 수 있다.

코드를 추가해보자.

 

movies.service.spec.ts

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();
  });

  it("should be 4", () => {
    expect(2+2).toEqual(4)
  })
});

 

expect 에 조건을 넣고 2+2 가 toEqual안에 들어있는 4와 같으면 테스트가 통과한다.

테스트를 진행해보면,

 

 

이렇게 결과를 얻을 수 있다.

 

만약, toEqual 부분에 숫자 5를 넣고 돌려보면,

it("should be 4", () => {
    expect(2+2).toEqual(5)
  })

이렇게 실패했다는 화면을 볼 수 있다. 왜 실패했는지도 말해준다.

728x90
반응형
728x90
반응형

참조

https://nomadcoders.co/nestjs-fundamentals/lectures/1953

 

All Courses – 노마드 코더 Nomad Coders

초급부터 고급까지! 니꼬쌤과 함께 풀스택으로 성장하세요!

nomadcoders.co

 

pakage.json 파일을 보면 테스팅과 관련된 스크립트가 5가지 정도 있다.

 

 

먼저 jest는 자바스크립트를 아주 쉽게 테스팅하는 npm 패키지이다.

 

우리가 controller나 service를 만들 때, 이러한 파일들을 볼 수 있었는데

이것이 바로 테스트를 포함한 파일이다.

Nest.js에선 jest가 .spec.ts 파일들을 찾아볼 수 있도록 설정 되어 있다.

 

아래 명령어를 입력해보자.

$ npm run test:cov

 

 

그럼 이런식으로 테스트가 진행된 것을 확인할 수 있다. 이렇게 ts 파일들을 테스트 할 수 있다.

 

 

이번에는 유닛 테스팅을 해보자. 유닛테스팅은 모든 function을 따로 테스트하는 것이다.

예를들면 getAll() function 하나만 테스트하고 싶을 때 사용한다.

 

e2e 테스팅은 특정 링크로 가면 특정 페이지가 나와야하는 경우 사용한다.

728x90
반응형
728x90
반응형

NestJS도 Express 위에서 돌아간다.

때문에 Controller에서 Request, Response 객체가 필요하면 사용할 수 있다.

 

예를 들어 Controller에 Req가 필요하면,

    @Get()
    getAll(@Req() req, @Res() res): Movie[] {
        return this.moviesService.getAll();
    }

 

이런식으로 사용할 수 있다. 하지만 NestJS는 두 개의 프레임워크랑 작동하기 때문에 직접적으로 객체를 사용하는 것이

마냥 좋지만은 않다.

 

이걸 Fstify라는 Express보다 2배 이상 빠른 라이브러리로 대체할 수 있다.

 

아래 Nest.js Doc에서 확인할 수 있다.

 

https://docs.nestjs.kr/techniques/performance

 

네스트JS 한국어 매뉴얼 사이트

네스트JS 한국, 네스트JS Korea 한국어 매뉴얼

docs.nestjs.kr

 

728x90
반응형
728x90
반응형

이전까지 만들었던 모듈을 좀 더 좋은 구조로 만들어보자.

 

app.module.ts

우린 이런 식으로 Controller와 Service를 하나씩 만들었었다.

먼저 모듈을 생성한다는 아래 명령어를 실행해보자.

$nest g mo

 

그다음 이름을 movies로 하게 되면 아래와 같이 되는 것을 확인할 수 있다.

 

먼저 app.module.ts에 먼저 담겨있던 controller와 providers를 삭제한다.

 

app.module.ts

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

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

 

 

 

그다음 movies.module.ts를 아래와 같이 수정해보면 우리는 controller와 providers를 가진 모듈 한 개를 완성한 것이다.

 

movies.module.ts

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

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

}

 

 

 

자 그럼 루트 모듈인 app.module.ts에서의 controllers와 providers는 언제 사용이 될까?

먼저 아래 명령어를 실행해서 app.controller.ts를 생성하고 이름을 app이라 지어준다.

 

$nest g co

 

그다음 app.controller.ts를 아래 사진과 같이 옮기고 app 폴더를 삭제한다.

 

 

app.controller.ts

import { Controller } from '@nestjs/common';

@Controller('app')
export class AppController {}

 

처음 만들 때의 controller의 모습은 위의 코드와 같다.

우리가 첫 라우트 path를 지정할 때  @Controller 안에 이름을 지정을 했었는데, 지금까지 루트 라우터는 없었다.

루트 라우터를 만들어줄 차례다.

 

app.controller.ts

import { Controller, Get } from '@nestjs/common';

@Controller('')
export class AppController {
    @Get()
    home() {
        return 'this is home'
    }
}

이렇게 코드를 바꿔주면 home화면에 this is home 글자를 확인할 수 있을 것이다.

728x90
반응형
728x90
반응형

 

전 글에서 movieData DTO를 만들어봤다.

이번 글에선 updateData DTO를 만들어보겠다.

 

먼저 update-movie.dto.ts 파일을 만든다.

 

update-movie.dto.ts

import {IsString, IsNumber} from 'class-validator'

export class CreateMovieDto {
    @IsString()
    readonly title?: string;

    @IsNumber()
    readonly year?: number;
    
    @IsString({each:true})
    readonly genres?: string[];
}

 

movieData의 DTO와 다른 점은 물음표가 있다는 점인데, 이는 필수사항은 아니게 한다는 뜻이다.

 

그다음 Controller와 Service에 추가하는 방법까지는 똑같다.

여기서 우리는 부분 객체라는 것을 써볼 것이다. 먼저 아래와 같이 설치해준다.

$npm i @nextjs/mapped-types

 

 

그다음 PartialType을 사용하기 위해 코드를 바꿔준다.

import {IsString, IsNumber} from 'class-validator'
import { PartialType } from '@nestjs/mapped-types';
import { CreateMovieDto } from './create-movie.dto';

export class UpdateMovieDto extends PartialType(CreateMovieDto) {}

 

 

이런식으로 CreateMovieDto를 상속할 수 있게 만들 수 있다. 또한 각각의 요소가 필수사항이 아니게끔 만들어준다.

 

이렇게 하고 POST 메소드로 Create하고 PATCH로 업데이트를 실행해보면 성공!

728x90
반응형
728x90
반응형

 

updateData랑 movieData한테 타입을 부여하기 위해서 우리는 DTO(Data Transfer Object, 데이터 전송 객체)를 만들어야한다. 먼저 create-movie.dto.ts 파일을 만들자.

 

 

create-movie.dto.ts

export class CreateMovieDto {
    readonly title: string;
    readonly year: number;
    readonly genres: string[];
}

 

 

그 다음 Controller 부분과 Service 부분에 타입을 추가해주자. (movieData)

 

Controller

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

 

Service

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

 

이렇게 해두면 아래와 같이 movieData. 만 입력해도 클래스가 가진 속성들이 자동으로 나와서 코딩하기에도 편리하다.

또한 타입에 대한 유효성을 검사할 수도 있다.

 

클래스의 유효성 검사를 위해 main.ts에 다음과같이 pipe를 만들어주겠다.

 

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()
  )
  await app.listen(3000);
}
bootstrap();

 

 

또한 아래 명령어를 입력해서  npm 모듈을 설치하자.

$npm i class-validator class-transformer

 

그다음 아까 만들어두었던 dto파일에 다음과같이 데코레이터를 추가하자.

 

create-movie.dto.ts

import {IsString, IsNumber} from 'class-validator'

export class CreateMovieDto {
    @IsString()
    readonly title: string;

    @IsNumber()
    readonly year: number;
    
    @IsString({each:true})
    readonly genres: string[];
}

 

 

 

그 다음 postman에서 다음과 같이 POST 메소드로 {hacked: "by me"} 라는  body를 요청해보면 이런 결과창이 뜬다.

 

ValidationPipe 와 CreateMovieDto를 사용하고 있기 때문에 DTO의 타입을 실시간으로 확인할 수 있는 것이다.

 

ValidationPipe의 옵션 중 유용한 옵션인 whitelist라는 것이 있는데,

true로 설정하면 아무 Decoreator도 없는 어떠한 property의 object를 거룰 수 있다.

 

또한 보안을 위해 forbidNonWhitelisted 라는 옵션도 있다. 

이는 정의를 하지 않은 데이터가 error메세지에 뜨게끔 도와준다

 

transform라는 옵션은

보통 데이터를 body로 받을 때 string으로 받아서 정수형은 parseInt를 써야 했지만,

그럴 필요 없이 바로 원하는 자료형으로 바꿔주는 기능을 한다.

 

 

여기까지 추가를 해보면

 

 

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();

 

 

이렇게 되고, 추가했었던 parseInt를 바꿔주면 된다.

 

movies.controller.ts

import { Controller, Get, Param, Post, Delete, Patch, Body, Query } from '@nestjs/common';
import { CreateMovieDto } from './dto/create-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){
        return this.moviesService.getOne(movieId)
    }

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

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

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

 

 

movies.service.ts

import { Injectable, NotFoundException } from '@nestjs/common';
import { NotFoundError } from 'rxjs';
import { CreateMovieDto } from './dto/create-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(" ID가 존재하지 않습니다. ")
        }

        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
        })
        return true;
    }

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

}

 

728x90
반응형
728x90
반응형

 

우리가 만약 getOne으로 데이터를 찾고자 하는데 실수로 입력을 했다고 치면 데이터가 없는지, 어떤 오류가 있는지 에러 핸들링을 해주어야 한다.

 

movies.service.ts

    getOne(id:string): Movie {
        
        const movie = this.movies.find(movie => movie.id === parseInt(id));
        if(!movie) {
            throw new NotFoundException(" ID가 존재하지 않습니다. ")
        }

        return movie;

    }

 

이런 식으로  getOne부분에 movie가 존재하지 않을 경우 error를 내보내도록 했다.

실제로 postman에서 request를 보내보면

 

 

이런식으로 에러가 뜨는 것을 확인할 수 있다.

 

방금 만든 getOne() 함수로 우리가 데이터를 삭제할 때 해당 Id를 가지고 있는 Movie가 존재하는지 먼저 확인해 볼 수 있고 에러 처리를 할 수도 있다. 때문에 코드가 더 깔끔해진다.

 

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

 

 

이번엔 update service를 만들어보자. 먼저, Controller 부분을 바꿔주자.

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

 

 

아직 Service부분에서 update 함수를 선언하지 않았기 때문에 당연히 에러가 뜬다. Service부분을 보자.

 

movies.service.ts

import { Injectable, NotFoundException } from '@nestjs/common';
import { NotFoundError } from 'rxjs';
import { Movie } from './entities/movie.entity';

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

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

    getOne(id:string): Movie {
        
        const movie = this.movies.find(movie => movie.id === parseInt(id));
        if(!movie) {
            throw new NotFoundException(" ID가 존재하지 않습니다. ")
        }

        return movie;

    } 

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

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

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

}

 

 

postman에서 실행해보면 정상작동하는것을 확인할 수 있다.

728x90
반응형
728x90
반응형

Service는 movie의 로직을 관리하는 역할을 맡을 것이다. Controller가 URL을 받아오면, 원하는 비즈니스 로직을

실행해준다. ( DB 작업 등이 이곳에서 이루어질 것이다. )

 

먼저 Controller를 만들었던것과 같이 Service를 만들어보자.

nest g s

 

그럼, 이름을 입력하라고 하는데, Controller와 같은 movies로 만들어보겠다.

 

이렇게 실행을 해주면!

 

 

 

위와 같이 자동으로 파일들이 생성되고, 루트 모듈에서 선언되는것을 확인 할 수 있다. ( 매우 편리한 기능! )

위에서 말했듯이 Service에선 주로 DB작업등과 같은 로직을 실행하는데, 이 강의에서는 실제DB를 다루는 것은 하진 않는다. 내 블로그에 있는 MongoDB 파트를 완벽히 이해하면 충분히 할 수 있을 것이다.

 

 

먼저, src 폴더 아래 다음과 같이 파일을 생성해준다.

 

 

그 다음 Movie class를 만들어준다.

 

movie.entity.ts

export class Movie {
    id: number;
    title: string;
    year: number;
    genres: string[];
}

 

 

이 class를 Service 파일에서 가져온다.

 

movie.service.ts

import { Injectable } from '@nestjs/common';
import { Movie } from './entities/movie.entity';

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

 

그 다음  Controller에서 쓸 함수를 정의한다. 이 때 이름은 중복되어도 상관이 없다.

 

movie.service.ts

import { Injectable } from '@nestjs/common';
import { Movie } from './entities/movie.entity';

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

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

    getOne(id:string): Movie {
        return this.movies.find(movie => movie.id === parseInt(id));
    } 
}

 

DB에 접근해서 모든 Movie를 조회하는 함수와 id를 통해 Movie 한개를 조회하는 함수를 작성했다.

 

 

이번엔 Controller에서 방금 만들었던 Service를 사용해보자. express.js처럼 수도으로 import하는 방법은 Nest.js에서 기본적으로 쓰는 방법이 아니다. 우리가 요청을 해야한다.

 

먼저 class에 구조체(constructor)를 가져오고, getAll() 부분과 getOne() 부분을 바꿔보겠다.

 

import { Controller, Get, Param, Post, Delete, Patch, Body, Query } from '@nestjs/common';
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('search') 
    search(@Query('year') searchingYear : string) {
        return `${searchingYear} 이라는 데이터를 찾을 것입니다.`
    }

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

    @Post()
    create(@Body() movieData : Object) {
        return movieData;
    }

    @Delete('/:id')
    remove(@Param('id') movieId:string) {
        return `${movieId} id를 가진 movie를 삭제합니다.`;
    }

    @Patch('/:id')
    patch(@Param('id') movieId:string, @Body() updateData) {
        return {
            updatedMovie : movieId,
            ...updateData
        }
    }
}

 

 

이번엔 Service에서 delete함수와 create함수를 만들어보겠다.

 

movies.service.ts

import { Injectable } from '@nestjs/common';
import { Movie } from './entities/movie.entity';

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

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

    getOne(id:string): Movie {
        return this.movies.find(movie => movie.id === parseInt(id));
    } 

    deleteOne(id:string): boolean {
        this.movies.filter(movie => movie.id !== parseInt(id))
        return true;
    }

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

 

그리고 Controller 부분을 바꿔주자. search부분은 이미 설명을 했으니 삭제하겠다.

 

movies.controller.ts

import { Controller, Get, Param, Post, Delete, Patch, Body, Query } from '@nestjs/common';
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: string){
        return this.moviesService.getOne(movieId)
    }

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

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

    @Patch('/:id')
    patch(@Param('id') movieId:string, @Body() updateData) {
        return {
            updatedMovie : movieId,
            ...updateData
        }
    }
}

 

 

동작은 postman에서 확인할 수 있다.

 

 

 

 

 

참조

https://nomadcoders.co/nestjs-fundamentals/lectures/1947

 

All Courses – 노마드 코더 Nomad Coders

초급부터 고급까지! 니꼬쌤과 함께 풀스택으로 성장하세요!

nomadcoders.co

 

728x90
반응형

'Back-End > Nest.js' 카테고리의 다른 글

Nest.js | REST API | DTOs and Validation(1)  (0) 2021.09.29
Nest.js | REST API | Movies Service(2)  (0) 2021.09.29
Nest.js | REST API | More Routes  (0) 2021.09.28
Nest.js | REST API | Movies Controller  (0) 2021.09.24
Nest.js | 개요 | Modules  (0) 2021.09.24

+ Recent posts