mcpField
S
syncable-entity-testing

syncable-entity-testing

github.com/twentyhq测试与质量60.0k54 次安装今天更新
代码仓库

关于 syncable-entity-testing

用于 Twenty 可同步实体的集成测试技能,指导构建查询工具,并覆盖验证失败、CRUD 操作和边缘情况,适用于实现完成后的最终测试步骤。

主要功能

  1. 01可复用的查询与封装工具
  2. 02验证失败及错误场景测试
  3. 03成功的 CRUD 场景
  4. 04基于快照的错误检查
  5. 05测试清理模式

适用场景

  1. 01测试新实体的验证失败情况
  2. 02验证创建、更新和删除行为

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/twentyhq/twenty --skill syncable-entity-testing

安装后在 Claude Code 中输入/syncable-entity-testing确认已加载。

详细介绍

可同步实体:集成测试(第 6/6 步 - 强制执行)

目的:创建全面的测试套件,覆盖所有校验场景、输入转换异常以及成功用例。

使用时机:在完成第 1-5 步之后。集成测试对所有可同步实体都是必需的。


快速开始

测试必须覆盖:

  1. 失败场景 - 所有校验器异常和输入转换错误
  2. 成功场景 - 所有 CRUD 操作和边界情况
  3. 测试工具 - 可复用的查询工厂和辅助函数

测试模式:每种操作采用双文件模式(查询工厂 + 封装器)。


第 1 步:创建测试工具

模式:查询工厂

文件:test/integration/metadata/suites/my-entity/utils/create-my-entity-query-factory.util.ts

import gql from 'graphql-tag';
import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type';
import { type CreateMyEntityInput } from 'src/engine/metadata-modules/my-entity/dtos/create-my-entity.input';

export type CreateMyEntityFactoryInput = CreateMyEntityInput;

const DEFAULT_MY_ENTITY_GQL_FIELDS = `
  id
  name
  label
  description
  isCustom
  createdAt
  updatedAt
`;

export const createMyEntityQueryFactory = ({
  input,
  gqlFields = DEFAULT_MY_ENTITY_GQL_FIELDS,
}: PerformMetadataQueryParams<CreateMyEntityFactoryInput>) => ({
  query: gql`
    mutation CreateMyEntity($input: CreateMyEntityInput!) {
      createMyEntity(input: $input) {
        ${gqlFields}
      }
    }
  `,
  variables: {
    input,
  },
});
模式:封装器工具

文件:test/integration/metadata/suites/my-entity/utils/create-my-entity.util.ts

import {
  type CreateMyEntityFactoryInput,
  createMyEntityQueryFactory,
} from 'test/integration/metadata/suites/my-entity/utils/create-my-entity-query-factory.util';
import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util';
import { type CommonResponseBody } from 'test/integration/metadata/types/common-response-body.type';
import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type';
import { warnIfErrorButNotExpectedToFail } from 'test/integration/metadata/utils/warn-if-error-but-not-expected-to-fail.util';
import { warnIfNoErrorButExpectedToFail } from 'test/integration/metadata/utils/warn-if-no-error-but-expected-to-fail.util';
import { type MyEntityDto } from 'src/engine/metadata-modules/my-entity/dtos/my-entity.dto';

export const createMyEntity = async ({
  input,
  gqlFields,
  expectToFail = false,
  token,
}: PerformMetadataQueryParams<CreateMyEntityFactoryInput>): CommonResponseBody<{
  createMyEntity: MyEntityDto;
}> => {
  const graphqlOperation = createMyEntityQueryFactory({
    input,
    gqlFields,
  });

  const response = await makeMetadataAPIRequest(graphqlOperation, token);

  if (expectToFail === true) {
    warnIfNoErrorButExpectedToFail({
      response,
      errorMessage: 'My entity creation should have failed but did not',
    });
  }

  if (expectToFail === false) {
    warnIfErrorButNotExpectedToFail({
      response,
      errorMessage: 'My entity creation has failed but should not',
    });
  }

  return { data: response.body.data, errors: response.body.errors };
};

必需的工具(遵循相同模式):

  • update-my-entity-query-factory.util.ts + update-my-entity.util.ts
  • delete-my-entity-query-factory.util.ts + delete-my-entity.util.ts

第 2 步:失败的创建测试

文件:test/integration/metadata/suites/my-entity/failing-my-entity-creation.integration-spec.ts

import { expectOneNotInternalServerErrorSnapshot } from 'test/integration/graphql/utils/expect-one-not-internal-server-error-snapshot.util';
import { createMyEntity } from 'test/integration/metadata/suites/my-entity/utils/create-my-entity.util';
import { deleteMyEntity } from 'test/integration/metadata/suites/my-entity/utils/delete-my-entity.util';
import {
  eachTestingContextFilter,
  type EachTestingContext,
} from 'twenty-shared/testing';
import { isDefined } from 'twenty-shared/utils';
import { type CreateMyEntityInput } from 'src/engine/metadata-modules/my-entity/dtos/create-my-entity.input';

type TestContext = {
  input: CreateMyEntityInput;
};

type GlobalTestContext = {
  existingEntityLabel: string;
  existingEntityName: string;
};

const globalTestContext: GlobalTestContext = {
  existingEntityLabel: 'Existing Test Entity',
  existingEntityName: 'existingTestEntity',
};

type CreateMyEntityTestingContext = EachTestingContext<TestContext>[];

describe('My entity creation should fail', () => {
  let existingEntityId: string | undefined;

  beforeAll(async () => {
    // Setup: Create entity for uniqueness tests
    const { data } = await createMyEntity({
      expectToFail: false,
      input: {
        name: globalTestContext.existingEntityName,
        label: globalTestContext.existingEntityLabel,
      },
    });

    existingEntityId = data.createMyEntity.id;
  });

  afterAll(async () => {
    // Cleanup
    if (isDefined(existingEntityId)) {
      await deleteMyEntity({
        expectToFail: false,
        input: { id: existingEntityId },
      });
    }
  });

  const failingMyEntityCreationTestCases: CreateMyEntityTestingContext = [
    // Input transpilation validation
    {
      title: 'when name is missing',
      context: {
        input: {
          label: 'Entity Missing Name',
        } as CreateMyEntityInput,
      },
    },
    {
      title: 'when label is missing',
      context: {
        input: {
          name: 'entityMissingLabel',
        } as CreateMyEntityInput,
      },
    },
    {
      title: 'when name is empty string',
      context: {
        input: {
          name: '',
          label: 'Empty Name Entity',
        },
      },
    },

    // Validator business logic
    {
      title: 'when name already exists (uniqueness)',
      context: {
        input: {
          name: globalTestContext.existingEntityName,
          label: 'Duplicate Name Entity',
        },
      },
    },
    {
      title: 'when trying to create standard entity',
      context: {
        input: {
          name: 'myEntity',
          label: 'Standard Entity',
          isCustom: false,
        } as CreateMyEntityInput,
      },
    },

    // Foreign key validation
    {
      title: 'when parentEntityId does not exist',
      context: {
        input: {
          name: 'invalidParentEntity',
          label: 'Invalid Parent Entity',
          parentEntityId: '00000000-0000-0000-0000-000000000000',
        },
      },
    },
  ];

  it.each(eachTestingContextFilter(failingMyEntityCreationTestCases))(
    '$title',
    async ({ context }) => {
      const { errors } = await createMyEntity({
        expectToFail: true,
        input: context.input,
      });

      expectOneNotInternalServerErrorSnapshot({
        errors,
      });
    },
  );
});

测试覆盖要求:

  • ✅ 缺少必填字段
  • ✅ 空字符串
  • ✅ 格式无效
  • ✅ 唯一性冲突
  • ✅ 标准实体保护
  • ✅ 外键校验

第 3 步:成功的创建测试

文件:test/integration/metadata/suites/my-entity/successful-my-entity-creation.integration-spec.ts

import { createMyEntity } from 'test/integration/metadata/suites/my-entity/utils/create-my-entity.util';
import { deleteMyEntity } from 'test/integration/metadata/suites/my-entity/utils/delete-my-entity.util';
import { type CreateMyEntityInput } from 'src/engine/metadata-modules/my-entity/dtos/create-my-entity.input';

describe('My entity creation should succeed', () => {
  let createdEntityId: string;

  afterEach(async () => {
    if (createdEntityId) {
      await deleteMyEntity({
        expectToFail: false,
        input: { id: createdEntityId },
      });
    }
  });

  it('should create entity with minimal required input', async () => {
    const { data } = await createMyEntity({
      expectToFail: false,
      input: {
        name: 'minimalEntity',
        label: 'Minimal Entity',
      },
    });

    createdEntityId = data?.createMyEntity?.id;

    expect(data.createMyEntity).toMatchObject({
      id: expect.any(String),
      name: 'minimalEntity',
      label: 'Minimal Entity',
      description: null,
      isCustom: true,
      createdAt: expect.any(String),
      updatedAt: expect.any(String),
    });
  });

  it('should create entity with all optional fields', async () => {
    const input = {
      name: 'fullEntity',
      label: 'Full Entity',
      description: 'Entity with all fields specified',
    } as const satisfies CreateMyEntityInput;

    const { data } = await createMyEntity({
      expectToFail: false,
      input,
    });

    createdEntityId = data?.createMyEntity?.id;

    expect(data.createMyEntity).toMatchObject({
      id: expect.any(String),
      name: 'fullEntity',
      label: 'Full Entity',
      description: 'Entity with all fields specified',
      isCustom: true,
    });
  });

  it('should sanitize input by trimming whitespace', async () => {
    const { data } = await createMyEntity({
      expectToFail: false,
      input: {
        name: '  entityWithSpaces  ',
        label: '  Entity With Spaces  ',
        description: '  Description with spaces  ',
      },
    });

    createdEntityId = data?.createMyEntity?.id;

    expect(data.createMyEntity).toMatchObject({
      id: expect.any(String),
      name: 'entityWithSpaces',
      label: 'Entity With Spaces',
      description: 'Description with spaces',
    });
  });

  it('should handle long text content', async () => {
    const longDescription = 'A'.repeat(1000);

    const { data } = await createMyEntity({
      expectToFail: false,
      input: {
        name: 'longDescEntity',
        label: 'Long Description Entity',
        description: longDescription,
      },
    });

    createdEntityId = data?.createMyEntity?.id;

    expect(data.createMyEntity).toMatchObject({
      id: expect.any(String),
      description: longDescription,
    });
  });
});

测试覆盖要求:

  • ✅ 最小必填输入
  • ✅ 所有可选字段
  • ✅ 输入清理
  • ✅ 长文本内容
  • ✅ 特殊字符

第 4 步:更新和删除测试

为更新和删除操作创建类似的测试文件:

必需的文件:

  • failing-my-entity-update.integration-spec.ts
  • successful-my-entity-update.integration-spec.ts
  • failing-my-entity-deletion.integration-spec.ts
  • successful-my-entity-deletion.integration-spec.ts

测试最佳实践

模式:清理
afterEach(async () => {
  if (createdEntityId) {
    await deleteMyEntity({
      expectToFail: false,
      input: { id: createdEntityId },
    });
  }
});
模式:类型安全输入
const input = {
  name: 'myEntity',
  label: 'My Entity',
} as const satisfies CreateMyEntityInput;
模式:快照测试
expectOneNotInternalServerErrorSnapshot({
  errors,
});

运行测试

# Run all entity tests
npx jest test/integration/metadata/suites/my-entity --config=packages/twenty-server/jest.config.mjs

# Run specific test file
npx jest test/integration/metadata/suites/my-entity/failing-my-entity-creation.integration-spec.ts --config=packages/twenty-server/jest.config.mjs

# Update snapshots
npx jest test/integration/metadata/suites/my-entity --updateSnapshot --config=packages/twenty-server/jest.config.mjs

完整测试清单

测试工具
  • 已创建 create-my-entity-query-factory.util.ts
  • 已创建 create-my-entity.util.ts
  • 已创建 update-my-entity-query-factory.util.ts
  • 已创建 update-my-entity.util.ts
  • 已创建 delete-my-entity-query-factory.util.ts
  • 已创建 delete-my-entity.util.ts
失败测试覆盖
  • 缺少必填字段
  • 空字符串校验
  • 唯一性冲突
  • 标准实体保护
  • 外键校验
  • JSONB 属性校验(如适用)
成功测试覆盖
  • 使用最小输入创建
  • 使用所有可选字段创建
  • 输入清理(空白字符)
  • 长文本内容
  • 更新单个字段
  • 更新多个字段
  • 成功删除
快照测试
  • 所有失败测试都使用 expectOneNotInternalServerErrorSnapshot
  • 快照已提交到 __snapshots__/ 目录

成功标准

当满足以下条件时,你的集成测试即告完成:

✅ 已创建所有测试工具(至少 6 个文件) ✅ 失败的创建测试覆盖所有校验器 ✅ 失败的更新测试覆盖业务规则 ✅ 失败的删除测试覆盖保护规则 ✅ 成功的测试覆盖所有用例 ✅ 所有快照均已生成并提交 ✅ 所有测试稳定通过 ✅ 测试覆盖率满足要求(>80%)


最后一步

✅ 第 6 步完成! → 你的可同步实体已通过完整测试,可用于生产环境!

恭喜! 你已成功在 Twenty 的工作区迁移系统中创建了一个新的可同步实体。

完整工作流请参阅 @creating-syncable-entity 规则。

评论

支持 Markdown