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
typescript
1import gql from 'graphql-tag';
2import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type';
3import { type CreateMyEntityInput } from 'src/engine/metadata-modules/my-entity/dtos/create-my-entity.input';
4
5export type CreateMyEntityFactoryInput = CreateMyEntityInput;
6
7const DEFAULT_MY_ENTITY_GQL_FIELDS = `
8 id
9 name
10 label
11 description
12 isCustom
13 createdAt
14 updatedAt
15`;
16
17export const createMyEntityQueryFactory = ({
18 input,
19 gqlFields = DEFAULT_MY_ENTITY_GQL_FIELDS,
20}: PerformMetadataQueryParams<CreateMyEntityFactoryInput>) => ({
21 query: gql`
22 mutation CreateMyEntity($input: CreateMyEntityInput!) {
23 createMyEntity(input: $input) {
24 ${gqlFields}
25 }
26 }
27 `,
28 variables: {
29 input,
30 },
31});
Pattern: Wrapper Utility
File: test/integration/metadata/suites/my-entity/utils/create-my-entity.util.ts
typescript
1import {
2 type CreateMyEntityFactoryInput,
3 createMyEntityQueryFactory,
4} from 'test/integration/metadata/suites/my-entity/utils/create-my-entity-query-factory.util';
5import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util';
6import { type CommonResponseBody } from 'test/integration/metadata/types/common-response-body.type';
7import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type';
8import { warnIfErrorButNotExpectedToFail } from 'test/integration/metadata/utils/warn-if-error-but-not-expected-to-fail.util';
9import { warnIfNoErrorButExpectedToFail } from 'test/integration/metadata/utils/warn-if-no-error-but-expected-to-fail.util';
10import { type MyEntityDto } from 'src/engine/metadata-modules/my-entity/dtos/my-entity.dto';
11
12export const createMyEntity = async ({
13 input,
14 gqlFields,
15 expectToFail = false,
16 token,
17}: PerformMetadataQueryParams<CreateMyEntityFactoryInput>): CommonResponseBody<{
18 createMyEntity: MyEntityDto;
19}> => {
20 const graphqlOperation = createMyEntityQueryFactory({
21 input,
22 gqlFields,
23 });
24
25 const response = await makeMetadataAPIRequest(graphqlOperation, token);
26
27 if (expectToFail === true) {
28 warnIfNoErrorButExpectedToFail({
29 response,
30 errorMessage: 'My entity creation should have failed but did not',
31 });
32 }
33
34 if (expectToFail === false) {
35 warnIfErrorButNotExpectedToFail({
36 response,
37 errorMessage: 'My entity creation has failed but should not',
38 });
39 }
40
41 return { data: response.body.data, errors: response.body.errors };
42};
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
typescript
1import { expectOneNotInternalServerErrorSnapshot } from 'test/integration/graphql/utils/expect-one-not-internal-server-error-snapshot.util';
2import { createMyEntity } from 'test/integration/metadata/suites/my-entity/utils/create-my-entity.util';
3import { deleteMyEntity } from 'test/integration/metadata/suites/my-entity/utils/delete-my-entity.util';
4import {
5 eachTestingContextFilter,
6 type EachTestingContext,
7} from 'twenty-shared/testing';
8import { isDefined } from 'twenty-shared/utils';
9import { type CreateMyEntityInput } from 'src/engine/metadata-modules/my-entity/dtos/create-my-entity.input';
10
11type TestContext = {
12 input: CreateMyEntityInput;
13};
14
15type GlobalTestContext = {
16 existingEntityLabel: string;
17 existingEntityName: string;
18};
19
20const globalTestContext: GlobalTestContext = {
21 existingEntityLabel: 'Existing Test Entity',
22 existingEntityName: 'existingTestEntity',
23};
24
25type CreateMyEntityTestingContext = EachTestingContext<TestContext>[];
26
27describe('My entity creation should fail', () => {
28 let existingEntityId: string | undefined;
29
30 beforeAll(async () => {
31 // Setup: Create entity for uniqueness tests
32 const { data } = await createMyEntity({
33 expectToFail: false,
34 input: {
35 name: globalTestContext.existingEntityName,
36 label: globalTestContext.existingEntityLabel,
37 },
38 });
39
40 existingEntityId = data.createMyEntity.id;
41 });
42
43 afterAll(async () => {
44 // Cleanup
45 if (isDefined(existingEntityId)) {
46 await deleteMyEntity({
47 expectToFail: false,
48 input: { id: existingEntityId },
49 });
50 }
51 });
52
53 const failingMyEntityCreationTestCases: CreateMyEntityTestingContext = [
54 // Input transpilation validation
55 {
56 title: 'when name is missing',
57 context: {
58 input: {
59 label: 'Entity Missing Name',
60 } as CreateMyEntityInput,
61 },
62 },
63 {
64 title: 'when label is missing',
65 context: {
66 input: {
67 name: 'entityMissingLabel',
68 } as CreateMyEntityInput,
69 },
70 },
71 {
72 title: 'when name is empty string',
73 context: {
74 input: {
75 name: '',
76 label: 'Empty Name Entity',
77 },
78 },
79 },
80
81 // Validator business logic
82 {
83 title: 'when name already exists (uniqueness)',
84 context: {
85 input: {
86 name: globalTestContext.existingEntityName,
87 label: 'Duplicate Name Entity',
88 },
89 },
90 },
91 {
92 title: 'when trying to create standard entity',
93 context: {
94 input: {
95 name: 'myEntity',
96 label: 'Standard Entity',
97 isCustom: false,
98 } as CreateMyEntityInput,
99 },
100 },
101
102 // Foreign key validation
103 {
104 title: 'when parentEntityId does not exist',
105 context: {
106 input: {
107 name: 'invalidParentEntity',
108 label: 'Invalid Parent Entity',
109 parentEntityId: '00000000-0000-0000-0000-000000000000',
110 },
111 },
112 },
113 ];
114
115 it.each(eachTestingContextFilter(failingMyEntityCreationTestCases))(
116 '$title',
117 async ({ context }) => {
118 const { errors } = await createMyEntity({
119 expectToFail: true,
120 input: context.input,
121 });
122
123 expectOneNotInternalServerErrorSnapshot({
124 errors,
125 });
126 },
127 );
128});
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
typescript
1import { createMyEntity } from 'test/integration/metadata/suites/my-entity/utils/create-my-entity.util';
2import { deleteMyEntity } from 'test/integration/metadata/suites/my-entity/utils/delete-my-entity.util';
3import { type CreateMyEntityInput } from 'src/engine/metadata-modules/my-entity/dtos/create-my-entity.input';
4
5describe('My entity creation should succeed', () => {
6 let createdEntityId: string;
7
8 afterEach(async () => {
9 if (createdEntityId) {
10 await deleteMyEntity({
11 expectToFail: false,
12 input: { id: createdEntityId },
13 });
14 }
15 });
16
17 it('should create entity with minimal required input', async () => {
18 const { data } = await createMyEntity({
19 expectToFail: false,
20 input: {
21 name: 'minimalEntity',
22 label: 'Minimal Entity',
23 },
24 });
25
26 createdEntityId = data?.createMyEntity?.id;
27
28 expect(data.createMyEntity).toMatchObject({
29 id: expect.any(String),
30 name: 'minimalEntity',
31 label: 'Minimal Entity',
32 description: null,
33 isCustom: true,
34 createdAt: expect.any(String),
35 updatedAt: expect.any(String),
36 });
37 });
38
39 it('should create entity with all optional fields', async () => {
40 const input = {
41 name: 'fullEntity',
42 label: 'Full Entity',
43 description: 'Entity with all fields specified',
44 } as const satisfies CreateMyEntityInput;
45
46 const { data } = await createMyEntity({
47 expectToFail: false,
48 input,
49 });
50
51 createdEntityId = data?.createMyEntity?.id;
52
53 expect(data.createMyEntity).toMatchObject({
54 id: expect.any(String),
55 name: 'fullEntity',
56 label: 'Full Entity',
57 description: 'Entity with all fields specified',
58 isCustom: true,
59 });
60 });
61
62 it('should sanitize input by trimming whitespace', async () => {
63 const { data } = await createMyEntity({
64 expectToFail: false,
65 input: {
66 name: ' entityWithSpaces ',
67 label: ' Entity With Spaces ',
68 description: ' Description with spaces ',
69 },
70 });
71
72 createdEntityId = data?.createMyEntity?.id;
73
74 expect(data.createMyEntity).toMatchObject({
75 id: expect.any(String),
76 name: 'entityWithSpaces',
77 label: 'Entity With Spaces',
78 description: 'Description with spaces',
79 });
80 });
81
82 it('should handle long text content', async () => {
83 const longDescription = 'A'.repeat(1000);
84
85 const { data } = await createMyEntity({
86 expectToFail: false,
87 input: {
88 name: 'longDescEntity',
89 label: 'Long Description Entity',
90 description: longDescription,
91 },
92 });
93
94 createdEntityId = data?.createMyEntity?.id;
95
96 expect(data.createMyEntity).toMatchObject({
97 id: expect.any(String),
98 description: longDescription,
99 });
100 });
101});
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
typescript
1afterEach(async () => {
2 if (createdEntityId) {
3 await deleteMyEntity({
4 expectToFail: false,
5 input: { id: createdEntityId },
6 });
7 }
8});
typescript
1const input = {
2 name: 'myEntity',
3 label: 'My Entity',
4} as const satisfies CreateMyEntityInput;
Pattern: Snapshot Testing
typescript
1expectOneNotInternalServerErrorSnapshot({
2 errors,
3});
Running Tests
bash
1# Run all entity tests
2npx jest test/integration/metadata/suites/my-entity --config=packages/twenty-server/jest.config.mjs
3
4# Run specific test file
5npx jest test/integration/metadata/suites/my-entity/failing-my-entity-creation.integration-spec.ts --config=packages/twenty-server/jest.config.mjs
6
7# Update snapshots
8npx 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.