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

  1. Unit Tests - Individual components and functions
  2. Integration Tests - Service interactions and API endpoints
  3. Performance Tests - Load and stress testing
  4. End-to-End Tests - Complete user workflows
  5. 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: 5

Performance 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