kakasoo

Nestia를 활용한 e2e 테스트 본문

프로그래밍/NestJS

Nestia를 활용한 e2e 테스트

카카수(kakasoo) 2023. 3. 1. 19:41
반응형

이번에 테스팅할 API는?

import { TypedBody, TypedRoute } from '@nestia/core';
import { Controller } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { createResponseForm, ResponseForm } from '../interceptors/transform.interceptor';
import { CreateUserDto } from '../models/dtos/create-user.dto';
import { DecodedUserToken } from '../models/tables/user.entity';
import { UsersService } from '../providers/users.service';

@Controller('api/v1/auth')
export class AuthController {
  constructor(private readonly usersService: UsersService, private readonly jwtService: JwtService) {}

  /**
   * 230129 - Local 로그인을 위한 User 생성
   *
   * @tag auth
   * @param CreateUserDto 유저를 생성하기 위해 필요한 최소한의 값 정의
   */
  @TypedRoute.Post('sign-up')
  async signUp(@TypedBody() createUserDto: CreateUserDto): Promise<ResponseForm<DecodedUserToken>> {
    const { password, ...user } = await this.usersService.create(createUserDto);
    return createResponseForm(user);
  }
}
  • POST api/v1/auth/sign-up
  • body로는 CreateUserDto라는 타입으로 정의된 객체를 받아, 유저를 생성해주는 회원가입 메서드다.

 

CreateUserDto

import { UserEntity } from '../tables/user.entity';

export type CreateUserDto = Pick<
  UserEntity,
  'name' | 'nickname' | 'email' | 'password' | 'phoneNumber' | 'birth' | 'gender' | 'smsAdsConsent' | 'emailAdsConsent'
>;
  • name
  • nickname
  • email
  • password
  • phoneNumber
  • birth
  • gender
  • smsAdsConsent
  • emailAdsConsent

이러한 값들을 가지고 있다.

성별과 생일 등 민감한 정보를 제외하고는 무조건 입력해야만 회원가입할 수 있는 타입이다.

 

SDK 생성하기

import type SDK from '@nestia/sdk';

export const NESTIA_CONFIG: SDK.INestiaConfig = {
  /**
   * List of files or directories containing the NestJS controller classes.
   */
  input: ['src/controllers', 'src/auth'],

  /**
   * Output directory that SDK would be placed in.
   *
   * If not configured, you can't build the SDK library.
   */
  output: 'src/api',

  /**
   * Whether to assert parameter types or not.
   *
   * If you configure this property to be `true`, all of the function parameters would be
   * checked through the [typia](<https://github.com/samchon/typia#runtime-type-checkers>).
   * This option would make your SDK library slower, but would enahcne the type safety even
   * in the runtime level.
   *
   * @default false
   */
  // assert: true,

  /**
   * Whether to optimize JSON string conversion 2x faster or not.
   *
   * If you configure this property to be `true`, the SDK library would utilize the
   * [typia](<https://github.com/samchon/typia#fastest-json-string-converter>)
   * and the JSON string conversion speed really be 2x faster.
   *
   * @default false
   */
  // json: true,

  /**
   * Whether to wrap DTO by primitive type.
   *
   * If you don't configure this property as `false`, all of DTOs in the
   * SDK library would be automatically wrapped by {@link Primitive} type.
   *
   * For refenrece, if a DTO type be capsuled by the {@link Primitive} type,
   * all of methods in the DTO type would be automatically erased. Also, if
   * the DTO has a `toJSON()` method, the DTO type would be automatically
   * converted to return type of the `toJSON()` method.
   *
   * @default true
   */
  // primitive: false,

  /**
   * Building `swagger.json` is also possible.
   *
   * If not specified, you can't build the `swagger.json`.
   */
  swagger: {
    /**
     * Output path of the `swagger.json`.
     *
     * If you've configured only directory, the file name would be the `swagger.json`.
     * Otherwise you've configured the full path with file name and extension, the
     * `swagger.json` file would be renamed to it.
     */
    output: 'dist/swagger.json',
    security: {
      bearer: {
        type: 'apiKey',
        in: 'header',
        name: 'Authorization',
      },
    },
  },
};
export default NESTIA_CONFIG;

이번에는 SDK에 대해 설명할 것이기 때문에 아래 스웨거 관련된 설정은 볼 필요가 없다.

봐야 할 부분은 config.include, config.output으로 충분하다.

나의 경우 nestia sdk가 적용될 API들이 src/controllers 뿐만 아니라 src/auth에도 있었다.

나는 auth를 따로 관리하기 때문에 auth 폴더도 include에 포함시켜야 해서 이렇게 정의해두었다.

또한 output의 경우에는 src/api로 했다.

 

미리 말하는데, SDK는 우리가 작성한 API를 호출하는 함수로 구성된, TypeScript 파일들이다.

즉, 다시 한 번 컴파일하여 JS로 만들어야 패키지 형태로 프론트 개발자에게 전달할 수 있다.

생성 명령어는 npx nestia sdk 이다.

 

패키지 만들기

// tsconfig.api.json

{
  "compilerOptions": {
    /* Visit <https://aka.ms/tsconfig.json> to read more about this file */

    /* Basic Options */
    // "incremental": true,                   /* Enable incremental compilation */
    "target": "ES5" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */,
    "module": "CommonJS" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */,
    "lib": ["DOM", "ES2015"] /* Specify library files to be included in the compilation. */,
    // "allowJs": true,                       /* Allow javascript files to be compiled. */
    // "checkJs": true,                       /* Report errors in .js files. */
    // "jsx": "preserve",                     /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
    "declaration": true /* Generates corresponding '.d.ts' file. */,
    // "declarationMap": true,                /* Generates a sourcemap for each corresponding '.d.ts' file. */
    // "sourceMap": true,                     /* Generates corresponding '.map' file. */
    // "outFile": "./",                       /* Concatenate and emit output to single file. */
    "outDir": "./packages/api/lib" /* Redirect output structure to the directory. */,
    // "rootDir": "./",                       /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
    // "composite": true,                     /* Enable project compilation */
    // "tsBuildInfoFile": "./",               /* Specify file to store incremental compilation information */
    // "removeComments": true,                /* Do not emit comments to output. */
    // "noEmit": true,                        /* Do not emit outputs. */
    // "importHelpers": true,                 /* Import emit helpers from 'tslib'. */
    "downlevelIteration": true /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */,
    // "isolatedModules": true,               /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */

    /* Strict Type-Checking Options */
    "strict": true /* Enable all strict type-checking options. */,
    // "noImplicitAny": true,                 /* Raise error on expressions and declarations with an implied 'any' type. */
    // "strictNullChecks": true,              /* Enable strict null checks. */
    // "strictFunctionTypes": true,           /* Enable strict checking of function types. */
    // "strictBindCallApply": true,           /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
    // "strictPropertyInitialization": true,  /* Enable strict checking of property initialization in classes. */
    // "noImplicitThis": true,                /* Raise error on 'this' expressions with an implied 'any' type. */
    // "alwaysStrict": true,                  /* Parse in strict mode and emit "use strict" for each source file. */

    /* Additional Checks */
    "noUnusedLocals": true /* Report errors on unused locals. */,
    "noUnusedParameters": true /* Report errors on unused parameters. */,
    "noImplicitReturns": true /* Report error when not all code paths in function return a value. */,
    "noFallthroughCasesInSwitch": true /* Report errors for fallthrough cases in switch statement. */,

    /* Module Resolution Options */
    // "moduleResolution": "node",            /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
    // "baseUrl": "./",                       /* Base directory to resolve non-absolute module names. */
    // "paths": {},                           /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
    // "rootDirs": [],                        /* List of root folders whose combined content represents the structure of the project at runtime. */
    // "typeRoots": [],                       /* List of folders to include type definitions from. */
    // "types": [],                           /* Type declaration files to be included in compilation. */
    // "allowSyntheticDefaultImports": true,  /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
    "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */,
    // "preserveSymlinks": true,              /* Do not resolve the real path of symlinks. */
    // "allowUmdGlobalAccess": true,          /* Allow accessing UMD globals from modules. */

    /* Source Map Options */
    // "sourceRoot": "",                      /* Specify the location where debugger should locate TypeScript files instead of source locations. */
    // "mapRoot": "",                         /* Specify the location where debugger should locate map files instead of generated locations. */
    // "inlineSourceMap": true,               /* Emit a single file with source maps instead of having a separate file. */
    // "inlineSources": true,                 /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */

    /* Experimental Options */
    "experimentalDecorators": true /* Enables experimental support for ES7 decorators. */,
    // "emitDecoratorMetadata": true,         /* Enables experimental support for emitting type metadata for decorators. */
    "stripInternal": true,

    /* Advanced Options */
    "skipLibCheck": true /* Skip type checking of declaration files. */,
    "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
  },
  "include": ["src/api"]
}

일단 서버 프로젝트 폴더 내부에 tsconfig.json 대신 사용할 tsconfig.api.json을 만든다.

이건 이제 nestia를 이용해 생성된 SDK ( ts file ) 을 다시 한 번 JS 파일로 바꿔주기 위해 사용된다.

사실 클라이언트 ( = 프론트 개발자 ) 에게 주기 위해서는 빌드 결과물만 있어도 충분하지만,

이 SDK가 있다면 서버 입장에서는 e2e 테스팅 용도로 사용하는 데 매우 효과적이다.

그래서 내 생각엔, Nestia에서는 SDK를 한 번 거쳐서 packages를 생성하게끔 만든 것으로 보인다.

sdk를 컴파일하는 명령어는 tsc -p tsconfig.api.json 이다.

 

// package.json
{
    "script": {
        "sdk": "npx nestia sdk && npx tsc -p tsconfig.api.json"
    }
}

그래서 나는 이와 같이 스크립트 명령어를 하나 추가해두었다.

 

SDK 사용 방법

import * as AuthApis from '../../api/functional/api/v1/auth'; // src/api/functional/api/v1/auth

const host = '<http://127.0.0.1:3000>';

AuthApis.sign_up.signUp({ host }, {
    name: 'kakasoo',
    nickname: 'kakasoo',
    email: 'test@example.com',
    password: 'string',
    phoneNumber: '01085257658',
    gender: true,
    smsAdsConsent: true,
    emailAdsConsent: true,
}).then((el) => console.log(el)); // NOTE : createdUser

추후 배포할 package나 SDK는 경로 상 다음과 같은 규칙이 있다.

api/functional 내부부터는 api의 경로와 일치한다는 점이다.

이 함수 호출은 typia.random<T>() 을 이용해서 아래와 같이 더 간편한 형태로 바꿀 수 있다.

 

AuthApis.sign_up.signUp({ host }, typia.random<CreateUserDto>()).then((el) => console.log(el));

typia의 random 함수는 지정한 타입 파라미터 형태에 맞게 값을 생성해주는 함수다.

이게 어떻게 가능한지는 아직 잘 모르겠지만(!?) 테스트에 정말로 유용하다.

이제 faker.js 따위는(?) 쓰지 않아도 된다.

 

import * as AuthApis from '../../api/functional/api/v1/auth'; // src/api/functional/api/v1/auth
import * as Apis from '../../api/functional';

/**
 * 아래 두 함수 호출은 동일하다. 
 */

AuthApis.sign_up.signUp(); // 파라미터 생략
Apis.api.v1.auth.sign_up.signUp(); // 파라미터 생략

아래 Apis.api.v1.auth.sign_up은, 내가 만든 AuthController를 보면 바로 이해할 수 있다.

내 회원가입 메서드의 경로는 `POST api/v1/auth/sign-up' 이었고, 컨트롤러 함수 이름은 signUp이었다.

하이픈(-) 기호는 언더바로 대체되었을 뿐이고, 최종적인 함수 네이밍은 컨트롤러와 일치한다.

따라서 import를 할 때, 원하는 API 경로만 갖고와도 되고, 전체를 갖고 온 다음에 함수로 작성해도 된다.

 

Nestia SDK를 활용한 e2e 테스트

import { Test, TestingModule } from '@nestjs/testing';
import { AppModule } from '../../app.module';
import { INestApplication } from '@nestjs/common';
import * as AuthApis from '../../api/functional/api/v1/auth';
import { CreateUserDto } from '../../models/dtos/create-user.dto';
import typia from 'typia';

describe('E2E article test', () => {
  const host = '<http://localhost:3000>';
  let app: INestApplication;
  let testingModule: TestingModule;

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

    app = testingModule.createNestApplication();
    await (await app.init()).listen(3000);
  });

  afterAll(async () => {
    await app.close();
  });

  describe('auth API test', () => {
    it('should return an array of articles', async () => {
      const signUpResponse = await AuthApis.sign_up.signUp({ host }, typia.random<CreateUserDto>());
      const createdUser = signUpResponse.data;

      expect(createdUser.id).toBeDefined();
    });
  });
});

Testing Module을 만들고, app을 직접 실행시킨 다음에 API를 호출해서 테스트해볼 수 있다.

모든 테스트가 끝나면 afterAll을 이용해서 서버를 닫아주면 된다.

반응형