Я отправляю PR с новым resolver’ом в GraphQL-схему. Коллега говорит, что смерджил и задеплоил. Но когда я пытаюсь обратиться к новому resolver’у, он недоступен, и я не понимаю почему. Смотрю инфраструктурный (IaC) тест — схема изменилась, но моего resolver’а нет. Начинаю сходить с ума и теряю время.

Именно так могло бы быть, если бы у меня не было тестов.

Проблема тестирования Infrastructure-as-Code

Когда разрабатываешь Infrastructure-as-Code (по крайней мере на AWS — другие платформы не пробовал), часто замечаю, что написанные тесты на самом деле не проверяют то поведение, которое мне важно.

Главная цель тестирования — это проверка поведения системы. Но некоторые тесты вообще не занимаются поведением. Вместо этого они делают снимок исходного кода, фактически фиксируя его и сигнализируя об изменениях в коде. На первый взгляд звучит разумно. Но для меня это недостаток — мы тестируем то, как код выглядит, а не то, что код делает.

Типичный тест схемы

При тестировании Infrastructure-as-Code можно написать вот такой тест для проверки GraphQL-схемы:

describe("GraphQL schema tests", () => {
  const stack = new StackWithSchema();
  const template = Template.fromStack(stack);

  it("should have a GraphQL API schema", () => {
    template.resourceCountIs("AWS::AppSync::GraphQLSchema", 1);

    template.hasResourceProperties("AWS::AppSync::GraphQLSchema", {
      ApiId: { "Fn::GetAtt": [Match.stringLikeRegexp(`GraphQL*`), "ApiId"] },
      Definition: "schema {... }", // Тут внутри схема
    });
  });
});

Но что именно проверяет этот тест? Он проверяет схему в .json-выхлопе из AWS CDK. Он не проверяет схему, задеплоенную на реальном GraphQL-сервере — то есть не проверяет то поведение, которое мне на самом деле нужно.

По сути это snapshot-тест кода, а не тест поведения. Лучше, чем ничего, но в идеале хочется проверять реальный функционал. Я заметил, что интеграционные тесты часто неявно покрывают поведение инфраструктуры — например, если resolver недоступен по API-ключу, ошибка очевидна. Если у Lambda нет доступа к DynamoDB — ошибка уже менее очевидна и нужно копаться в логах.

Тестирование схемы через интроспекцию

К счастью, GraphQL предоставляет встроенный способ тестировать схему через интроспекцию. В GraphQL есть определение схемы как типа:

extend type __Schema {
  description: String
  types: [__Type!]!
  queryType: __Type!
  mutationType: __Type
  subscriptionType: __Type
  directives: [__Directive!]!
}

Меня интересуют resolver’ы и типы, на которые они ссылаются, поэтому я написал такой интеграционный тест:

const introspectOperations = `{
    __schema {
        queryType {
          fields {
            name
          }
        }
        mutationType {
          fields {
            name
          }
        }
        subscriptionType {
          fields {
            name
          }
        }
    }
}`;

const introspectTypes = `{
    __schema {
        types {
          name
          fields {
            name
          }
        }
    }
}`;

describe("Schema tests", () => {
  const byName = (field: any) => field.name;

  function testSchemaFields(schemaFields: any) {
    const sortedSchemaFields = schemaFields.sort(byName);
    expect(sortedSchemaFields).toMatchSnapshot();
  }

  it.each(["query", "mutation", "subscription"])(
    "should verify %s operations",
    async (operation) => {
      const response = await sendGraphQLRequest({
        query: introspectOperations,
      });

      const schema = response.data.__schema;
      expect(schema).toBeTruthy();

      const operationFields = schema[`${operation}Type`]?.fields || [];
      testSchemaFields(operationFields);
    },
  );

  it("should verify types", async () => {
    const response = await sendGraphQLRequest({ query: introspectTypes });

    const schema = response.data.__schema;
    expect(schema).toBeTruthy();

    const typeFields = schema.types;
    testSchemaFields(typeFields);
  });
});

Несколько заметок по коду:

  • Обычно избегаю any, но тут уж можно потерпеть.
  • Проверка expect(schema).toBeTruthy(); необязательна — просто проверяет, что GraphQL-сервер ответил схемой.
  • Реализацию sendGraphQLRequest() опускаю, так как она зависит от инфраструктуры, но внутри обычный HTTP-запрос.
  • Сортировка (schemaFields.sort(byName)) нужна, чтобы тест не падал из-за разного порядка полей схемы.

Как это работает на практике

Каждый раз, когда я деплою новую схему для GraphQL-сервиса (AWS AppSync), я запускаю этот тест в рамках интеграционных тестов. Он дает немедленную обратную связь о поведении задеплоенной схемы.

Вернемся к исходной ситуации. Я добавил новый resolver, коллега сказал, что смерджил и задеплоил изменение. Когда я запустил тест схемы, новый resolver отсутствовал. Мы проверили логи и обнаружили, что он смерджил мой код, но деплоя не было.

Тест дал мне быстрый, однозначный способ показать, что ожидаемое поведение не появилось. Это было не мнение, а фактов — и убедить коллегу оказалось просто. В этом и есть сила хорошего тестирования.