| name | syncable-entity-testing |
| description | Create comprehensive integration tests for syncable entities in Twenty. Use when writing integration tests for metadata entities, covering validator exceptions, input transpilation errors, and CRUD operations. Tests are MANDATORY for all syncable entities. |
Syncable Entity: Integration Testing (Step 6/6 - MANDATORY)
Purpose: Create comprehensive test suite covering all validation scenarios, input transpilation exceptions, and successful use cases.
When to use: After completing Steps 1-5. Integration tests are REQUIRED for all syncable entities.
Quick Start
Tests must cover:
- Failing scenarios - All validator exceptions and input transpilation errors
- Successful scenarios - All CRUD operations and edge cases
- Test utilities - Reusable query factories and helper functions
Test pattern: Two-file pattern (query factory + wrapper) for each operation.
Step 1: Create Test Utilities
Pattern: Query Factory
File: 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,
},
});
Pattern: Wrapper Utility
File: 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 = (graphqlOperation, token);
(expectToFail === ) {
({
response,
: ,
});
}
(expectToFail === ) {
({
response,
: ,
});
}
{ : response.., : response.. };
};
Required utilities (follow same pattern):
update-my-entity-query-factory.util.ts + update-my-entity.util.ts
delete-my-entity-query-factory.util.ts + delete-my-entity.util.ts
Step 2: Failing Creation Tests
File: 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', {
: | ;
( () => {
{ data } = ({
: ,
: {
: globalTestContext.,
: globalTestContext.,
},
});
existingEntityId = data..;
});
( () => {
((existingEntityId)) {
({
: ,
: { : existingEntityId },
});
}
});
: = [
{
: ,
: {
: {
: ,
} ,
},
},
{
: ,
: {
: {
: ,
} ,
},
},
{
: ,
: {
: {
: ,
: ,
},
},
},
{
: ,
: {
: {
: globalTestContext.,
: ,
},
},
},
{
: ,
: {
: {
: ,
: ,
: ,
} ,
},
},
{
: ,
: {
: {
: ,
: ,
: ,
},
},
},
];
it.((failingMyEntityCreationTestCases))(
,
({ context }) => {
{ errors } = ({
: ,
: context.,
});
({
errors,
});
},
);
});
Test coverage requirements:
- ✅ Missing required fields
- ✅ Empty strings
- ✅ Invalid format
- ✅ Uniqueness violations
- ✅ Standard entity protection
- ✅ Foreign key validation
Step 3: Successful Creation Tests
File: 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(),
: ,
: ,
: ,
: ,
: expect.(),
: expect.(),
});
});
(, () => {
input = {
: ,
: ,
: ,
} ;
{ data } = ({
: ,
input,
});
createdEntityId = data?.?.;
(data.).({
: expect.(),
: ,
: ,
: ,
: ,
});
});
(, () => {
{ data } = ({
: ,
: {
: ,
: ,
: ,
},
});
createdEntityId = data?.?.;
(data.).({
: expect.(),
: ,
: ,
: ,
});
});
(, () => {
longDescription = .();
{ data } = ({
: ,
: {
: ,
: ,
: longDescription,
},
});
createdEntityId = data?.?.;
(data.).({
: expect.(),
: longDescription,
});
});
});
Test coverage requirements:
- ✅ Minimal required input
- ✅ All optional fields
- ✅ Input sanitization
- ✅ Long text content
- ✅ Special characters
Step 4: Update and Delete Tests
Create similar test files for update and delete operations:
Required files:
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
Testing Best Practices
Pattern: Cleanup
afterEach(async () => {
if (createdEntityId) {
await deleteMyEntity({
expectToFail: false,
input: { id: createdEntityId },
});
}
});
Pattern: Type-Safe Inputs
const input = {
name: 'myEntity',
label: 'My Entity',
} as const satisfies CreateMyEntityInput;
Pattern: Snapshot Testing
expectOneNotInternalServerErrorSnapshot({
errors,
});
Running Tests
npx jest test/integration/metadata/suites/my-entity --config=packages/twenty-server/jest.config.mjs
npx jest test/integration/metadata/suites/my-entity/failing-my-entity-creation.integration-spec.ts --config=packages/twenty-server/jest.config.mjs
npx jest test/integration/metadata/suites/my-entity --updateSnapshot --config=packages/twenty-server/jest.config.mjs
Complete Test Checklist
Test Utilities
Failing Tests Coverage
Successful Tests Coverage
Snapshot Tests
Success Criteria
Your integration tests are complete when:
✅ All test utilities created (minimum 6 files)
✅ Failing creation tests cover all validators
✅ Failing update tests cover business rules
✅ Failing deletion tests cover protection rules
✅ Successful tests cover all use cases
✅ All snapshots generated and committed
✅ All tests pass consistently
✅ Test coverage meets requirements (>80%)
Final Step
✅ Step 6 Complete! → Your syncable entity is fully tested and production-ready!
Congratulations! You've successfully created a new syncable entity in Twenty's workspace migration system.
For complete workflow, see @creating-syncable-entity rule.