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
typescript1import 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
typescript1import { 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.tsdelete-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
typescript1import { 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
typescript1import { 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.tssuccessful-my-entity-update.integration-spec.tsfailing-my-entity-deletion.integration-spec.tssuccessful-my-entity-deletion.integration-spec.ts
Testing Best Practices
Pattern: Cleanup
typescript1afterEach(async () => { 2 if (createdEntityId) { 3 await deleteMyEntity({ 4 expectToFail: false, 5 input: { id: createdEntityId }, 6 }); 7 } 8});
Pattern: Type-Safe Inputs
typescript1const input = { 2 name: 'myEntity', 3 label: 'My Entity', 4} as const satisfies CreateMyEntityInput;
Pattern: Snapshot Testing
typescript1expectOneNotInternalServerErrorSnapshot({ 2 errors, 3});
Running Tests
bash1# 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
-
create-my-entity-query-factory.util.tscreated -
create-my-entity.util.tscreated -
update-my-entity-query-factory.util.tscreated -
update-my-entity.util.tscreated -
delete-my-entity-query-factory.util.tscreated -
delete-my-entity.util.tscreated
Failing Tests Coverage
- Missing required fields
- Empty string validation
- Uniqueness violations
- Standard entity protection
- Foreign key validation
- JSONB property validation (if applicable)
Successful Tests Coverage
- Create with minimal input
- Create with all optional fields
- Input sanitization (whitespace)
- Long text content
- Update single field
- Update multiple fields
- Successful deletion
Snapshot Tests
- All failing tests use
expectOneNotInternalServerErrorSnapshot - Snapshots committed to
__snapshots__/directory
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.