Testing
Comprehensive testing strategies and examples for the Esto platform
Testing
This guide covers comprehensive testing strategies for the Esto platform, including unit tests, integration tests, performance tests, and end-to-end testing.
🧪 Testing Strategy Overview
Testing Pyramid
Our testing approach follows the testing pyramid:
E2E Tests (Few)
/\
/ \
Integration Tests (Some)
/\
/ \
Unit Tests (Many)Test Types
- Unit Tests - Individual components and functions
- Integration Tests - Service interactions and API endpoints
- Performance Tests - Load and stress testing
- End-to-End Tests - Complete user workflows
- Security Tests - Authentication and authorization
🔧 Unit Testing
Contact Service Tests
import { Test, TestingModule } from '@nestjs/testing';
import { ContactService } from '../contact.service';
import { PrismaService } from '../../infrastructure/prisma/prisma.service';
describe('ContactService', () => {
let service: ContactService;
let prisma: PrismaService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
ContactService,
{
provide: PrismaService,
useValue: {
contact: {
create: jest.fn(),
findUnique: jest.fn(),
findMany: jest.fn(),
update: jest.fn(),
delete: jest.fn(),
count: jest.fn(),
},
},
},
],
}).compile();
service = module.get<ContactService>(ContactService);
prisma = module.get<PrismaService>(PrismaService);
});
describe('createContact', () => {
it('should create a new contact successfully', async () => {
const contactData = {
email: 'test@example.com',
fullName: 'Test User',
phone: '+213 123 456 789',
age: 30,
familySituation: 'single',
wilaya: 'Alger',
minBudgets: [50000000],
maxBudgets: [70000000],
minSurface: [80],
maxSurface: [100],
preferedPropertyType: 'apartment',
bedrooms: 2,
options: ['parking'],
};
const expectedContact = {
id: 'c_123456',
...contactData,
createdAt: new Date(),
updatedAt: new Date(),
};
jest.spyOn(prisma.contact, 'create').mockResolvedValue(expectedContact);
const result = await service.createContact(contactData);
expect(result.contact).toEqual(expectedContact);
expect(result.message).toBe('Contact created successfully');
});
it('should throw ConflictException when contact already exists', async () => {
const contactData = {
email: 'existing@example.com',
fullName: 'Existing User',
phone: '+213 123 456 789',
age: 30,
familySituation: 'single',
wilaya: 'Alger',
minBudgets: [50000000],
maxBudgets: [70000000],
minSurface: [80],
maxSurface: [100],
preferedPropertyType: 'apartment',
bedrooms: 2,
options: ['parking'],
};
jest.spyOn(prisma.contact, 'findUnique').mockResolvedValue({
id: 'c_existing',
...contactData,
createdAt: new Date(),
updatedAt: new Date(),
});
await expect(service.createContact(contactData)).rejects.toThrow('Contact with this email already exists');
});
});
describe('listContacts', () => {
it('should return paginated contacts', async () => {
const mockContacts = [
{
id: 'c_123456',
email: 'test1@example.com',
fullName: 'Test User 1',
phone: '+213 123 456 789',
age: 30,
familySituation: 'single',
children: null,
wilaya: 'Alger',
baladiya: 'Hydra',
minBudgets: [50000000],
maxBudgets: [70000000],
minSurface: [80],
maxSurface: [100],
preferedPropertyType: 'apartment',
bedrooms: 2,
options: ['parking'],
createdAt: new Date(),
updatedAt: new Date(),
},
];
jest.spyOn(prisma.contact, 'findMany').mockResolvedValue(mockContacts);
jest.spyOn(prisma.contact, 'count').mockResolvedValue(1);
const result = await service.listContacts({
page: 1,
limit: 10,
});
expect(result.contacts).toEqual(mockContacts);
expect(result.total).toBe(1);
expect(result.page).toBe(1);
expect(result.limit).toBe(10);
expect(result.totalPages).toBe(1);
});
});
});Matcher Service Tests (Rust)
#[cfg(test)]
mod tests {
use super::*;
use crate::types::{Contact, Property, ContactMatch};
#[tokio::test]
async fn test_calculate_budget_score() {
let contact = Contact {
min_budgets: vec![50000000.0, 80000000.0],
max_budgets: vec![70000000.0, 100000000.0],
// ... other fields
};
let property = Property {
price: 75000000.0,
// ... other fields
};
let score = calculate_budget_score(&contact, &property);
assert!(score > 0.8);
}
#[tokio::test]
async fn test_calculate_location_score() {
let contact = Contact {
wilaya: "Alger".to_string(),
baladiya: Some("Hydra".to_string()),
// ... other fields
};
let property = Property {
wilaya: "Alger".to_string(),
baladiya: "Hydra".to_string(),
// ... other fields
};
let score = calculate_location_score(&contact, &property);
assert_eq!(score, 1.0);
}
#[tokio::test]
async fn test_matcher_service_integration() {
let config = MatcherConfig::default();
let matcher = MatcherService::new(config).await.unwrap();
let property_embedding = vec![0.1, 0.2, 0.3, 0.4, 0.5];
let property_id = "test_property_123";
let result = matcher.match_property(property_id, &property_embedding).await;
assert!(result.is_ok());
let matches = result.unwrap();
assert!(matches.len() <= config.max_matches);
}
}AI Agents Tests (Python)
import pytest
from unittest.mock import Mock, patch
from src.agents.facebook_marketing_agent import generate_facebook_strategy
from src.models.property import Property
class TestFacebookMarketingAgent:
@patch('src.agents.facebook_marketing_agent.groq')
def test_generate_facebook_strategy(self, mock_groq):
# Mock property
property = Property(
id="e_123456",
title="Modern Apartment in Hydra",
property_type="apartment",
location="Alger, Hydra",
price=75000000,
surface=120,
rooms=3,
description="Beautiful apartment with sea view"
)
# Mock AI response
mock_response = Mock()
mock_response.choices = [Mock()]
mock_response.choices[0].message.content = """
Target Audience: Young professionals aged 25-35
Ad Format: Carousel ads
Budget: 5000-10000 DZD per day
Creative: Highlight sea view and modern amenities
"""
mock_groq.Groq.return_value.chat.completions.create.return_value = mock_response
# Test function
strategy = generate_facebook_strategy(property)
# Assertions
assert "Target Audience" in strategy
assert "Ad Format" in strategy
assert "Budget" in strategy
assert "Creative" in strategy
# Verify AI call
mock_groq.Groq.assert_called_once()
mock_groq.Groq.return_value.chat.completions.create.assert_called_once()
@patch('src.agents.property_comparator.Agent')
def test_compare_properties(self, mock_agent):
# Mock properties
property1 = Property(
id="e_123456",
title="Apartment A",
property_type="apartment",
location="Alger",
price=70000000,
surface=100,
rooms=2,
description="Nice apartment"
)
property2 = Property(
id="e_123457",
title="Apartment B",
property_type="apartment",
location="Oran",
price=80000000,
surface=120,
rooms=3,
description="Larger apartment"
)
# Mock AI response
mock_agent_instance = Mock()
mock_agent_instance.run.return_value.content = """
Comparison Summary:
- Property A is cheaper but smaller
- Property B is more expensive but larger
- Both are good options depending on needs
"""
mock_agent.return_value = mock_agent_instance
# Test function
comparison = compare_properties(property1, property2)
# Assertions
assert "Comparison Summary" in comparison
assert "Property A" in comparison
assert "Property B" in comparison🔗 Integration Testing
API Endpoint Tests
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from '../app.module';
describe('Contact API (e2e)', () => {
let app: INestApplication;
beforeEach(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
await app.init();
});
afterEach(async () => {
await app.close();
});
describe('/contacts (POST)', () => {
it('should create a new contact', () => {
const contactData = {
email: 'test@example.com',
fullName: 'Test User',
phone: '+213 123 456 789',
age: 30,
familySituation: 'single',
wilaya: 'Alger',
minBudgets: [50000000],
maxBudgets: [70000000],
minSurface: [80],
maxSurface: [100],
preferedPropertyType: 'apartment',
bedrooms: 2,
options: ['parking'],
};
return request(app.getHttpServer())
.post('/contacts')
.send(contactData)
.expect(201)
.expect((res) => {
expect(res.body.contact).toBeDefined();
expect(res.body.contact.email).toBe(contactData.email);
expect(res.body.message).toBe('Contact created successfully');
});
});
it('should return 409 when contact already exists', () => {
const contactData = {
email: 'existing@example.com',
fullName: 'Existing User',
phone: '+213 123 456 789',
age: 30,
familySituation: 'single',
wilaya: 'Alger',
minBudgets: [50000000],
maxBudgets: [70000000],
minSurface: [80],
maxSurface: [100],
preferedPropertyType: 'apartment',
bedrooms: 2,
options: ['parking'],
};
return request(app.getHttpServer()).post('/contacts').send(contactData).expect(409);
});
});
describe('/contacts (GET)', () => {
it('should return paginated contacts', () => {
return request(app.getHttpServer())
.get('/contacts?page=1&limit=10')
.expect(200)
.expect((res) => {
expect(res.body.contacts).toBeDefined();
expect(Array.isArray(res.body.contacts)).toBe(true);
expect(res.body.total).toBeDefined();
expect(res.body.page).toBe(1);
expect(res.body.limit).toBe(10);
});
});
it('should filter contacts by wilaya', () => {
return request(app.getHttpServer())
.get('/contacts?wilaya=Alger')
.expect(200)
.expect((res) => {
expect(res.body.contacts).toBeDefined();
res.body.contacts.forEach((contact: any) => {
expect(contact.wilaya).toBe('Alger');
});
});
});
});
});Matcher Service Integration Tests
#[cfg(test)]
mod integration_tests {
use super::*;
use crate::qdrant::client::QdrantClient;
use crate::db::postgres::PostgresClient;
#[tokio::test]
async fn test_full_matching_workflow() {
// Setup test data
let config = MatcherConfig {
max_matches: 10,
min_similarity_threshold: 0.6,
..Default::default()
};
let matcher = MatcherService::new(config).await.unwrap();
// Create test property embedding
let property_embedding = vec![0.1, 0.2, 0.3, 0.4, 0.5];
let property_id = "test_property_123";
// Test matching
let matches = matcher.match_property(property_id, &property_embedding).await.unwrap();
// Assertions
assert!(matches.len() <= config.max_matches);
for match_result in &matches {
assert!(match_result.score >= config.min_similarity_threshold);
assert!(!match_result.explanation.is_empty());
}
}
#[tokio::test]
async fn test_health_check() {
let config = MatcherConfig::default();
let matcher = MatcherService::new(config).await.unwrap();
let health_status = matcher.health_check().await.unwrap();
assert!(health_status);
}
}âš¡ Performance Testing
Load Testing with Artillery
# artillery-config.yml
config:
target: 'http://localhost:8000'
phases:
- duration: 60
arrivalRate: 10
name: 'Warm up'
- duration: 300
arrivalRate: 50
name: 'Sustained load'
- duration: 60
arrivalRate: 100
name: 'Peak load'
defaults:
headers:
Authorization: 'Bearer test_api_key'
scenarios:
- name: 'Contact Management'
weight: 40
flow:
- post:
url: '/contacts'
json:
email: '{{ $randomString() }}@example.com'
fullName: 'Test User'
phone: '+213 123 456 789'
age: 30
familySituation: 'single'
wilaya: 'Alger'
minBudgets: [50000000]
maxBudgets: [70000000]
minSurface: [80]
maxSurface: [100]
preferedPropertyType: 'apartment'
bedrooms: 2
options: ['parking']
- get:
url: '/contacts?page=1&limit=10'
- think: 1
- name: 'Property Matching'
weight: 30
flow:
- post:
url: '/matcher/match'
json:
property_id: 'test_property_123'
embedding: [0.1, 0.2, 0.3, 0.4, 0.5]
- think: 2
- name: 'Marketing Strategy'
weight: 20
flow:
- post:
url: '/agents/facebook-strategy'
json:
property_id: 'test_property_123'
- think: 3
- name: 'PDF Generation'
weight: 10
flow:
- post:
url: '/documents/quote'
json:
property_id: 'test_property_123'
contact_id: 'test_contact_123'
quote_details:
valid_until: '2024-02-15'
agent_name: 'Test Agent'
- think: 5Performance Test Script
// performance-test.js
import { EstoClient } from '@esto/sdk';
const client = new EstoClient({
apiKey: process.env.ESTO_API_KEY,
baseUrl: process.env.ESTO_API_BASE_URL,
});
async function runPerformanceTest() {
console.log('Starting performance test...');
const startTime = Date.now();
const results = {
totalRequests: 0,
successfulRequests: 0,
failedRequests: 0,
responseTimes: [],
};
// Test contact creation
for (let i = 0; i < 100; i++) {
const requestStart = Date.now();
try {
await client.contacts.create({
email: `test${i}@example.com`,
fullName: `Test User ${i}`,
phone: '+213 123 456 789',
age: 30,
familySituation: 'single',
wilaya: 'Alger',
minBudgets: [50000000],
maxBudgets: [70000000],
minSurface: [80],
maxSurface: [100],
preferedPropertyType: 'apartment',
bedrooms: 2,
options: ['parking'],
});
results.successfulRequests++;
} catch (error) {
results.failedRequests++;
console.error(`Request ${i} failed:`, error.message);
}
results.totalRequests++;
results.responseTimes.push(Date.now() - requestStart);
}
const endTime = Date.now();
const totalTime = endTime - startTime;
// Calculate statistics
const avgResponseTime = results.responseTimes.reduce((a, b) => a + b, 0) / results.responseTimes.length;
const minResponseTime = Math.min(...results.responseTimes);
const maxResponseTime = Math.max(...results.responseTimes);
const requestsPerSecond = results.totalRequests / (totalTime / 1000);
console.log('Performance Test Results:');
console.log(`Total Requests: ${results.totalRequests}`);
console.log(`Successful: ${results.successfulRequests}`);
console.log(`Failed: ${results.failedRequests}`);
console.log(`Success Rate: ${((results.successfulRequests / results.totalRequests) * 100).toFixed(2)}%`);
console.log(`Average Response Time: ${avgResponseTime.toFixed(2)}ms`);
console.log(`Min Response Time: ${minResponseTime}ms`);
console.log(`Max Response Time: ${maxResponseTime}ms`);
console.log(`Requests per Second: ${requestsPerSecond.toFixed(2)}`);
}
runPerformanceTest().catch(console.error);🔒 Security Testing
Authentication Tests
describe('Authentication', () => {
it('should reject requests without API key', () => {
return request(app.getHttpServer()).post('/contacts').send(contactData).expect(401);
});
it('should reject requests with invalid API key', () => {
return request(app.getHttpServer())
.post('/contacts')
.set('Authorization', 'Bearer invalid_key')
.send(contactData)
.expect(401);
});
it('should accept requests with valid API key', () => {
return request(app.getHttpServer())
.post('/contacts')
.set('Authorization', 'Bearer valid_api_key')
.send(contactData)
.expect(201);
});
});Input Validation Tests
describe('Input Validation', () => {
it('should reject invalid email format', () => {
const invalidContact = {
...contactData,
email: 'invalid-email',
};
return request(app.getHttpServer())
.post('/contacts')
.send(invalidContact)
.expect(400)
.expect((res) => {
expect(res.body.error.code).toBe('VALIDATION_ERROR');
expect(res.body.error.details.field).toBe('email');
});
});
it('should reject invalid phone format', () => {
const invalidContact = {
...contactData,
phone: '123456789',
};
return request(app.getHttpServer())
.post('/contacts')
.send(invalidContact)
.expect(400)
.expect((res) => {
expect(res.body.error.code).toBe('VALIDATION_ERROR');
expect(res.body.error.details.field).toBe('phone');
});
});
it('should reject negative age', () => {
const invalidContact = {
...contactData,
age: -5,
};
return request(app.getHttpServer()).post('/contacts').send(invalidContact).expect(400);
});
});🧪 End-to-End Testing
Complete Workflow Test
describe('Complete Workflow', () => {
it('should handle full property-client matching workflow', async () => {
// 1. Create contact
const contactResponse = await request(app.getHttpServer()).post('/contacts').send(contactData).expect(201);
const contactId = contactResponse.body.contact.id;
// 2. Create property
const propertyResponse = await request(app.getHttpServer()).post('/estates').send(propertyData).expect(201);
const propertyId = propertyResponse.body.estate.id;
// 3. Get AI recommendations
const recommendationsResponse = await request(app.getHttpServer())
.post('/matcher/match')
.send({
property_id: propertyId,
embedding: [0.1, 0.2, 0.3, 0.4, 0.5],
})
.expect(200);
expect(recommendationsResponse.body.matches_found).toBeGreaterThan(0);
// 4. Generate marketing strategy
const strategyResponse = await request(app.getHttpServer())
.post('/agents/facebook-strategy')
.send({
property_id: propertyId,
})
.expect(200);
expect(strategyResponse.body.strategy).toBeDefined();
// 5. Generate PDF quote
const quoteResponse = await request(app.getHttpServer())
.post('/documents/quote')
.send({
property_id: propertyId,
contact_id: contactId,
quote_details: {
valid_until: '2024-02-15',
agent_name: 'Test Agent',
},
})
.expect(200);
expect(quoteResponse.body.pdf_url).toBeDefined();
});
});📊 Test Coverage
Coverage Configuration
// jest.config.js
module.exports = {
collectCoverageFrom: [
'src/**/*.{ts,tsx}',
'!src/**/*.d.ts',
'!src/**/*.test.{ts,tsx}',
'!src/**/*.spec.{ts,tsx}'
],
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80
}
},
coverageReporters: ['text', 'lcov', 'html'],
testEnvironment: 'node',
testMatch: [
'**/__tests__/**/*.{ts,tsx}',
'**/*.{test,spec}.{ts,tsx}'
]
};Coverage Report
# Run tests with coverage
npm run test:coverage
# Coverage output example:
# PASS src/contact/contact.service.spec.ts
# PASS src/estate/estate.service.spec.ts
# PASS src/matcher/matcher.service.spec.ts
#
# Test Suites: 3 passed, 3 total
# Tests: 15 passed, 15 total
# Snapshots: 0 total
# Time: 5.234 s
#
# ----------|---------|----------|---------|---------|-------------------
# File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
# ----------|---------|----------|---------|---------|-------------------
# All files | 85.23 | 82.15 | 87.65 | 85.23 |
# ----------|---------|----------|---------|---------|-------------------🚀 CI/CD Testing
GitHub Actions Workflow
# .github/workflows/test.yml
name: Tests
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:13
env:
POSTGRES_PASSWORD: postgres
POSTGRES_DB: esto_test
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run unit tests
run: npm run test:unit
- name: Run integration tests
run: npm run test:integration
env:
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/esto_test
- name: Run E2E tests
run: npm run test:e2e
- name: Upload coverage
uses: codecov/codecov-action@v3
with:
file: ./coverage/lcov.info
flags: unittests
name: codecov-umbrella
fail_ci_if_error: true
performance:
runs-on: ubuntu-latest
needs: test
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
- name: Install dependencies
run: npm ci
- name: Run performance tests
run: npm run test:performance📚 Next Steps
- API Reference - Complete API documentation
- Data Models - Entity schemas and relationships
- Integration Guide - Implementation examples
- PDF Generation - Document creation features