Hun's Blog

Nexus GraphQL tutorials (4) - Testing your API 본문

Backend/GraphQL

Nexus GraphQL tutorials (4) - Testing your API

jhk-im 2020. 12. 7. 12:36

GraphQL을 개인 프로젝트에 적용하기위해 제대로 학습해보기 시리즈

해당 글은 Nexus 를 개인 프로젝트에 적용하고자 제대로 학습하기 위해 Nexus tutorial의 내용을 정리한 것입니다.

Nexus tutorial에도 상세하게 나와있음을 알려드립니다. 

Testing your API

 

4. Testing your API

4. Testing your API

nexusjs.org

지금까지 구현된 내용은 Playground를 통해 수동으로 검증하였다. 처음에는 문제가 없지만 어느 시점부터는 자동화 된 테스트를 원할 것이다. 해당 챕터에서 몇 가지 자동화된 테스트를 추가해보자. 

 

GraphQL API 테스트 방법은 여러가지가 있다. 한 가지 방법은 Resolver를 분리하여 분리 된 함수에 추출한 후 유닛 테스트를 하는 것이다. 이러한 기능은 pure function이 아니기 때문에 부분 통합 테스트 혹은 테스트 유닛 level을 유지하기 위한 Mock이 도입된다. 

 

Resolver Unit Test 의 문제

  • 추후 사용하게 될 Prisma는 Resolver를 쓸 필요가 없을 수 있다. 이 경우 Reslover를 테스트하는 것은 무의미하다. 
  • Nexus가 가져오는 Static type safety 덕분에 입력 및 예상 출력 유형에 대한 테스트가 크게 감소할 수 있다. ( 예를들어 Resolver가 null을 검사하는지, 올바른 유형을 반환하는지를 테스트할 필요가 없다.)
  • Resolver는 내부적인 것에 초점을 맞추기 때문에 정확한 client의 관점을 제공할 수 없다. 

Nexus는 실제 Client 처럼 API를 실제로 실행하여 테스트 하는 System testing을 서포트한다. 

 

Setting up your test environment

Jest를 사용한다. 의무사항은 아니지만 Nexus에서 추천하는 방법이다.

선호하는 테스트 프레임워크를 사용해도 무방하다. 

yarn add --dev jest @types/jest ts-jest graphql-request get-port
// package.json
"scripts": {
	...
  "generate": "ts-node --transpile-only api/schema",
  "test": "npm run generate && jest"
},
	...
"jest": {
  "preset": "ts-jest",
  "globals": {
    "ts-jest": {
      "diagnostics": { "warnOnly": true }
    }
  },
  "testEnvironment": "node"
}
mkdir tests && touch tests/Post.test.ts

 

Testing the publish mutation

쉬운 테스트를 위해서 통합 테스트를 실행하도록 설계 된 createTestContext 유틸리티를 생성한다. 

테스트가 실행되면 동일한 프로세스에서 앱을 부팅하고 테스트와 상호작용하기 위한 인터페이스를 노출한다. 

Jest는 각 테스트를 자체 프로세스로 실행하므로 8개의 테스트를 병렬로 실행하면 8개의 앱 프로세스도 실행됨을 의미한다. 

// tests/__helpers.ts                                            // 1
import { ServerInfo } from "apollo-server";
import getPort, { makeRange } from "get-port";
import { GraphQLClient } from "graphql-request";
import { server } from "../api/server";

type TestContext = {
  client: GraphQLClient;
};

export function createTestContext(): TestContext {
  let ctx = {} as TestContext;
  const graphqlCtx = graphqlTestContext();
  beforeEach(async () => {                                        // 2
    const client = await graphqlCtx.before();
    Object.assign(ctx, {
      client,
    });
  });
  afterEach(async () => {                                         // 3
    await graphqlCtx.after();
  });
  return ctx;                                                     // 8
}

function graphqlTestContext() {
  let serverInstance: ServerInfo | null = null;
  return {
    async before() {
      const port = await getPort({ port: makeRange(4000, 6000) });  // 4
      serverInstance = await server.listen({ port });               // 5
      return new GraphQLClient(`http://localhost:${port}`);         // 6
    },
    async after() {
      serverInstance?.server.close();                               // 7
    },
  };
}
  1. 모듈 네임의 prefix(__helpers.ts)는 Jest 스냅샷 폴더의 __snapthots__과 일치한다. 
  2. Jest 라이프사이클 beforeEach를 활용한다. 각각의 테스트 이전에 Jest에 의해 호출된다. 
  3. Jest 라이프사이클 afterEach를 활용한다. 각각의 테스트 종료 후 Jest에 의해 호출된다. 
  4. 여러개의 서버를 동시에 실행할수 있도록 random 포트를 사용한다. Jest가 기본적으로 동일한 파일에서 테스트를 동시에 실행할 때 유용하다. 
  5. 테스트를 시작하기 전에 GraphQL 서버를 시작한다. 
  6. 테스트 컨텍스트에 pre-configured GraphQL 클라이언트를 추가하여 각각의 테스트가 GraphQL 서버로 쉽게 쿼리를 보낼 수 있도록 한다. 
  7. 테스트가 종료되었을 때 GraphQL 서버를 멈춘다. 
  8. GraphQL 클라이언트가 configured 객체를 반환하여 GraphQL 서버로 쿼리를 보낸다. 

이제 mutation을 테스트해보자. 

우선 메모리에 미리 셋팅된 데이터를 제거한다. 

// api/db.ts
...
export const db = {
 posts: []
}

테스트 코드를 다음과 같이 작성한다. 

// tests/Post.test.ts
import { createTestContext } from './__helpers'

const ctx = createTestContext()

it('ensures that a draft can be created and published', async () => {
  // Create a new draft
  const draftResult = await ctx.client.request(`            # 1
    mutation {
      createDraft(title: "Nexus", body: "...") {            # 2
        id
        title
        body
        published
      }
    }
  `)

  // Snapshot that draft and expect `published` to be false
  expect(draftResult).toMatchInlineSnapshot()              // 3
  
  // Publish the previously created draft
  const publishResult = await ctx.client.request(`
    mutation publishDraft($draftId: Int!) {
      publish(draftId: $draftId) {
        id
        title
        body
        published
      }
    }
  `,
    { draftId: draftResult.createDraft.id }
  )
  
  // Snapshot the published draft and expect `published` to be true
  expect(publishResult).toMatchInlineSnapshot()
})
  1. 테스트 컨텍스트는 ctx.client.request에 GraphQL 클라이언트를 노출한다. 이곳에 테스트할 mutation을 작성한다.
  2. 이전에 작성한 createDraft() mutation이다.
  3. 테스트 결과로 스냅샷 인라인으로 입력과 출력이 정렬된 것을 볼 수 있게 해준다. 

Try it out

prettier를 추가하라는 error 메세지가 떠서 추가하였다. 

yarn add --dev prettier

테스트 실행

yarn test

다음과 같이 Object가 .toMatchInlineSnapshot에 추가된다. 

// tests/Post.test.ts
...
  // Snapshot that draft and expect `published` to be false
  expect(draftResult).toMatchInlineSnapshot(`
    Object {
      "createDraft": Object {
        "body": "...",
        "id": 1,
        "published": false,
        "title": "Nexus",
      },
    }
  `); // 3
  
    // Snapshot the published draft and expect `published` to be true
  expect(publishResult).toMatchInlineSnapshot(`
    Object {
      "publish": Object {
        "body": "...",
        "id": 1,
        "published": true,
        "title": "Nexus",
      },
    }
  `);
  
  ...

테스트 결과