Testing Your Document Q&A Application
Overview
Comprehensive testing is essential for ensuring the reliability and quality of your Document Q&A application. This tutorial covers various testing strategies for both frontend and backend components, helping you build a robust application that handles edge cases gracefully.
Frontend Testing
1. Setting Up Jest with React Testing Library
First, set up Jest and React Testing Library in your Next.js project:
# Install dependencies
npm install --save-dev jest @testing-library/react @testing-library/jest-dom @testing-library/user-event jest-environment-jsdom
# Create jest.config.js
module.exports = {
testEnvironment: 'jsdom',
setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
testPathIgnorePatterns: ['<rootDir>/.next/', '<rootDir>/node_modules/'],
moduleNameMapper: {
'^@/components/(.*)$': '<rootDir>/src/components/$1',
'^@/lib/(.*)$': '<rootDir>/src/lib/$1',
},
};
# Create jest.setup.js
import '@testing-library/jest-dom';
# Add to package.json
"scripts": {
"test": "jest",
"test:watch": "jest --watch"
}2. Component Testing
Test your React components to ensure they render correctly and handle user interactions:
// src/components/QuestionInput.test.js
import React from 'react';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import QuestionInput from './QuestionInput';
import { client } from '../lib/document-qa';
// Mock the document-qa client
jest.mock('../lib/document-qa', () => ({
client: {
askQuestion: jest.fn(),
},
}));
describe('QuestionInput Component', () => {
beforeEach(() => {
// Reset mocks before each test
jest.clearAllMocks();
});
test('renders the question input form', () => {
render(<QuestionInput documentId="test-doc-123" />);
// Check if the input field and button are rendered
expect(screen.getByPlaceholderText(/ask a question/i)).toBeInTheDocument();
expect(screen.getByRole('button', { name: /ask question/i })).toBeInTheDocument();
});
test('handles question submission', async () => {
// Mock the API response
client.askQuestion.mockResolvedValueOnce('This is the answer to your question.');
render(<QuestionInput documentId="test-doc-123" />);
// Type a question
const inputElement = screen.getByPlaceholderText(/ask a question/i);
await userEvent.type(inputElement, 'What is the main topic?');
// Submit the form
const buttonElement = screen.getByRole('button', { name: /ask question/i });
fireEvent.click(buttonElement);
// Check if the API was called with correct parameters
expect(client.askQuestion).toHaveBeenCalledWith({
documentId: 'test-doc-123',
question: 'What is the main topic?',
});
// Wait for the answer to be displayed
await waitFor(() => {
expect(screen.getByText('This is the answer to your question.')).toBeInTheDocument();
});
});
test('displays error message when API call fails', async () => {
// Mock the API error
client.askQuestion.mockRejectedValueOnce({
response: { data: { detail: 'Failed to process question' } },
});
render(<QuestionInput documentId="test-doc-123" />);
// Type a question and submit
const inputElement = screen.getByPlaceholderText(/ask a question/i);
await userEvent.type(inputElement, 'What is the main topic?');
fireEvent.click(screen.getByRole('button', { name: /ask question/i }));
// Check if error message is displayed
await waitFor(() => {
expect(screen.getByText('Failed to process question')).toBeInTheDocument();
});
});
});3. Testing File Upload Components
Test file upload components with mocked file objects:
// src/components/FileUpload.test.js
import React from 'react';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import FileUpload from './FileUpload';
import apiClient from '../lib/api-client';
// Mock the API client
jest.mock('../lib/api-client', () => ({
post: jest.fn(),
}));
// Mock react-dropzone
jest.mock('react-dropzone', () => ({
useDropzone: () => ({
getRootProps: () => ({
onClick: jest.fn(),
}),
getInputProps: () => ({}),
isDragActive: false,
acceptedFiles: [],
}),
}));
describe('FileUpload Component', () => {
beforeEach(() => {
jest.clearAllMocks();
});
test('renders the dropzone', () => {
render(<FileUpload onDocumentUploaded={() => {}} />);
expect(screen.getByText(/drag and drop your file here/i)).toBeInTheDocument();
});
test('handles file upload', async () => {
// Mock successful API response
apiClient.post.mockResolvedValueOnce({
data: { document_id: 'test-doc-123' },
});
const onDocumentUploaded = jest.fn();
render(<FileUpload onDocumentUploaded={onDocumentUploaded} />);
// Create a mock file
const file = new File(['file content'], 'test.pdf', { type: 'application/pdf' });
// Simulate file drop
const dropzone = screen.getByText(/drag and drop your file here/i).closest('div');
fireEvent.drop(dropzone, {
dataTransfer: {
files: [file],
},
});
// Wait for the upload to complete
await waitFor(() => {
expect(apiClient.post).toHaveBeenCalledWith('/upload', expect.any(FormData), expect.any(Object));
expect(onDocumentUploaded).toHaveBeenCalledWith('test-doc-123');
});
});
test('displays error message when upload fails', async () => {
// Mock API error
apiClient.post.mockRejectedValueOnce({
response: { data: { detail: 'Invalid file format' } },
});
render(<FileUpload onDocumentUploaded={() => {}} />);
// Create a mock file
const file = new File(['file content'], 'test.txt', { type: 'text/plain' });
// Simulate file drop
const dropzone = screen.getByText(/drag and drop your file here/i).closest('div');
fireEvent.drop(dropzone, {
dataTransfer: {
files: [file],
},
});
// Check if error message is displayed
await waitFor(() => {
expect(screen.getByText('Invalid file format')).toBeInTheDocument();
});
});
});Integration Testing
1. Testing API Integration
Test the integration between your frontend and API:
// src/tests/integration/document-qa.test.js
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import App from '../../app/page';
import apiClient from '../../lib/api-client';
// Mock the API client
jest.mock('../../lib/api-client', () => ({
post: jest.fn(),
}));
describe('Document Q&A Integration', () => {
beforeEach(() => {
jest.clearAllMocks();
});
test('complete document upload and question flow', async () => {
// Mock successful upload
apiClient.post.mockResolvedValueOnce({
data: { document_id: 'test-doc-123' },
});
// Mock successful question response
apiClient.post.mockResolvedValueOnce({
data: { answer: 'This is the answer to your question.' },
});
render(<App />);
// Step 1: Upload a document
const file = new File(['file content'], 'test.pdf', { type: 'application/pdf' });
const dropzone = screen.getByText(/drag and drop your file here/i).closest('div');
fireEvent.drop(dropzone, {
dataTransfer: {
files: [file],
},
});
// Wait for upload to complete and UI to update
await waitFor(() => {
expect(screen.getByText(/ask questions/i)).toBeInTheDocument();
});
// Step 2: Ask a question
const inputElement = screen.getByPlaceholderText(/ask a question/i);
await userEvent.type(inputElement, 'What is the main topic?');
fireEvent.click(screen.getByRole('button', { name: /ask question/i }));
// Check if the answer is displayed
await waitFor(() => {
expect(screen.getByText('This is the answer to your question.')).toBeInTheDocument();
});
// Verify API calls
expect(apiClient.post).toHaveBeenCalledTimes(2);
expect(apiClient.post.mock.calls[0][0]).toBe('/upload');
expect(apiClient.post.mock.calls[1][0]).toBe('/ask');
expect(apiClient.post.mock.calls[1][1]).toEqual({
document_id: 'test-doc-123',
question: 'What is the main topic?',
});
});
});2. Mock Service Worker
Use Mock Service Worker (MSW) to intercept and mock API requests:
# Install MSW
npm install --save-dev msw
// src/mocks/handlers.js
import { rest } from 'msw';
export const handlers = [
// Mock document upload endpoint
rest.post('/api/upload', (req, res, ctx) => {
return res(
ctx.status(200),
ctx.json({
document_id: 'mock-doc-123',
message: 'Document uploaded successfully',
})
);
}),
// Mock question answering endpoint
rest.post('/api/ask', (req, res, ctx) => {
const { question } = req.body;
return res(
ctx.status(200),
ctx.json({
answer: `This is a mock answer to: "${question}"`,
})
);
}),
];
// src/mocks/server.js
import { setupServer } from 'msw/node';
import { handlers } from './handlers';
export const server = setupServer(...handlers);
// src/mocks/browser.js (for browser usage)
import { setupWorker } from 'msw';
import { handlers } from './handlers';
export const worker = setupWorker(...handlers);
// Add to jest.setup.js
import { server } from './src/mocks/server';
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());Backend Testing
1. FastAPI Endpoint Testing
Test your FastAPI endpoints using pytest:
# Install pytest and testing dependencies
pip install pytest pytest-asyncio httpx
# api/tests/test_documents.py
import pytest
from fastapi.testclient import TestClient
from ..main import app
from ..services.document_service import process_document
# Mock the document processing service
@pytest.fixture
def mock_process_document(monkeypatch):
async def mock_process(*args, **kwargs):
return {"success": True}
monkeypatch.setattr("api.services.document_service.process_document", mock_process)
client = TestClient(app)
def test_upload_document(mock_process_document):
# Create a test file
file_content = b"Test document content"
files = {"file": ("test.pdf", file_content, "application/pdf")}
# Make the request
response = client.post("/upload", files=files)
# Check the response
assert response.status_code == 200
assert "document_id" in response.json()
assert "message" in response.json()
def test_upload_invalid_file_type():
# Create an invalid file type
file_content = b"Invalid file content"
files = {"file": ("test.exe", file_content, "application/octet-stream")}
# Make the request
response = client.post("/upload", files=files)
# Check the response
assert response.status_code == 400
assert "detail" in response.json()
assert "Invalid file type" in response.json()["detail"]
# api/tests/test_qa.py
import pytest
from fastapi.testclient import TestClient
from ..main import app
from ..services.llm_service import get_llm_response
# Mock the LLM service
@pytest.fixture
def mock_llm_service(monkeypatch):
async def mock_get_response(*args, **kwargs):
return "This is a mock answer from the LLM."
monkeypatch.setattr("api.services.llm_service.get_llm_response", mock_get_response)
client = TestClient(app)
def test_ask_question(mock_llm_service):
# Prepare the request data
data = {
"document_id": "test-doc-123",
"question": "What is the main topic?"
}
# Make the request
response = client.post("/ask", json=data)
# Check the response
assert response.status_code == 200
assert "answer" in response.json()
assert response.json()["answer"] == "This is a mock answer from the LLM."
def test_ask_question_missing_parameters():
# Missing document_id
data = {
"question": "What is the main topic?"
}
# Make the request
response = client.post("/ask", json=data)
# Check the response
assert response.status_code == 400
assert "detail" in response.json()
assert "Missing required parameters" in response.json()["detail"]2. Testing LLM Integration
Test your LLM service integration:
# api/tests/test_llm_service.py
import pytest
from unittest.mock import patch, MagicMock
from ..services.llm_service import get_llm_response, get_llm_client
@pytest.fixture
def mock_llm_client():
mock_client = MagicMock()
mock_completion = MagicMock()
mock_completion.choices = [MagicMock(text="Mock LLM response")]
mock_client.completions.create.return_value = mock_completion
with patch("api.services.llm_service.get_llm_client", return_value=mock_client):
yield mock_client
@pytest.mark.asyncio
async def test_get_llm_response(mock_llm_client):
# Test parameters
document_id = "test-doc-123"
document_content = "This is a test document about AI."
question = "What is the document about?"
# Mock the document retrieval
with patch("api.services.document_service.get_document", return_value=document_content):
# Call the function
response = await get_llm_response(document_id, question)
# Check the response
assert response == "Mock LLM response"
# Verify the LLM client was called correctly
mock_llm_client.completions.create.assert_called_once()
call_args = mock_llm_client.completions.create.call_args[1]
assert "Document: This is a test document about AI." in call_args["prompt"]
assert f"Question: {question}" in call_args["prompt"]End-to-End Testing
1. Setting Up Cypress
Set up Cypress for end-to-end testing:
# Install Cypress
npm install --save-dev cypress
# Add to package.json
"scripts": {
"cypress:open": "cypress open",
"cypress:run": "cypress run"
}
# cypress/e2e/document-qa.cy.js
describe('Document Q&A Application', () => {
beforeEach(() => {
// Visit the application
cy.visit('/');
// Intercept API calls
cy.intercept('POST', '/api/upload', {
statusCode: 200,
body: {
document_id: 'e2e-test-doc-123',
message: 'Document uploaded successfully',
},
}).as('uploadDocument');
cy.intercept('POST', '/api/ask', {
statusCode: 200,
body: {
answer: 'This is a test answer from the E2E test.',
},
}).as('askQuestion');
});
it('should upload a document and ask a question', () => {
// Check if the upload component is visible
cy.contains('h2', 'Upload a Document').should('be.visible');
// Upload a file
cy.get('input[type="file"]').attachFile('test-document.pdf');
// Wait for the upload to complete
cy.wait('@uploadDocument');
// Check if the question input is now visible
cy.contains('h2', 'Ask Questions').should('be.visible');
// Type a question
cy.get('input[placeholder*="ask a question"]').type('What is the main topic?');
// Submit the question
cy.contains('button', 'Ask Question').click();
// Wait for the answer
cy.wait('@askQuestion');
// Check if the answer is displayed
cy.contains('This is a test answer from the E2E test.').should('be.visible');
});
it('should handle file upload errors', () => {
// Override the upload intercept to return an error
cy.intercept('POST', '/api/upload', {
statusCode: 400,
body: {
detail: 'Invalid file type',
},
}).as('uploadError');
// Upload an invalid file
cy.get('input[type="file"]').attachFile('invalid-file.exe');
// Wait for the error response
cy.wait('@uploadError');
// Check if the error message is displayed
cy.contains('Invalid file type').should('be.visible');
});
});2. Visual Regression Testing
Add visual regression testing with Cypress and Percy:
# Install Percy
npm install --save-dev @percy/cypress
# Add to cypress/support/e2e.js
import '@percy/cypress';
# Add to cypress/e2e/visual.cy.js
describe('Visual Regression Tests', () => {
it('should match the homepage snapshot', () => {
cy.visit('/');
cy.percySnapshot('Homepage');
});
it('should match the question interface snapshot', () => {
// Set up the application state
cy.visit('/');
// Mock the document upload
cy.window().then((win) => {
win.localStorage.setItem('documentId', 'visual-test-doc-123');
});
// Reload to apply the state
cy.reload();
// Take a snapshot of the question interface
cy.percySnapshot('Question Interface');
});
});Performance Testing
1. Lighthouse CI
Set up Lighthouse CI to test performance metrics:
# Install Lighthouse CI
npm install --save-dev @lhci/cli
# Create lighthouserc.js
module.exports = {
ci: {
collect: {
startServerCommand: 'npm run start',
url: ['http://localhost:3000'],
numberOfRuns: 3,
},
upload: {
target: 'temporary-public-storage',
},
assert: {
preset: 'lighthouse:recommended',
assertions: {
'first-contentful-paint': ['warn', { maxNumericValue: 2000 }],
'interactive': ['error', { maxNumericValue: 3500 }],
'max-potential-fid': ['warn', { maxNumericValue: 300 }],
'cumulative-layout-shift': ['error', { maxNumericValue: 0.1 }],
'largest-contentful-paint': ['warn', { maxNumericValue: 2500 }],
},
},
},
};
# Add to package.json
"scripts": {
"lhci": "lhci autorun"
}2. Load Testing with k6
Set up k6 for load testing your API:
// k6/load-test.js
import http from 'k6/http';
import { sleep, check } from 'k6';
export const options = {
stages: [
{ duration: '30s', target: 20 }, // Ramp up to 20 users
{ duration: '1m', target: 20 }, // Stay at 20 users for 1 minute
{ duration: '30s', target: 0 }, // Ramp down to 0 users
],
thresholds: {
http_req_duration: ['p(95)<500'], // 95% of requests should be below 500ms
http_req_failed: ['rate<0.01'], // Less than 1% of requests should fail
},
};
export default function () {
// Test document upload
const uploadUrl = 'http://localhost:3000/api/upload';
const uploadPayload = {
file: http.file('test-document.pdf', 'application/pdf'),
};
const uploadRes = http.post(uploadUrl, uploadPayload);
check(uploadRes, {
'upload status is 200': (r) => r.status === 200,
'upload has document_id': (r) => JSON.parse(r.body).document_id !== undefined,
});
// Extract document ID
const documentId = JSON.parse(uploadRes.body).document_id;
// Test question answering
const askUrl = 'http://localhost:3000/api/ask';
const askPayload = JSON.stringify({
document_id: documentId,
question: 'What is the main topic?',
});
const askRes = http.post(askUrl, askPayload, {
headers: { 'Content-Type': 'application/json' },
});
check(askRes, {
'ask status is 200': (r) => r.status === 200,
'ask has answer': (r) => JSON.parse(r.body).answer !== undefined,
});
sleep(1);
}Next Steps
Now that you've implemented comprehensive testing for your Document Q&A application, you can: