Integration Testing Guidelines
Overview
This document outlines integration testing practices for our SaaS CRM application, covering API testing, database integration, end-to-end workflows, and third-party service integration.
Integration Testing Scope
What to Test
- API endpoint integration
- Database transactions
- Service layer interactions
- Authentication flows
- Third-party API integrations
- Message queue processing
- Cache layer functionality
- File upload/download
- Email sending
- Cross-service communication
API Integration Testing
REST API Testing Setup
// tests/integration/api/setup.ts
import { beforeAll, afterAll, beforeEach } from 'vitest'
import { setupServer } from 'msw/node'
import { db } from './db'
export const server = setupServer()
beforeAll(() => {
server.listen({ onUnhandledRequest: 'error' })
db.migrate.latest()
})
beforeEach(() => {
server.resetHandlers()
db.seed.run()
})
afterAll(() => {
server.close()
db.destroy()
})
API Endpoint Testing
// tests/integration/api/customers.test.ts
import { describe, it, expect, beforeEach } from 'vitest'
import { createApp } from '~/server'
import request from 'supertest'
import { generateToken } from '~/utils/auth'
describe('Customer API Integration', () => {
let app: any
let authToken: string
beforeEach(async () => {
app = await createApp()
authToken = generateToken({ id: 1, role: 'admin' })
})
describe('GET /api/customers', () => {
it('should return paginated customers', async () => {
const response = await request(app)
.get('/api/customers')
.set('Authorization', `Bearer ${authToken}`)
.query({ page: 1, limit: 10 })
.expect(200)
expect(response.body).toHaveProperty('data')
expect(response.body.data).toBeInstanceOf(Array)
expect(response.body.data.length).toBeLessThanOrEqual(10)
expect(response.body).toHaveProperty('meta')
expect(response.body.meta).toHaveProperty('total')
expect(response.body.meta).toHaveProperty('current_page')
})
it('should filter customers by status', async () => {
const response = await request(app)
.get('/api/customers')
.set('Authorization', `Bearer ${authToken}`)
.query({ status: 'active' })
.expect(200)
response.body.data.forEach((customer: any) => {
expect(customer.status).toBe('active')
})
})
it('should return 401 without authentication', async () => {
await request(app)
.get('/api/customers')
.expect(401)
})
})
describe('POST /api/customers', () => {
it('should create a new customer', async () => {
const newCustomer = {
name: 'Integration Test Company',
email: 'integration@test.com',
phone: '555-0100',
address: {
street: '123 Test St',
city: 'Test City',
state: 'TS',
zip: '12345'
}
}
const response = await request(app)
.post('/api/customers')
.set('Authorization', `Bearer ${authToken}`)
.send(newCustomer)
.expect(201)
expect(response.body.data).toHaveProperty('id')
expect(response.body.data.name).toBe(newCustomer.name)
expect(response.body.data.email).toBe(newCustomer.email)
// Verify in database
const dbCustomer = await db('customers')
.where('id', response.body.data.id)
.first()
expect(dbCustomer).toBeTruthy()
expect(dbCustomer.name).toBe(newCustomer.name)
})
it('should validate required fields', async () => {
const invalidCustomer = {
name: 'Test Company'
// Missing required email
}
const response = await request(app)
.post('/api/customers')
.set('Authorization', `Bearer ${authToken}`)
.send(invalidCustomer)
.expect(422)
expect(response.body.error).toHaveProperty('details')
expect(response.body.error.details).toHaveProperty('email')
})
it('should prevent duplicate emails', async () => {
const customer = {
name: 'Test Company',
email: 'duplicate@test.com'
}
// Create first customer
await request(app)
.post('/api/customers')
.set('Authorization', `Bearer ${authToken}`)
.send(customer)
.expect(201)
// Attempt to create duplicate
const response = await request(app)
.post('/api/customers')
.set('Authorization', `Bearer ${authToken}`)
.send(customer)
.expect(422)
expect(response.body.error.message).toContain('already exists')
})
})
describe('PUT /api/customers/:id', () => {
it('should update customer data', async () => {
// Create customer first
const createResponse = await request(app)
.post('/api/customers')
.set('Authorization', `Bearer ${authToken}`)
.send({
name: 'Original Name',
email: 'original@test.com'
})
.expect(201)
const customerId = createResponse.body.data.id
// Update customer
const updateResponse = await request(app)
.put(`/api/customers/${customerId}`)
.set('Authorization', `Bearer ${authToken}`)
.send({
name: 'Updated Name',
status: 'inactive'
})
.expect(200)
expect(updateResponse.body.data.name).toBe('Updated Name')
expect(updateResponse.body.data.status).toBe('inactive')
expect(updateResponse.body.data.email).toBe('original@test.com')
})
it('should return 404 for non-existent customer', async () => {
await request(app)
.put('/api/customers/99999')
.set('Authorization', `Bearer ${authToken}`)
.send({ name: 'Updated' })
.expect(404)
})
})
describe('DELETE /api/customers/:id', () => {
it('should soft delete customer', async () => {
// Create customer
const createResponse = await request(app)
.post('/api/customers')
.set('Authorization', `Bearer ${authToken}`)
.send({
name: 'To Delete',
email: 'delete@test.com'
})
.expect(201)
const customerId = createResponse.body.data.id
// Delete customer
await request(app)
.delete(`/api/customers/${customerId}`)
.set('Authorization', `Bearer ${authToken}`)
.expect(200)
// Verify soft delete
const dbCustomer = await db('customers')
.where('id', customerId)
.first()
expect(dbCustomer.deleted_at).toBeTruthy()
// Should not appear in list
const listResponse = await request(app)
.get('/api/customers')
.set('Authorization', `Bearer ${authToken}`)
.expect(200)
const found = listResponse.body.data.find(
(c: any) => c.id === customerId
)
expect(found).toBeUndefined()
})
})
})
Database Integration Testing
Database Test Setup
// tests/integration/db/setup.ts
import knex from 'knex'
import config from '~/knexfile'
const testConfig = {
...config.test,
migrations: {
directory: './migrations'
},
seeds: {
directory: './tests/seeds'
}
}
export const db = knex(testConfig)
export async function setupTestDb() {
await db.migrate.rollback()
await db.migrate.latest()
await db.seed.run()
}
export async function teardownTestDb() {
await db.migrate.rollback()
await db.destroy()
}
Transaction Testing
// tests/integration/db/transactions.test.ts
import { describe, it, expect, beforeEach } from 'vitest'
import { db, setupTestDb, teardownTestDb } from './setup'
import { CustomerService } from '~/services/CustomerService'
describe('Database Transactions', () => {
let service: CustomerService
beforeEach(async () => {
await setupTestDb()
service = new CustomerService(db)
})
afterEach(async () => {
await teardownTestDb()
})
it('should rollback transaction on error', async () => {
const trx = await db.transaction()
try {
// Insert customer
const [customerId] = await trx('customers').insert({
name: 'Test Customer',
email: 'test@example.com'
})
// Insert related data
await trx('contacts').insert({
customer_id: customerId,
name: 'Test Contact',
email: 'contact@example.com'
})
// Simulate error
throw new Error('Simulated error')
await trx.commit()
} catch (error) {
await trx.rollback()
}
// Verify rollback
const customers = await db('customers')
.where('email', 'test@example.com')
const contacts = await db('contacts')
.where('email', 'contact@example.com')
expect(customers).toHaveLength(0)
expect(contacts).toHaveLength(0)
})
it('should handle concurrent transactions', async () => {
const promises = Array.from({ length: 10 }, (_, i) =>
service.createCustomer({
name: `Customer ${i}`,
email: `customer${i}@test.com`
})
)
const results = await Promise.all(promises)
expect(results).toHaveLength(10)
results.forEach((customer, index) => {
expect(customer.name).toBe(`Customer ${index}`)
})
const count = await db('customers').count('* as total')
expect(count[0].total).toBe(10)
})
it('should enforce foreign key constraints', async () => {
await expect(
db('contacts').insert({
customer_id: 99999, // Non-existent customer
name: 'Test Contact',
email: 'contact@test.com'
})
).rejects.toThrow(/foreign key constraint/i)
})
it('should handle deadlocks gracefully', async () => {
const customer1 = await service.createCustomer({
name: 'Customer 1',
email: 'c1@test.com'
})
const customer2 = await service.createCustomer({
name: 'Customer 2',
email: 'c2@test.com'
})
// Simulate potential deadlock scenario
const trx1 = await db.transaction()
const trx2 = await db.transaction()
try {
// Transaction 1: Update customer1 then customer2
await trx1('customers')
.where('id', customer1.id)
.update({ status: 'active' })
// Transaction 2: Update customer2 then customer1
await trx2('customers')
.where('id', customer2.id)
.update({ status: 'active' })
await trx1('customers')
.where('id', customer2.id)
.update({ status: 'inactive' })
await trx2('customers')
.where('id', customer1.id)
.update({ status: 'inactive' })
await Promise.all([trx1.commit(), trx2.commit()])
} catch (error) {
await trx1.rollback()
await trx2.rollback()
expect(error.message).toMatch(/deadlock|timeout/i)
}
})
})
Service Integration Testing
Multi-Service Testing
// tests/integration/services/workflow.test.ts
import { describe, it, expect, beforeEach, vi } from 'vitest'
import { CustomerService } from '~/services/CustomerService'
import { EmailService } from '~/services/EmailService'
import { NotificationService } from '~/services/NotificationService'
import { QueueService } from '~/services/QueueService'
describe('Customer Onboarding Workflow', () => {
let customerService: CustomerService
let emailService: EmailService
let notificationService: NotificationService
let queueService: QueueService
beforeEach(() => {
customerService = new CustomerService()
emailService = new EmailService()
notificationService = new NotificationService()
queueService = new QueueService()
vi.spyOn(emailService, 'send')
vi.spyOn(notificationService, 'notify')
vi.spyOn(queueService, 'publish')
})
it('should complete full onboarding workflow', async () => {
// Step 1: Create customer
const customer = await customerService.create({
name: 'New Company',
email: 'new@company.com',
plan: 'enterprise'
})
expect(customer).toHaveProperty('id')
expect(customer.status).toBe('pending')
// Step 2: Verify welcome email sent
expect(emailService.send).toHaveBeenCalledWith({
to: 'new@company.com',
template: 'welcome',
data: expect.objectContaining({
name: 'New Company'
})
})
// Step 3: Verify internal notification
expect(notificationService.notify).toHaveBeenCalledWith({
type: 'new_customer',
data: expect.objectContaining({
customerId: customer.id
})
})
// Step 4: Verify queue message
expect(queueService.publish).toHaveBeenCalledWith(
'customer.created',
expect.objectContaining({
customerId: customer.id,
plan: 'enterprise'
})
)
// Step 5: Activate customer
const activated = await customerService.activate(customer.id)
expect(activated.status).toBe('active')
// Step 6: Verify activation email
expect(emailService.send).toHaveBeenCalledWith({
to: 'new@company.com',
template: 'activation',
data: expect.any(Object)
})
// Step 7: Check audit trail
const auditLogs = await customerService.getAuditLogs(customer.id)
expect(auditLogs).toContainEqual(
expect.objectContaining({
action: 'created',
userId: expect.any(Number)
})
)
expect(auditLogs).toContainEqual(
expect.objectContaining({
action: 'activated',
userId: expect.any(Number)
})
)
})
it('should handle workflow errors gracefully', async () => {
// Simulate email service failure
emailService.send = vi.fn().mockRejectedValue(
new Error('Email service down')
)
const customer = await customerService.create({
name: 'Test Company',
email: 'test@company.com'
})
// Customer should still be created
expect(customer).toHaveProperty('id')
// But status should indicate email failure
expect(customer.emailVerified).toBe(false)
// Error should be logged
const logs = await customerService.getErrorLogs(customer.id)
expect(logs).toContainEqual(
expect.objectContaining({
error: 'Email service down',
context: 'welcome_email'
})
)
// Retry queue should have the task
const retryTasks = await queueService.getRetryQueue()
expect(retryTasks).toContainEqual(
expect.objectContaining({
type: 'send_welcome_email',
customerId: customer.id
})
)
})
})
Third-Party Integration Testing
External API Mocking
// tests/integration/external/payment.test.ts
import { describe, it, expect, beforeEach } from 'vitest'
import nock from 'nock'
import { PaymentService } from '~/services/PaymentService'
describe('Payment Gateway Integration', () => {
let paymentService: PaymentService
beforeEach(() => {
paymentService = new PaymentService({
apiKey: 'test_key',
baseUrl: 'https://api.payment.com'
})
nock.cleanAll()
})
it('should process payment successfully', async () => {
// Mock payment API response
nock('https://api.payment.com')
.post('/charges')
.reply(200, {
id: 'ch_123',
status: 'succeeded',
amount: 9999,
currency: 'usd'
})
const result = await paymentService.charge({
amount: 9999,
currency: 'usd',
source: 'tok_visa',
description: 'CRM Subscription'
})
expect(result.status).toBe('succeeded')
expect(result.id).toBe('ch_123')
})
it('should handle payment failures', async () => {
nock('https://api.payment.com')
.post('/charges')
.reply(402, {
error: {
code: 'card_declined',
message: 'Your card was declined'
}
})
await expect(
paymentService.charge({
amount: 9999,
source: 'tok_declined'
})
).rejects.toThrow('Your card was declined')
})
it('should retry on network errors', async () => {
// First attempt fails
nock('https://api.payment.com')
.post('/charges')
.replyWithError('Network error')
// Second attempt fails
nock('https://api.payment.com')
.post('/charges')
.replyWithError('Network error')
// Third attempt succeeds
nock('https://api.payment.com')
.post('/charges')
.reply(200, {
id: 'ch_123',
status: 'succeeded'
})
const result = await paymentService.chargeWithRetry({
amount: 9999,
source: 'tok_visa'
})
expect(result.status).toBe('succeeded')
expect(result.attempts).toBe(3)
})
it('should handle webhook validation', async () => {
const payload = {
id: 'evt_123',
type: 'charge.succeeded',
data: {
object: {
id: 'ch_123',
amount: 9999
}
}
}
const signature = paymentService.generateWebhookSignature(
JSON.stringify(payload)
)
const isValid = await paymentService.validateWebhook(
payload,
signature
)
expect(isValid).toBe(true)
})
})
Message Queue Integration
Queue Testing
// tests/integration/queue/message.test.ts
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
import amqp from 'amqplib'
import { QueueService } from '~/services/QueueService'
import { EmailWorker } from '~/workers/EmailWorker'
describe('Message Queue Integration', () => {
let connection: amqp.Connection
let channel: amqp.Channel
let queueService: QueueService
let emailWorker: EmailWorker
beforeEach(async () => {
connection = await amqp.connect('amqp://localhost')
channel = await connection.createChannel()
queueService = new QueueService(channel)
emailWorker = new EmailWorker(channel)
await channel.assertQueue('test_queue', { durable: true })
})
afterEach(async () => {
await channel.deleteQueue('test_queue')
await channel.close()
await connection.close()
})
it('should publish and consume messages', async () => {
const messages: any[] = []
// Set up consumer
await emailWorker.consume('test_queue', async (msg) => {
messages.push(JSON.parse(msg.content.toString()))
channel.ack(msg)
})
// Publish messages
await queueService.publish('test_queue', {
type: 'welcome_email',
customerId: 123
})
await queueService.publish('test_queue', {
type: 'password_reset',
userId: 456
})
// Wait for processing
await new Promise(resolve => setTimeout(resolve, 100))
expect(messages).toHaveLength(2)
expect(messages[0].type).toBe('welcome_email')
expect(messages[1].type).toBe('password_reset')
})
it('should handle message acknowledgment', async () => {
let processedCount = 0
await emailWorker.consume('test_queue', async (msg) => {
const data = JSON.parse(msg.content.toString())
if (data.shouldFail) {
// Reject and requeue
channel.nack(msg, false, true)
} else {
processedCount++
channel.ack(msg)
}
})
// Send messages
await queueService.publish('test_queue', { shouldFail: true })
await queueService.publish('test_queue', { shouldFail: false })
await new Promise(resolve => setTimeout(resolve, 100))
expect(processedCount).toBe(1)
// Check queue for requeued message
const msg = await channel.get('test_queue')
expect(msg).toBeTruthy()
if (msg) {
const data = JSON.parse(msg.content.toString())
expect(data.shouldFail).toBe(true)
}
})
it('should handle dead letter queue', async () => {
// Set up DLQ
await channel.assertExchange('dlx', 'direct')
await channel.assertQueue('dlq')
await channel.bindQueue('dlq', 'dlx', '')
// Main queue with DLX
await channel.assertQueue('main_queue', {
arguments: {
'x-dead-letter-exchange': 'dlx',
'x-max-retries': 3
}
})
let attempts = 0
await emailWorker.consume('main_queue', async (msg) => {
attempts++
// Always reject to test DLQ
channel.nack(msg, false, false)
})
await queueService.publish('main_queue', { test: 'data' })
await new Promise(resolve => setTimeout(resolve, 200))
// Message should be in DLQ after max retries
const dlqMsg = await channel.get('dlq')
expect(dlqMsg).toBeTruthy()
if (dlqMsg) {
const data = JSON.parse(dlqMsg.content.toString())
expect(data.test).toBe('data')
}
})
})
Cache Integration Testing
Redis Cache Testing
// tests/integration/cache/redis.test.ts
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
import Redis from 'ioredis'
import { CacheService } from '~/services/CacheService'
describe('Redis Cache Integration', () => {
let redis: Redis
let cache: CacheService
beforeEach(async () => {
redis = new Redis({
host: 'localhost',
port: 6379,
db: 1 // Use separate DB for tests
})
cache = new CacheService(redis)
await redis.flushdb()
})
afterEach(async () => {
await redis.quit()
})
it('should cache and retrieve data', async () => {
const data = { id: 1, name: 'Test Customer' }
await cache.set('customer:1', data, 60)
const cached = await cache.get('customer:1')
expect(cached).toEqual(data)
})
it('should expire cache after TTL', async () => {
await cache.set('temp:1', 'data', 1) // 1 second TTL
// Should exist immediately
let value = await cache.get('temp:1')
expect(value).toBe('data')
// Wait for expiration
await new Promise(resolve => setTimeout(resolve, 1100))
value = await cache.get('temp:1')
expect(value).toBeNull()
})
it('should handle cache stampede', async () => {
let dbCalls = 0
const fetchFromDb = async () => {
dbCalls++
await new Promise(resolve => setTimeout(resolve, 100))
return { data: 'from db' }
}
// Simulate multiple concurrent requests
const promises = Array.from({ length: 10 }, () =>
cache.getOrSet('shared:key', fetchFromDb, 60)
)
const results = await Promise.all(promises)
// All should get same data
results.forEach(result => {
expect(result).toEqual({ data: 'from db' })
})
// DB should only be called once
expect(dbCalls).toBe(1)
})
it('should handle cache invalidation', async () => {
// Set multiple related keys
await cache.set('customer:1', { name: 'Customer 1' })
await cache.set('customer:2', { name: 'Customer 2' })
await cache.set('customer:list', ['1', '2'])
// Invalidate pattern
await cache.invalidatePattern('customer:*')
// All should be gone
expect(await cache.get('customer:1')).toBeNull()
expect(await cache.get('customer:2')).toBeNull()
expect(await cache.get('customer:list')).toBeNull()
})
})
File Upload Integration
File Upload Testing
// tests/integration/upload/file.test.ts
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
import request from 'supertest'
import fs from 'fs/promises'
import path from 'path'
import { app } from '~/app'
describe('File Upload Integration', () => {
const uploadDir = path.join(__dirname, 'test-uploads')
beforeEach(async () => {
await fs.mkdir(uploadDir, { recursive: true })
})
afterEach(async () => {
await fs.rm(uploadDir, { recursive: true, force: true })
})
it('should upload single file', async () => {
const response = await request(app)
.post('/api/upload')
.attach('file', path.join(__dirname, 'fixtures/test.pdf'))
.expect(200)
expect(response.body).toHaveProperty('filename')
expect(response.body).toHaveProperty('size')
expect(response.body).toHaveProperty('mimetype')
expect(response.body.mimetype).toBe('application/pdf')
// Verify file exists
const uploadedFile = path.join(uploadDir, response.body.filename)
const stats = await fs.stat(uploadedFile)
expect(stats.size).toBeGreaterThan(0)
})
it('should validate file type', async () => {
const response = await request(app)
.post('/api/upload')
.attach('file', path.join(__dirname, 'fixtures/test.exe'))
.expect(422)
expect(response.body.error).toContain('Invalid file type')
})
it('should enforce file size limit', async () => {
// Create large file
const largeFile = path.join(uploadDir, 'large.txt')
await fs.writeFile(largeFile, Buffer.alloc(11 * 1024 * 1024)) // 11MB
const response = await request(app)
.post('/api/upload')
.attach('file', largeFile)
.expect(422)
expect(response.body.error).toContain('File too large')
})
it('should handle multiple file uploads', async () => {
const response = await request(app)
.post('/api/upload/multiple')
.attach('files', path.join(__dirname, 'fixtures/doc1.pdf'))
.attach('files', path.join(__dirname, 'fixtures/doc2.pdf'))
.attach('files', path.join(__dirname, 'fixtures/image.png'))
.expect(200)
expect(response.body.files).toHaveLength(3)
response.body.files.forEach((file: any) => {
expect(file).toHaveProperty('filename')
expect(file).toHaveProperty('size')
})
})
it('should scan uploaded files for viruses', async () => {
const response = await request(app)
.post('/api/upload/secure')
.attach('file', path.join(__dirname, 'fixtures/test.pdf'))
.expect(200)
expect(response.body.scanned).toBe(true)
expect(response.body.clean).toBe(true)
})
})
Performance Testing
Load Testing
// tests/integration/performance/load.test.ts
import { describe, it, expect } from 'vitest'
import autocannon from 'autocannon'
import { app } from '~/app'
describe('Load Testing', () => {
it('should handle concurrent requests', async () => {
const server = app.listen(0)
const port = server.address().port
const result = await autocannon({
url: `http://localhost:${port}/api/customers`,
connections: 10,
pipelining: 1,
duration: 10,
headers: {
'Authorization': 'Bearer test-token'
}
})
server.close()
expect(result.errors).toBe(0)
expect(result.timeouts).toBe(0)
expect(result.requests.average).toBeGreaterThan(100) // 100+ req/sec
expect(result.latency.p99).toBeLessThan(1000) // 99% under 1 second
})
it('should handle database connection pool', async () => {
const promises = Array.from({ length: 100 }, (_, i) =>
db.raw('SELECT SLEEP(0.1) as result')
)
const start = Date.now()
await Promise.all(promises)
const duration = Date.now() - start
// Should use connection pool efficiently
expect(duration).toBeLessThan(5000) // Should complete in under 5 seconds
})
})
End-to-End Testing
Complete User Journey
// tests/integration/e2e/customer-journey.test.ts
import { describe, it, expect } from 'vitest'
import { Browser, Page } from 'playwright'
import { chromium } from 'playwright'
describe('Customer Journey E2E', () => {
let browser: Browser
let page: Page
beforeAll(async () => {
browser = await chromium.launch()
})
afterAll(async () => {
await browser.close()
})
beforeEach(async () => {
page = await browser.newPage()
})
afterEach(async () => {
await page.close()
})
it('should complete full customer signup flow', async () => {
// Navigate to signup
await page.goto('http://localhost:3000/signup')
// Fill signup form
await page.fill('[name="company"]', 'Test Company')
await page.fill('[name="email"]', 'test@company.com')
await page.fill('[name="password"]', 'SecurePass123!')
await page.click('[type="submit"]')
// Should redirect to verification
await page.waitForURL('**/verify-email')
expect(page.url()).toContain('/verify-email')
// Simulate email verification (in test mode)
await page.goto('http://localhost:3000/verify?token=test-token')
// Should redirect to dashboard
await page.waitForURL('**/dashboard')
expect(page.url()).toContain('/dashboard')
// Should see welcome message
const welcome = await page.textContent('.welcome-message')
expect(welcome).toContain('Welcome, Test Company')
// Create first customer
await page.click('[data-testid="add-customer"]')
await page.fill('[name="customerName"]', 'First Customer')
await page.fill('[name="customerEmail"]', 'customer@example.com')
await page.click('[data-testid="save-customer"]')
// Should appear in list
await page.waitForSelector('[data-testid="customer-list"]')
const customerName = await page.textContent(
'[data-testid="customer-list"] li:first-child'
)
expect(customerName).toContain('First Customer')
})
})
Test Environment Configuration
Docker Compose for Testing
# docker-compose.test.yml
version: '3.8'
services:
mysql-test:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: test
MYSQL_DATABASE: crm_test
ports:
- "3307:3306"
tmpfs:
- /var/lib/mysql
redis-test:
image: redis:7-alpine
ports:
- "6380:6379"
rabbitmq-test:
image: rabbitmq:3-management
ports:
- "5673:5672"
- "15673:15672"
environment:
RABBITMQ_DEFAULT_USER: test
RABBITMQ_DEFAULT_PASS: test
mailhog:
image: mailhog/mailhog
ports:
- "1025:1025"
- "8025:8025"
Last Updated: January 2025 Version: 1.0.0