Testing Strategies
Complete guide for testing your integration with the MONCRENEAU API.
1. Unit Tests
Test critical functions in isolation.
Example: Webhook Signature Verification
const crypto = require('crypto');
const { verifyWebhookSignature } = require('../lib/webhooks');
describe('verifyWebhookSignature', () => {
const secret = 'test_secret_key';
const payload = JSON.stringify({
id: 'evt_123',
type: 'appointment.created'
});
it('should return true for valid signature', () => {
const signature = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
const result = verifyWebhookSignature(payload, signature, secret);
expect(result).toBe(true);
});
it('should return false for invalid signature', () => {
const result = verifyWebhookSignature(payload, 'sha256=invalid', secret);
expect(result).toBe(false);
});
it('should return false for missing signature', () => {
const result = verifyWebhookSignature(payload, null, secret);
expect(result).toBe(false);
});
});
Example: Credit Management
const { canCreateAppointment } = require('../lib/credits');
describe('canCreateAppointment', () => {
it('should allow creation with sufficient credits', async () => {
const org = { id: 1, availableCredits: 10 };
const result = await canCreateAppointment(org);
expect(result).toBe(true);
});
it('should block creation with insufficient credits', async () => {
const org = { id: 1, availableCredits: 0 };
const result = await canCreateAppointment(org);
expect(result).toBe(false);
});
});
2. Integration Tests
Test with the real API in TEST environment.
Configuration
# test/setup.js
process.env.MONCRENEAU_API_KEY = process.env.MONCRENEAU_API_KEY;
process.env.MONCRENEAU_API_URL = 'https://mc-prd.duckdns.org/api/v1';
Example: Appointment Creation
const axios = require('axios');
describe('API Integration Tests', () => {
const api = axios.create({
baseURL: process.env.MONCRENEAU_API_URL,
headers: {
'Authorization': `Bearer ${process.env.MONCRENEAU_API_KEY}`,
'Content-Type': 'application/json'
}
});
it('should create appointment', async () => {
const response = await api.post('/appointments', {
departmentId: 5,
dateTime: '2026-02-15T10:00:00',
name: 'Jean Test'
});
expect(response.status).toBe(201);
expect(response.data).toHaveProperty('id');
expect(response.data.status).toBe('CONFIRMED');
});
it('should fail with invalid data', async () => {
try {
await api.post('/appointments', {
// Missing required fields
departmentId: 5
});
fail('Should have thrown error');
} catch (error) {
expect(error.response.status).toBe(400);
expect(error.response.data).toHaveProperty('errors');
}
});
it('should fail with insufficient credits', async () => {
// Organization with 0 credits
try {
await api.post('/appointments', {
departmentId: 999, // Dept without credits
dateTime: '2026-02-15T10:00:00',
name: 'Test User'
});
fail('Should have thrown error');
} catch (error) {
expect(error.response.status).toBe(402);
expect(error.response.data.error.code).toBe('INSUFFICIENT_CREDITS');
}
});
});
3. Webhook Tests
Option A: Local Server with ngrok
# 1. Install ngrok
npm install -g ngrok
# 2. Start your local server
npm start # Port 3000
# 3. Expose with ngrok
ngrok http 3000
# 4. Configure webhook URL
# https://abc123.ngrok.io/webhooks/moncreneau
Option B: Unit Tests with Mocks
const request = require('supertest');
const app = require('../app');
const crypto = require('crypto');
describe('Webhook Endpoint', () => {
const secret = process.env.WEBHOOK_SECRET;
function signPayload(payload) {
const signature = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
return `sha256=${signature}`;
}
it('should process appointment.created event', async () => {
const payload = JSON.stringify({
id: 'evt_test_123',
type: 'appointment.created',
created: new Date().toISOString(),
data: {
object: {
id: 123,
status: 'pending',
userPhone: '+224622000000'
}
}
});
const signature = signPayload(payload);
const response = await request(app)
.post('/webhooks/moncreneau')
.set('Content-Type', 'application/json')
.set('X-Moncreneau-Signature', signature)
.set('X-Moncreneau-Event', 'appointment.created')
.send(payload);
expect(response.status).toBe(200);
// Verify that event was processed
const processed = await db.processedEvents.findOne({
eventId: 'evt_test_123'
});
expect(processed).toBeTruthy();
});
it('should reject invalid signature', async () => {
const payload = JSON.stringify({ id: 'evt_test' });
const response = await request(app)
.post('/webhooks/moncreneau')
.set('X-Moncreneau-Signature', 'sha256=invalid_signature')
.send(payload);
expect(response.status).toBe(401);
});
it('should be idempotent', async () => {
const payload = JSON.stringify({
id: 'evt_test_456',
type: 'appointment.created'
});
const signature = signPayload(payload);
// First call
await request(app)
.post('/webhooks/moncreneau')
.set('X-Moncreneau-Signature', signature)
.send(payload);
// Second call (duplicate)
const response = await request(app)
.post('/webhooks/moncreneau')
.set('X-Moncreneau-Signature', signature)
.send(payload);
expect(response.status).toBe(200);
// Verify only one record exists
const count = await db.processedEvents.countDocuments({
eventId: 'evt_test_456'
});
expect(count).toBe(1);
});
});
4. Load Testing
Verify that your integration can handle production volume.
With Artillery
# load-test.yml
config:
target: 'https://mc-prd.duckdns.org/api/v1'
phases:
- duration: 60
arrivalRate: 10 # 10 requests/second
defaults:
headers:
Authorization: 'Bearer {{ $processEnvironment.MONCRENEAU_API_KEY }}'
scenarios:
- name: 'Create appointments'
flow:
- post:
url: '/appointments'
json:
departmentId: 5
dateTime: '2026-02-15T{{ $randomNumber(10, 17) }}:00:00'
name: 'Test User {{ $randomNumber(1, 1000) }}'
# Run the test
artillery run load-test.yml
With k6
// load-test.js
import http from 'k6/http';
import { check, sleep } from 'k6';
export const options = {
vus: 10, // 10 virtual users
duration: '30s',
};
export default function () {
const payload = JSON.stringify({
departmentId: 5,
dateTime: new Date(Date.now() + 86400000).toISOString(),
name: 'Test User'
});
const params = {
headers: {
'Authorization': `Bearer ${__ENV.MONCRENEAU_API_KEY}`,
'Content-Type': 'application/json',
},
};
const res = http.post(
'https://mc-prd.duckdns.org/api/v1/appointments',
payload,
params
);
check(res, {
'status is 201': (r) => r.status === 201,
'response time < 500ms': (r) => r.timings.duration < 500,
});
sleep(1);
}
<ApiCode language="bash">{`
# Run
k6 run load-test.js
`}</ApiCode>
## 5. Contract Testing
Verify that your code respects the API contract.
### With Pact
```javascript
// consumer.pact.test.js
const { Pact } = require('@pact-foundation/pact');
const { createAppointment } = require('../lib/api');
describe('MONCRENEAU API Contract', () => {
const provider = new Pact({
consumer: 'YourApp',
provider: 'MONCRENEAU',
port: 8080
});
beforeAll(() => provider.setup());
afterEach(() => provider.verify());
afterAll(() => provider.finalize());
it('should create appointment', async () => {
await provider.addInteraction({
state: 'organization has credits',
uponReceiving: 'a request to create appointment',
withRequest: {
method: 'POST',
path: '/v1/appointments',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer test_key'
},
body: {
departmentId: 5,
dateTime: '2026-02-15T10:00:00',
name: 'Jean Dupont'
}
},
willRespondWith: {
status: 201,
headers: { 'Content-Type': 'application/json' },
body: {
id: Pact.like(123),
status: 'CONFIRMED',
departmentId: 5,
name: 'Jean Dupont'
}
}
});
const result = await createAppointment({
departmentId: 5,
dateTime: '2026-02-15T10:00:00',
name: 'Jean Dupont'
});
expect(result.id).toBeDefined();
expect(result.status).toBe('CONFIRMED');
});
});
6. End-to-End Tests
Complete user scenario.
describe('E2E: Complete Appointment Flow', () => {
it('should complete full appointment cycle', async () => {
// 1. Get available slots
const slotsResponse = await api.get('/departments/5/availability', {
params: {
startDate: '2026-02-01',
endDate: '2026-02-28'
}
});
expect(slotsResponse.status).toBe(200);
const slot = slotsResponse.data.availability[0].slots[0];
// 2. Create appointment
const createResponse = await api.post('/appointments', {
departmentId: 5,
dateTime: slot.dateTime,
name: 'Jean Test'
});
expect(createResponse.status).toBe(201);
const appointmentId = createResponse.data.id;
// 3. Check status
const statusResponse = await api.get(`/appointments/${appointmentId}`);
expect(statusResponse.data.status).toBe('CONFIRMED');
// 4. Cancel appointment
const cancelResponse = await api.delete(`/appointments/${appointmentId}`);
expect(cancelResponse.status).toBe(200);
// 5. Verify cancellation
const finalStatus = await api.get(`/appointments/${appointmentId}`);
expect(finalStatus.data.status).toBe('cancelled');
});
});
Best Practices
✅ Do
- Use TEST keys for all tests
- Clean up test data after each test
- Test error cases (not just the happy path)
- Automate tests in CI/CD
- Test webhook idempotence
- Measure code coverage (> 80%)
❌ Don't
- Use LIVE keys in tests
- Hardcode test IDs
- Ignore failing tests
- Only test success scenarios
- Forget to test timeouts
Test Checklist
Before deployment:
- Unit tests pass (> 80% coverage)
- Integration tests with TEST API OK
- Webhook tests with ngrok OK
- Load tests validated (100 req/min)
- E2E tests complete OK
- All error cases tested
- Test documentation up to date
- CI/CD runs tests automatically