- Place test files in
__test__directories - Name test files with
.test.tssuffix - Group related tests in describe blocks
- Use clear, descriptive test names
- All Graphql api tests are placed under
src/types/__test__/ - Example:
src/types/__test__/2/channel/articles.test.ts(related compiled filebuild/types/__test__/2/channel/articles.test.js)
let connections: Connections
let channelService: ChannelService
let atomService: AtomService
beforeAll(async () => {
connections = await genConnections()
channelService = new ChannelService(connections)
atomService = new AtomService(connections)
}, 30000) // Set appropriate timeout
afterAll(async () => {
await closeConnections(connections)
})
beforeEach(async () => {
// Clean up previous test data
await atomService.deleteMany({ table: 'topic_channel_article' })
await atomService.deleteMany({ table: 'topic_channel' })
})-
Query Structure
const GET_CHANNEL_ARTICLES = /* GraphQL */ ` query GetChannelArticles( $channelInput: ChannelInput! $articleInput: ChannelArticlesInput! ) { channel(input: $channelInput) { ... on TopicChannel { articles(input: $articleInput) { edges { pinned node { id } } } } } } `
-
Test Client Setup
const server = await testClient({ connections, isAuth: true, isAdmin: true, // Use this instead of context: { viewer: admin } if admin.id is not needed })
-
Operation Execution
const { data, errors } = await server.executeOperation({ query: GET_CHANNEL_ARTICLES, variables: { channelInput: { shortHash: channel.shortHash, // Required field }, articleInput: { first: 10, filter: { dateTimeRange: { start, end }, }, }, }, })
-
Testing Multi-language Content
const PUT_ANNOUNCEMENT = /* GraphQL */ ` mutation PutAnnouncement($input: PutAnnouncementInput!) { putAnnouncement(input: $input) { id title titleEn: title(input: { language: en }) content cover link type visible order } } ` const { data, errors } = await server.executeOperation({ query: PUT_ANNOUNCEMENT, variables: { input: { title: [ { language: 'zh_hant', text: '測試標題' }, { language: 'en', text: 'Test Title' }, ], content: [ { language: 'zh_hant', text: '測試內容' }, { language: 'en', text: 'Test Content' }, ], link: [ { language: 'zh_hant', text: 'https://example.com' }, { language: 'en', text: 'https://example.com' }, ], type: 'community', visible: true, order: 1, }, }, }) expect(errors).toBeUndefined() expect(data?.putAnnouncement).toBeDefined() expect(data?.putAnnouncement.title).toBe('測試標題') expect(data?.putAnnouncement.titleEn).toBe('Test Title')
-
Test Data Creation
// Create test channel, prefer using specific Services over AtomService const channel = await channelService.createTopicChannel({ name: 'test-topic', providerId: 'test-provider-id', enabled: true, })
-
Cleanup
beforeEach(async () => { await atomService.deleteMany({ table: 'topic_channel_article' }) await atomService.deleteMany({ table: 'topic_channel' }) })
-
Basic Assertions
expect(errors).toBeUndefined() expect(data?.node.articles.edges).toHaveLength(3)
-
ID Assertions
// When asserting IDs, use toGlobalId to match the GraphQL global ID format expect(data?.putAnnouncement.channels[0].channel.id).toBe(toGlobalId({ type: NODE_TYPES.TopicChannel, id: channel.id, }))
- Each test should be independent
- Clean up data between tests using
beforeEach - Don't rely on test execution order
- Example:
beforeEach(async () => { await atomService.deleteMany({ table: 'topic_channel_article' }) await atomService.deleteMany({ table: 'topic_channel' }) })
- Test both success and error cases
- Verify error codes using
errors?.[0].extensions.code, do not verify error messages - Handle async errors appropriately
- Example:
// Success case expect(errors).toBeUndefined() expect(data).toBeDefined() // Error cases expect(errors).toBeDefined() expect(errors?.[0].extensions.code).toBe('BAD_USER_INPUT') // For validation errors expect(errors?.[0].extensions.code).toBe('ENTITY_NOT_FOUND') // For not found errors expect(errors?.[0].extensions.code).toBe('FORBIDDEN') // For permission errors
- Use appropriate timeouts (e.g., 30000ms for
beforeAll) - Clean up resources promptly in
afterAll - Avoid unnecessary setup/teardown
- Group related tests in describe blocks
- Use descriptive test names
- Keep test files focused and concise
- Example:
describe('datetimeRange filtering', () => { test('filters articles within date range', async () => { // Test implementation }) })
- Always use
v4()from theuuidpackage to generate unique identifiers - Never use hardcoded test UUIDs like
'test-uuid-1'as they can cause conflicts - Import
v4at the top of test files:import { v4 } from 'uuid' - This ensures test isolation and prevents data conflicts between test runs
The test database is seeded with predefined data in db/seeds/ directory:
01_users.js: Test users with different roles and states- Users: test1-test4, admin1, matty, active, frozen, etc.
- Various states: active, banned
- Different roles: admin, user
05_articles.js: Test articles with different content and authors- Articles: test article 1-6
- Different authors and states
- Other seed files for comments, tags, etc.
-
Prefer Seed Data Over New Data
// Good: Use existing seed data const article = await atomService.findUnique({ table: 'article', where: { id: '1' }, // References test article 1 from seeds }) // Avoid: Creating new test data unless necessary const newArticle = await articleService.createArticle({ authorId: '1', title: 'test', content: 'test', })
-
Document Seed Data Usage
describe('article search', () => { test('finds article by title', async () => { // Using seed article "test article 1" from db/seeds/05_articles.js const { data } = await server.executeOperation({ query: SEARCH_ARTICLES, variables: { input: { searchKey: 'test article 1' }, }, }) expect(data.articles.edges.length).toBeGreaterThan(0) }) })
npm run build && MATTERS_ENV=test node --experimental-vm-modules --no-experimental-fetch node_modules/.bin/jest build/types/__test__/2/channel/articles.test.js
- Keep tests focused on one aspect
- Always check GraphQL schema for required fields in inputs
- Verify input types match schema definitions
- Use service methods for data creation when available
- Prefer using existing user IDs when possible