abol-server-testing과 SQLite로 종합 테스트를 해보도록 하겠습니다.

appollo-server를GraphiQL 백엔드 개발 시 종합 테스트(Integration Testing)로 선택하는 방법으로appollo-server-testing과sqlite를 선택했습니다.그 결과 빠른 실행이 가능하고 기존 DB에도 영향을 미치지 않는 방법이 이미 실행되고 있다는 점을 소개한다.

이른바 apol-server-testing


https://www.apollographql.com/docs/apollo-server/testing/testing/
Apollo에서 제공하는 테스트용 도구는 서버의 전체적인 통합 테스트를 더욱 간단하게 하는 도구를 제공합니다.
const { query, mutate } = createTestClient(server);

mutate({
  mutation: UPDATE_USER,
  variables: { id: 1, email: "[email protected]" },
});
createTestClient는 HTTP 서버를 만들지 않고 내부의resolver를 실행할 수 있는 방법이다.이를 기점으로 Resolver에서 보는 종합 테스트를 진행할 수 있다.

샘플 항목


여기 창고에 Push 샘플이 있으니 clone에서 시험해 보세요.
git clone https://github.com/suzukalight/sample-apollo-server-testing-integration-test
cd sample-apollo-server-testing-integration-test
yarn
다음과 같은 기술을 사용한다.
  • Clean Architecture
  • Node.js + TypeScript

  • Apollo Server w/Express + apollo-server-testing

  • TypeORM w/SQLite
  • GraphQL Code Generator
  • Jest w/ts-jest
  • 시험의 필요조건과 대상


    deleteUser mutation 고려


    mutation DeleteUser($input: DeleteUserRequest) {
      deleteUser(input: $input) {
        user {
          id
          email
        }
      }
    }
    
    사용자를 삭제하는 mutation deleteUser를 만들고 종합적인 테스트를 고려합니다.

    테스트 요구 사항

  • Admin 사용자 로그인 시 모든 사용자에 대한 삭제 성공
  • Member 사용자가 로그인했을 때 자신만 삭제
  • 비로그인 시 오류 반환
  • 존재하지 않는 ID에 대한 삭제 작업에 오류가 발생했습니다
  • .
  • 잘못된 매개 변수를 지정한 경우 오류
  • 를 반환합니다.
    등, 이번에는 통합 테스트에서 확인할 예정이다.
    실제로 이러한 검사를 요구하는 대부분은usecase의 단원 테스트를 통해 실현할 수 있다.종합테스트에서'부품의 결합이 정확한지'를 확인해야 하기 때문에 종합테스트에서degray와 인터페이스가 일치하지 않는지 확인하는 등 단원테스트를 두껍게 하면 피라미드에서도 더욱 좋다

    통일 테스트의 실현


    구체적인 실복에 들어가다.설치의 큰 절차로서
  • sqlite를 통해 테스트용 DB 자동 생성
  • apol-server-testing을 통해 통합 테스트에 사용되는GraphiQL 클라이언트 생성
  • GraphiQL 클라이언트를 통한 Jest 병렬 테스트
  • 특히 각 테스트마다 테스트 DB가 자동으로 생성됩니다.이로써 테스트는 서로 희소해져 부작용 없이 병행할 수 있다.

    매크로 패키지 설치


    yarn add -D apollo-server-testing
    

    각 테스트마다 별도의 DB 생성


    test/integration/setup/database.ts
    import { Connection, createConnection } from "typeorm";
    import { v4 as uuidv4 } from "uuid";
    import fs from "fs";
    
    // ランダムなDB名を生成
    export const getRandomDbPath = () => `./test_db/${uuidv4()}.sqlite`;
    
    // 各テストごとに独立したDBを作成し、テストの独立性を担保する
    export const createDbConnection = async (randomDbPath: string) =>
      createConnection({
        type: "sqlite",
        name: randomDbPath,
        database: randomDbPath,
        entities: [User],
        synchronize: true,
      });
    
    // DBファイルを削除
    export const deleteDbFile = (dbPath: string) => {
      fs.unlinkSync(dbPath);
    };
    
    실제 환경의 DB를 테스트에 활용할 수 없어 SQLite로 DB를 이동하기로 했다.TypeORM에서는 설정type: 'sqlite', database: filepath, synchronize: true을 통해 DB에 연결할 때 자동으로 DB 파일을 생성하고 모드 동기화를 진행한다.
    이 경우 각 테스트가 describe 단위로 병행되므로 DB 공유가 다른 테스트 결과를 오염시킬 수 있습니다.그래서 우리는 하나의 데이터베이스 구조를 테스트하는 것을 제정했다.
    SQLite의 경우 파일 기반 또는 스토리지 기반 기반 DB를 빠르게 만들 수 있기 때문에 통합 테스트처럼 반복적으로 수행하는 데 유리하다.메모리 기반이 없는 이유는 CI에서 실행할 때 용기의 메모리를 깨물어 의외의 실패가 발생하지 않도록 하기 때문이다.

    테스트 서버 시작


    test/integration/setup/apollo-server.ts
    import {
      createTestClient,
      ApolloServerTestClient,
    } from "apollo-server-testing";
    // ...
    
    export const createApolloServerForTesting = (
      dbConnection: Connection,
      actor?: UserDto
    ): ApolloServerTestClient => {
      // graphql-codegen でバンドルしたスキーマファイルを使用
      const schema = loadSchemaSync(
        path.join(__dirname, "../../../src/schema/schema.graphql"),
        {
          loaders: [new GraphQLFileLoader()],
        }
      );
    
      // resolverをスキーマと連結
      const schemaWithResolvers = addResolversToSchema({
        schema,
        resolvers,
      });
    
      // ApolloServerでGraphQLサーバを起動
      const server = new ApolloServer({
        schema: schemaWithResolvers,
        context: () => getContext(dbConnection, actor),
      });
    
      // テスト用のGraphQLクライアントを生成
      const testClient = createTestClient(server);
      return testClient;
    };
    
    Apollo Server의 실례를 만들고 apolo-server-testingcreateTestClient에 제출하면 테스트용 클라이언트를 얻을 수 있습니다.이 고객에서query와mutate를 호출하여 종합 테스트를 진행합니다.
    또한 Apollo Server의 인스턴스는 Express를 설정할 필요가 없습니다.이 종합 테스트는 HTTP 클라이언트를 사용하지 않고 테스트 클라이언트에서 Resolver를 직접 호출할 수 있습니다.

    각 테스트에 대한 서버 및 DB 생성


    describe("deleteUser", () => {
      describe("Admin", () => {
        const actor = {
          id: "1",
          email: "[email protected]",
          roles: [RoleTypes.Admin],
        }; // Adminロールのactor
        const randomDbPath = getRandomDbPath(); // テストごとに固有のファイルを作成
        let dbConnection: Connection;
        let testClient: ApolloServerTestClient | undefined;
    
        beforeAll(async () => {
          dbConnection = await createDbConnection(randomDbPath); // DBの作成とマイグレーション
          await seedAll(dbConnection); // UserをDBに流し込む
          testClient = createApolloServerForTesting(dbConnection, actor); // Adminをactorとしてサーバを起動
        });
    
        afterAll(async () => {
          await dbConnection.close();
          deleteDbFile(randomDbPath); // DBファイルを削除し、テストごとにDBを破棄
        });
    
        // ...
      });
    });
    
    deleteUser 테스트에서 actor 전환을 위한 2가지 테스트를 준비했다.actor가 Admin 역할이면 모든 사용자를 삭제할 수 있고, Member 역할이면 자신만 삭제할 수 있습니다.
    각 테스트 그룹이 초기화될 때 테스트 클라이언트와 DB를 생성하고 끝날 때 폐기합니다.모든 actor에 DB를 만들면 테스트 방안의 영향 범위를 국부적으로 설정하여 병행 실행에 편리하게 할 수 있습니다.

    {query,mutate} 병합 테스트 사용하기


    test("OK: Adminロールで、エンティティの削除ができた", async () => {
      const result = await testClient?.mutate({
        mutation: DELETE_USER,
        variables: {
          input: {
            id: "3",
          },
        },
      });
    
      const { user } = result?.data?.deleteUser ?? {};
      expect(user?.email).toBe("[email protected]"); // 削除したユーザの情報が返ってくる
    });
    
    test("NG: 存在しないIDを指定した", async () => {
      const result = await testClient?.mutate({
        mutation: DELETE_USER,
        variables: {
          input: {
            id: "99999",
          },
        },
      });
    
      const { data, errors } = result ?? {};
      expect(data?.deleteUser).toBeNull(); // dataはnullが返ってくる
      expect(errors?.length).toBeGreaterThan(0); // errorsにエラー内容が含まれている
    });
    
    각 테스트에서 mutate 방법으로 deleteUser의 Resolver를 실행하고 assert 형식으로 기대에 부합되는지 회답합니다.

    실행


    $ yarn test
    yarn run v1.22.4
    $ env-cmd -f .env.default jest
     PASS  src/entity/user/__tests__/index.ts
     PASS  src/policy/decision/__tests__/common.ts
     PASS  src/entity/common/Password/__tests__/encrypt.ts
     PASS  src/entity/common/Email/__tests__/index.ts
     PASS  src/entity/common/Password/__tests__/entity.ts
     PASS  test/integration/User/__tests__/deleteUser.ts
    
    Test Suites: 6 passed, 6 total
    Tests:       59 passed, 59 total
    Snapshots:   0 total
    Time:        4.087 s
    Ran all test suites.
    ✨  Done in 4.85s.
    
    PASS가 정상입니다.
    수중 환경에서 2종 5종의 종합테스트를 수행하는 데 걸리는 시간은 4~7초다.Google TestSize에서 Meduim 사이즈 테스트에 걸리는 시간은 최대 300초로 매우 짧은 시간이라고 할 수 있습니다.

    끝말


    apolo-server-testing과 sqlite를 통해 Resolver의 종합 테스트를 신속하게 진행할 수 있으며 기존 환경에 영향을 주지 않습니다.단원 테스트와 E2E 테스트 사이의 중요한 테스트를 쉽게 구축할 수 있는 메커니즘이 있기 어렵다.
    이번 종합 테스트는 Admin 스크롤 막대만 있는 경우 사용자를 삭제할 수 없다는 문제를 없앴다.종합테스트의 결과인가... 조금 미묘하지만 쓰기 테스트를 통해 BUG 상태에서 발표하지 않아도 된다.
    전제적으로 피라미드 아래의 단원 테스트를 두껍게 하고 고속으로 수행할 수 있는 종합 테스트가 더 많으면 E2E 테스트와 수동 테스트에 의존하지 않아도 품질 유지 능력을 높여야 한다.합병 테스트도 계속 개량하고 싶다.이렇게 하면degray가 두렵지 않아요!대략!

    보너스


    아래 창고에는 종합테스트를 진행하는 항목이 더 공개됐으니 함께 보시기 바랍니다.
    https://github.com/suzukalight/clean-architecture-nodejs-graphql-codegen

    좋은 웹페이지 즐겨찾기