Testing Strategies Guide
This comprehensive guide covers testing strategies for NatsPubsub applications, from unit tests to integration tests, including mocking strategies, test utilities, and best practices for both JavaScript/TypeScript and Ruby implementations.
Table of Contents
- Overview
- Testing Philosophy
- Unit Testing Publishers
- Unit Testing Subscribers
- Integration Testing
- Test Utilities and Helpers
- Mocking Strategies
- Testing Middleware
- Testing Error Handling
- Testing Batch Operations
- End-to-End Testing
- Performance Testing
- Best Practices
- CI/CD Integration
Overview
Testing pub/sub systems requires special consideration because of their asynchronous, distributed nature. NatsPubsub provides testing utilities to make this easier.
Testing Pyramid
Testing Philosophy
What to Test
- Unit Tests: Publisher and subscriber logic in isolation
- Integration Tests: Message flow through the system
- Contract Tests: Message format compatibility
- Performance Tests: Throughput and latency under load
What NOT to Test
- NATS server internals (trust the library)
- Network reliability (use mocks)
- JetStream features (already tested by NATS)
Unit Testing Publishers
JavaScript/TypeScript
Basic Publisher Test
import NatsPubsub from "nats-pubsub";
import connection from "../core/connection";
// Mock NATS connection
jest.mock("../core/connection");
describe("Publishing", () => {
let mockJetstream: any;
beforeEach(() => {
// Configure NatsPubsub for testing
NatsPubsub.configure({
natsUrls: "nats://localhost:4222",
env: "test",
appName: "test-app",
});
mockJetstream = {
publish: jest.fn().mockResolvedValue(undefined),
};
(connection.ensureConnection as jest.Mock) = jest
.fn()
.mockResolvedValue(undefined);
(connection.getJetStream as jest.Mock) = jest
.fn()
.mockReturnValue(mockJetstream);
});
afterEach(() => {
jest.clearAllMocks();
});
it("publishes a message successfully", async () => {
const message = {
order_id: "ORD-123",
customer_id: "CUST-456",
total: 99.99,
};
await NatsPubsub.publish("order.created", message);
// Verify publish was called
expect(connection.ensureConnection).toHaveBeenCalled();
expect(mockJetstream.publish).toHaveBeenCalledWith(
"test.test-app.order.created",
expect.any(String), // JSON payload
expect.objectContaining({
msgID: expect.any(String),
}),
);
// Verify message content
const publishCall = mockJetstream.publish.mock.calls[0];
const payload = JSON.parse(publishCall[1]);
expect(payload.payload).toEqual(message);
});
it("publishes with custom metadata", async () => {
const message = { order_id: "ORD-123" };
const options = {
trace_id: "trace-abc",
event_id: "evt-123",
};
await NatsPubsub.publish("order.created", message, options);
const publishCall = mockJetstream.publish.mock.calls[0];
const payload = JSON.parse(publishCall[1]);
expect(payload.trace_id).toBe("trace-abc");
expect(payload.event_id).toBe("evt-123");
});
it("publishes to multiple topics", async () => {
const message = { order_id: "ORD-123" };
await NatsPubsub.publish({
topics: ["order.created", "notification.email"],
message,
});
expect(mockJetstream.publish).toHaveBeenCalledTimes(2);
expect(mockJetstream.publish).toHaveBeenCalledWith(
"test.test-app.order.created",
expect.any(String),
expect.any(Object),
);
expect(mockJetstream.publish).toHaveBeenCalledWith(
"test.test-app.notification.email",
expect.any(String),
expect.any(Object),
);
});
});
Testing Batch Publishing
describe("Batch Publishing", () => {
it("publishes multiple messages in batch", async () => {
const batch = NatsPubsub.batch();
batch
.add("order.created", { order_id: "ORD-1" })
.add("order.created", { order_id: "ORD-2" })
.add("order.created", { order_id: "ORD-3" });
const result = await batch.publish();
expect(result.count).toBe(3);
expect(result.successCount).toBe(3);
expect(mockJetstream.publish).toHaveBeenCalledTimes(3);
});
it("handles partial failures in batch", async () => {
mockJetstream.publish
.mockResolvedValueOnce(undefined) // First succeeds
.mockRejectedValueOnce(new Error("Network error")) // Second fails
.mockResolvedValueOnce(undefined); // Third succeeds
const batch = NatsPubsub.batch();
batch
.add("order.created", { order_id: "ORD-1" })
.add("order.created", { order_id: "ORD-2" })
.add("order.created", { order_id: "ORD-3" });
const result = await batch.publish();
expect(result.count).toBe(3);
expect(result.successCount).toBe(2);
expect(result.failureCount).toBe(1);
});
});
Ruby
Basic Publisher Test
require 'rails_helper'
RSpec.describe 'Publishing', type: :publisher do
let(:mock_jetstream) { double('JetStream') }
before do
allow(NatsPubsub::Connection).to receive(:connect!).and_return(mock_jetstream)
allow(mock_jetstream).to receive(:publish)
NatsPubsub.configure do |config|
config.servers = 'nats://localhost:4222'
config.env = 'test'
config.app_name = 'test-app'
end
end
describe 'basic publishing' do
it 'publishes a message successfully' do
message = {
order_id: 'ORD-123',
customer_id: 'CUST-456',
total: 99.99
}
NatsPubsub.publish('order.created', message)
expect(mock_jetstream).to have_received(:publish).with(
'test.test-app.order.created',
kind_of(String),
hash_including(
headers: hash_including('nats-msg-id' => kind_of(String))
)
)
end
it 'publishes with custom metadata' do
message = { order_id: 'ORD-123' }
options = {
trace_id: 'trace-abc',
event_id: 'evt-123'
}
NatsPubsub.publish('order.created', message, **options)
expect(mock_jetstream).to have_received(:publish) do |_subject, payload, _opts|
parsed = JSON.parse(payload)
expect(parsed['trace_id']).to eq('trace-abc')
expect(parsed['event_id']).to eq('evt-123')
end
end
end
describe 'multi-topic publishing' do
it 'publishes to multiple topics' do
message = { order_id: 'ORD-123' }
NatsPubsub.publish(
topics: ['order.created', 'notification.email'],
message: message
)
expect(mock_jetstream).to have_received(:publish).twice
end
end
end
Testing with Fake Mode
require 'rails_helper'
RSpec.describe 'Publishing with Fake Mode', type: :publisher do
around do |example|
NatsPubsub::Testing.fake! do
example.run
end
end
it 'captures published events' do
NatsPubsub.publish('order.created', { order_id: 'ORD-123' })
expect(NatsPubsub::Testing).to have_published_event('order.created')
end
it 'captures event payload' do
NatsPubsub.publish('order.created', { order_id: 'ORD-123', total: 99.99 })
expect(NatsPubsub::Testing).to have_published_event_with_payload(
'order.created',
order_id: 'ORD-123',
total: 99.99
)
end
it 'tracks multiple events' do
NatsPubsub.publish('order.created', { order_id: 'ORD-1' })
NatsPubsub.publish('order.updated', { order_id: 'ORD-1' })
NatsPubsub.publish('order.cancelled', { order_id: 'ORD-1' })
expect(NatsPubsub::Testing.published_events.count).to eq(3)
end
end
Unit Testing Subscribers
JavaScript/TypeScript
Basic Subscriber Test
import { OrderCreatedSubscriber } from "../subscribers/order-created-subscriber";
import { TopicMetadata } from "nats-pubsub";
describe("OrderCreatedSubscriber", () => {
let subscriber: OrderCreatedSubscriber;
let mockOrderService: any;
beforeEach(() => {
subscriber = new OrderCreatedSubscriber();
mockOrderService = {
processOrder: jest.fn().mockResolvedValue(undefined),
};
// Inject mock dependency
(subscriber as any).orderService = mockOrderService;
});
it("processes order created event", async () => {
const message = {
order_id: "ORD-123",
customer_id: "CUST-456",
total: 99.99,
};
const metadata: TopicMetadata = {
event_id: "evt-123",
trace_id: "trace-456",
topic: "order.created",
subject: "production.order-service.order.created",
occurred_at: new Date(),
deliveries: 1,
};
await subscriber.handle(message, metadata);
expect(mockOrderService.processOrder).toHaveBeenCalledWith("ORD-123");
});
it("handles errors gracefully", async () => {
const message = { order_id: "INVALID" };
const metadata: TopicMetadata = {
event_id: "evt-123",
trace_id: "trace-456",
topic: "order.created",
subject: "production.order-service.order.created",
occurred_at: new Date(),
deliveries: 1,
};
mockOrderService.processOrder.mockRejectedValue(new Error("Invalid order"));
await expect(subscriber.handle(message, metadata)).rejects.toThrow(
"Invalid order",
);
});
it("is idempotent", async () => {
const message = { order_id: "ORD-123" };
const metadata: TopicMetadata = {
event_id: "evt-123",
trace_id: "trace-456",
topic: "order.created",
subject: "production.order-service.order.created",
occurred_at: new Date(),
deliveries: 1,
};
// Process twice with same event_id
await subscriber.handle(message, metadata);
await subscriber.handle(message, metadata);
// Should only process once
expect(mockOrderService.processOrder).toHaveBeenCalledTimes(1);
});
});
Testing with Spies
describe("NotificationSubscriber", () => {
let subscriber: NotificationSubscriber;
let emailSpy: jest.SpyInstance;
beforeEach(() => {
subscriber = new NotificationSubscriber();
emailSpy = jest.spyOn(emailService, "send").mockResolvedValue(undefined);
});
afterEach(() => {
emailSpy.mockRestore();
});
it("sends email notification", async () => {
const message = {
to: "user@example.com",
subject: "Welcome",
body: "Welcome to our service",
};
const metadata: TopicMetadata = {
event_id: "evt-123",
trace_id: "trace-456",
topic: "notification.email",
subject: "production.app.notification.email",
occurred_at: new Date(),
deliveries: 1,
};
await subscriber.handle(message, metadata);
expect(emailSpy).toHaveBeenCalledWith({
to: "user@example.com",
subject: "Welcome",
body: "Welcome to our service",
});
});
});
Ruby
Basic Subscriber Test
require 'rails_helper'
RSpec.describe OrderCreatedSubscriber, type: :subscriber do
let(:subscriber) { described_class.new }
let(:message) do
{
'order_id' => 'ORD-123',
'customer_id' => 'CUST-456',
'total' => 99.99
}
end
let(:context) do
double(
'MessageContext',
event_id: 'evt-123',
trace_id: 'trace-456',
topic: 'order.created',
subject: 'production.order-service.order.created',
occurred_at: Time.now,
deliveries: 1
)
end
describe '#handle' do
it 'processes order created event' do
expect(OrderService).to receive(:process_order).with('ORD-123')
subscriber.handle(message, context)
end
it 'handles errors gracefully' do
allow(OrderService).to receive(:process_order).and_raise('Invalid order')
expect { subscriber.handle(message, context) }
.to raise_error('Invalid order')
end
it 'is idempotent' do
# First call
subscriber.handle(message, context)
# Second call with same event_id
subscriber.handle(message, context)
# Should only process once
expect(OrderService).to have_received(:process_order).once
end
end
end
Testing with RSpec Matchers
require 'rails_helper'
RSpec.describe NotificationSubscriber, type: :subscriber do
let(:subscriber) { described_class.new }
describe '#handle' do
it 'sends email notification' do
message = {
'to' => 'user@example.com',
'subject' => 'Welcome',
'body' => 'Welcome to our service'
}
context = double(
'MessageContext',
event_id: 'evt-123',
topic: 'notification.email'
)
expect(EmailService).to receive(:send_email).with(
to: 'user@example.com',
subject: 'Welcome',
body: 'Welcome to our service'
)
subscriber.handle(message, context)
end
end
end
Integration Testing
JavaScript/TypeScript
End-to-End Message Flow
import NatsPubsub from "nats-pubsub";
import { OrderCreatedSubscriber } from "../subscribers/order-created-subscriber";
describe("Order Flow Integration", () => {
let subscriber: OrderCreatedSubscriber;
beforeAll(async () => {
// Use real NATS connection for integration tests
NatsPubsub.configure({
natsUrls: process.env.NATS_URL || "nats://localhost:4222",
env: "test",
appName: "test-app",
});
subscriber = new OrderCreatedSubscriber();
NatsPubsub.registerSubscriber(subscriber);
await NatsPubsub.start();
});
afterAll(async () => {
await NatsPubsub.stop();
});
it("processes published message", async () => {
const message = {
order_id: "ORD-INTEGRATION-123",
customer_id: "CUST-456",
total: 99.99,
};
// Publish message
await NatsPubsub.publish("order.created", message);
// Wait for processing
await new Promise((resolve) => setTimeout(resolve, 1000));
// Verify message was processed
const order = await Order.findOne({ order_id: "ORD-INTEGRATION-123" });
expect(order).toBeDefined();
expect(order.customer_id).toBe("CUST-456");
});
});
Testing with Test Harness
import { TestHarness } from "nats-pubsub/testing";
describe("Order Processing with Test Harness", () => {
let harness: TestHarness;
beforeEach(async () => {
harness = new TestHarness({
natsUrls: "nats://localhost:4222",
env: "test",
appName: "test-app",
});
await harness.start();
});
afterEach(async () => {
await harness.stop();
});
it("processes messages in test environment", async () => {
const receivedMessages: any[] = [];
// Register test subscriber
harness.subscribe("order.created", async (message, metadata) => {
receivedMessages.push(message);
});
// Publish message
await harness.publish("order.created", {
order_id: "ORD-TEST-123",
});
// Wait for processing
await harness.waitForMessages(1);
expect(receivedMessages).toHaveLength(1);
expect(receivedMessages[0].order_id).toBe("ORD-TEST-123");
});
});
Ruby
End-to-End Message Flow
require 'rails_helper'
RSpec.describe 'Order Flow Integration', type: :integration do
before(:all) do
NatsPubsub.configure do |config|
config.servers = ENV['NATS_URL'] || 'nats://localhost:4222'
config.env = 'test'
config.app_name = 'test-app'
end
NatsPubsub::Manager.register(OrderCreatedSubscriber)
NatsPubsub::Manager.start
end
after(:all) do
NatsPubsub::Manager.stop
end
it 'processes published message' do
message = {
order_id: 'ORD-INTEGRATION-123',
customer_id: 'CUST-456',
total: 99.99
}
# Publish message
NatsPubsub.publish('order.created', message)
# Wait for processing
sleep 1
# Verify message was processed
order = Order.find_by(order_id: 'ORD-INTEGRATION-123')
expect(order).to be_present
expect(order.customer_id).to eq('CUST-456')
end
end
Testing with Inline Mode
require 'rails_helper'
RSpec.describe 'Order Flow with Inline Mode', type: :integration do
around do |example|
NatsPubsub::Testing.inline! do
example.run
end
end
it 'processes messages synchronously' do
message = {
order_id: 'ORD-INLINE-123',
customer_id: 'CUST-456',
total: 99.99
}
# Publish and process immediately (synchronously)
NatsPubsub.publish('order.created', message)
# No sleep needed - processed synchronously
order = Order.find_by(order_id: 'ORD-INLINE-123')
expect(order).to be_present
end
end
Test Utilities and Helpers
JavaScript/TypeScript
Test Helper Functions
// test/helpers/nats-helpers.ts
import NatsPubsub from "nats-pubsub";
import { TopicMetadata } from "nats-pubsub";
export function createMockMetadata(
overrides?: Partial<TopicMetadata>,
): TopicMetadata {
return {
event_id: "evt-test-123",
trace_id: "trace-test-456",
topic: "test.topic",
subject: "test.app.test.topic",
occurred_at: new Date(),
deliveries: 1,
...overrides,
};
}
export function createMockMessage(data: Record<string, any>) {
return {
...data,
_test: true,
};
}
export async function waitForMessage(timeout: number = 5000): Promise<void> {
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
reject(new Error("Message timeout"));
}, timeout);
// Poll for message processing
const interval = setInterval(() => {
// Check if message processed
if (messageProcessed) {
clearTimeout(timer);
clearInterval(interval);
resolve();
}
}, 100);
});
}
Custom Matchers
// test/matchers/nats-matchers.ts
expect.extend({
toHavePublished(received: any, topic: string) {
const published = received.publishedMessages || [];
const found = published.some((msg: any) => msg.topic === topic);
return {
pass: found,
message: () =>
found
? `Expected not to have published to ${topic}`
: `Expected to have published to ${topic}, but found: ${published.map((m: any) => m.topic).join(", ")}`,
};
},
toHaveProcessed(received: any, eventId: string) {
const processed = received.processedEvents || [];
const found = processed.some((evt: any) => evt.event_id === eventId);
return {
pass: found,
message: () =>
found
? `Expected not to have processed event ${eventId}`
: `Expected to have processed event ${eventId}`,
};
},
});
Ruby
Test Helper Module
# spec/support/nats_helpers.rb
module NatsHelpers
def create_mock_context(**overrides)
double(
'MessageContext',
event_id: 'evt-test-123',
trace_id: 'trace-test-456',
topic: 'test.topic',
subject: 'test.app.test.topic',
occurred_at: Time.now,
deliveries: 1,
**overrides
)
end
def create_mock_message(**data)
data.stringify_keys.merge('_test' => true)
end
def wait_for_message(timeout: 5)
Timeout.timeout(timeout) do
loop do
return if message_processed?
sleep 0.1
end
end
rescue Timeout::Error
raise 'Message processing timeout'
end
end
RSpec.configure do |config|
config.include NatsHelpers, type: :subscriber
end
Custom RSpec Matchers
# spec/support/matchers/nats_matchers.rb
RSpec::Matchers.define :have_published_to do |topic|
match do |actual|
NatsPubsub::Testing.published_events.any? do |event|
event[:topic] == topic
end
end
failure_message do
published_topics = NatsPubsub::Testing.published_events.map { |e| e[:topic] }
"expected to have published to #{topic}, but published to: #{published_topics.join(', ')}"
end
end
RSpec::Matchers.define :have_processed_event do |event_id|
match do |actual|
ProcessedEvent.exists?(event_id: event_id)
end
failure_message do
"expected to have processed event #{event_id}"
end
end
Mocking Strategies
Mock NATS Connection
JavaScript/TypeScript
// test/mocks/nats-connection.ts
export function createMockConnection() {
const publishedMessages: any[] = [];
const mockJetstream = {
publish: jest.fn((subject, payload, options) => {
publishedMessages.push({ subject, payload, options });
return Promise.resolve();
}),
};
const mockConnection = {
ensureConnection: jest.fn().mockResolvedValue(undefined),
getJetStream: jest.fn().mockReturnValue(mockJetstream),
close: jest.fn().mockResolvedValue(undefined),
};
return {
mockConnection,
mockJetstream,
publishedMessages,
};
}
// Usage
const { mockConnection, publishedMessages } = createMockConnection();
jest.mock("../core/connection", () => mockConnection);
Ruby
# spec/support/mocks/nats_connection.rb
module NatsMocks
def mock_nats_connection
published_messages = []
mock_jetstream = instance_double('NATS::JetStream')
allow(mock_jetstream).to receive(:publish) do |subject, payload, opts|
published_messages << { subject: subject, payload: payload, options: opts }
end
allow(NatsPubsub::Connection).to receive(:connect!).and_return(mock_jetstream)
{ jetstream: mock_jetstream, published_messages: published_messages }
end
end
RSpec.configure do |config|
config.include NatsMocks
end
Testing Middleware
JavaScript/TypeScript
import { LoggingMiddleware } from "../middleware/logging-middleware";
describe("LoggingMiddleware", () => {
let middleware: LoggingMiddleware;
let mockLogger: any;
let mockNext: jest.Mock;
beforeEach(() => {
mockLogger = {
info: jest.fn(),
error: jest.fn(),
};
middleware = new LoggingMiddleware(mockLogger);
mockNext = jest.fn().mockResolvedValue(undefined);
});
it("logs before and after processing", async () => {
const event = { id: "123" };
const metadata = createMockMetadata({ subject: "test.subject" });
await middleware.call(event, metadata, mockNext);
expect(mockLogger.info).toHaveBeenCalledWith(
"Processing message",
expect.objectContaining({ subject: "test.subject" }),
);
expect(mockNext).toHaveBeenCalled();
expect(mockLogger.info).toHaveBeenCalledWith(
"Message processed",
expect.objectContaining({ subject: "test.subject" }),
);
});
it("logs errors", async () => {
const event = { id: "123" };
const metadata = createMockMetadata({ subject: "test.subject" });
const error = new Error("Processing failed");
mockNext.mockRejectedValue(error);
await expect(middleware.call(event, metadata, mockNext)).rejects.toThrow(
"Processing failed",
);
expect(mockLogger.error).toHaveBeenCalledWith(
"Message processing failed",
expect.objectContaining({
subject: "test.subject",
error: "Processing failed",
}),
);
});
});
Ruby
require 'rails_helper'
RSpec.describe LoggingMiddleware do
let(:middleware) { described_class.new }
let(:message) { { 'id' => '123' } }
let(:context) { create_mock_context(subject: 'test.subject') }
describe '#call' do
it 'logs before and after processing' do
expect(Rails.logger).to receive(:info).with('Processing message', hash_including(subject: 'test.subject'))
expect(Rails.logger).to receive(:info).with('Message processed', hash_including(subject: 'test.subject'))
middleware.call(message, context) do
# Handler logic
end
end
it 'logs errors' do
expect(Rails.logger).to receive(:error).with(
'Message processing failed',
hash_including(subject: 'test.subject', error: 'Processing failed')
)
expect {
middleware.call(message, context) do
raise 'Processing failed'
end
}.to raise_error('Processing failed')
end
end
end
Testing Error Handling
JavaScript/TypeScript
describe("Error Handling", () => {
it("retries on network errors", async () => {
let attempts = 0;
const subscriber = new TestSubscriber();
const message = { id: "123" };
const metadata = createMockMetadata();
// Mock handler to fail twice, then succeed
jest
.spyOn(subscriber as any, "processMessage")
.mockImplementation(async () => {
attempts++;
if (attempts < 3) {
throw new Error("ECONNREFUSED");
}
});
await subscriber.handle(message, metadata);
expect(attempts).toBe(3);
});
it("sends to DLQ after max retries", async () => {
const subscriber = new TestSubscriber();
const message = { id: "123" };
const metadata = createMockMetadata({ deliveries: 5 }); // Max retries
jest
.spyOn(subscriber as any, "processMessage")
.mockRejectedValue(new Error("Permanent failure"));
await expect(subscriber.handle(message, metadata)).rejects.toThrow(
"Permanent failure",
);
// Verify sent to DLQ
expect(mockJetstream.publish).toHaveBeenCalledWith(
"test.events.dlq",
expect.any(String),
expect.any(Object),
);
});
});
Best Practices
1. Use Descriptive Test Names
// Good
it("publishes order.created event with correct payload", async () => {});
it("retries processing on network errors up to max attempts", async () => {});
// Bad
it("works", async () => {});
it("test1", async () => {});
2. Test One Thing at a Time
// Good: Focused test
it("publishes message to correct subject", async () => {
await NatsPubsub.publish("order.created", { order_id: "ORD-123" });
expect(mockJetstream.publish).toHaveBeenCalledWith(
"test.app.order.created",
expect.any(String),
expect.any(Object),
);
});
// Bad: Testing multiple things
it("publishes and processes message", async () => {
// Publishing logic...
// Subscribing logic...
// Validation logic...
});
3. Use Factories for Test Data
// test/factories/message-factory.ts
export class MessageFactory {
static orderCreated(overrides?: any) {
return {
order_id: "ORD-123",
customer_id: "CUST-456",
total: 99.99,
...overrides,
};
}
static orderUpdated(overrides?: any) {
return {
order_id: "ORD-123",
status: "shipped",
...overrides,
};
}
}
// Usage
const message = MessageFactory.orderCreated({ total: 199.99 });
4. Clean Up After Tests
afterEach(async () => {
// Clear mocks
jest.clearAllMocks();
// Clear database
await Order.deleteMany({});
// Clear cache
await cache.flushAll();
});
CI/CD Integration
GitHub Actions
# .github/workflows/test.yml
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
services:
nats:
image: nats:latest
ports:
- 4222:4222
options: >-
-js
postgres:
image: postgres:15
env:
POSTGRES_PASSWORD: postgres
ports:
- 5432:5432
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: "20"
- name: Install dependencies
run: npm install
- name: Run tests
env:
NATS_URL: nats://localhost:4222
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/test
run: npm test
- name: Upload coverage
uses: codecov/codecov-action@v3
Docker Compose for Testing
# docker-compose.test.yml
version: "3.8"
services:
nats:
image: nats:latest
command: ["-js"]
ports:
- "4222:4222"
postgres:
image: postgres:15
environment:
POSTGRES_PASSWORD: postgres
ports:
- "5432:5432"
test:
build: .
depends_on:
- nats
- postgres
environment:
NATS_URL: nats://nats:4222
DATABASE_URL: postgresql://postgres:postgres@postgres:5432/test
command: npm test
Navigation
- Previous: Middleware System
- Next: Deployment Guide
- Related: