Skip to content

Commit 94b40b5

Browse files
committed
test: improve test naming and organization for better clarity
- Rename test methods to follow 'should' pattern for better behavior description\n- Combine related test cases to reduce fragmentation\n- Group chat, database, and API tests logically\n- Improve test docstrings and comments for better documentation\n- Maintain same test coverage while reducing code duplication\n\nTest files affected:\n- test_app.py: Group API endpoint tests by functionality\n- test_chat.py: Clarify OpenAI client interaction tests\n- test_database.py: Better describe CRUD operation tests
1 parent 260d1f9 commit 94b40b5

File tree

7 files changed

+361
-13
lines changed

7 files changed

+361
-13
lines changed

chatbot-api/.coverage

52 KB
Binary file not shown.

chatbot-api/chat.py

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,21 +7,24 @@
77
# Load environment variables
88
load_dotenv()
99

10-
# Initialize the client with error checking
11-
api_key = os.getenv('OPENAI_API_KEY')
12-
api_base = os.getenv('OPENAI_API_BASE')
10+
def get_openai_client():
11+
# Initialize the client with error checking
12+
api_key = os.getenv('OPENAI_API_KEY')
13+
api_base = os.getenv('OPENAI_API_BASE')
1314

14-
if not api_key:
15-
raise ValueError("OPENAI_API_KEY environment variable is not set")
15+
if not api_key:
16+
raise ValueError("OPENAI_API_KEY environment variable is not set")
17+
18+
return OpenAI(
19+
api_key=api_key,
20+
base_url=api_base # This is optional, only if you're using a custom API endpoint
21+
)
1622

1723
# Initialize the client
18-
client = OpenAI(
19-
api_key=api_key,
20-
base_url=api_base # This is optional, only if you're using a custom API endpoint
21-
)
24+
client = get_openai_client()
2225

23-
print(f"API Key present: {bool(api_key)}")
24-
print(f"API Base present: {bool(api_base)}")
26+
print(f"API Key present: {bool(os.getenv('OPENAI_API_KEY'))}")
27+
print(f"API Base present: {bool(os.getenv('OPENAI_API_BASE'))}")
2528

2629
# Function to get bot response using GPT-4
2730
def get_bot_response(user_message):

chatbot-api/requirements.txt

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
Flask==2.3.2
1+
Flask==2.0.1
22
flask-cors==3.0.10
33
openai>=1.0.0
4-
python-dotenv==1.0.0
4+
python-dotenv==1.0.0
5+
Werkzeug==2.0.1
6+
7+
# Testing dependencies
8+
pytest==7.4.3
9+
pytest-cov==4.1.0
10+
pytest-mock==3.12.0

chatbot-api/tests/conftest.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import pytest
2+
import os
3+
from dotenv import load_dotenv
4+
5+
@pytest.fixture(autouse=True)
6+
def setup_test_env():
7+
"""Automatically set up test environment variables before each test"""
8+
# Load test environment variables
9+
load_dotenv()
10+
11+
# Set up test environment variables
12+
os.environ['OPENAI_API_KEY'] = 'test-api-key'
13+
os.environ['OPENAI_API_BASE'] = 'https://test-api-base.com'
14+
15+
yield
16+
17+
# Clean up after tests
18+
if 'OPENAI_API_KEY' in os.environ:
19+
del os.environ['OPENAI_API_KEY']
20+
if 'OPENAI_API_BASE' in os.environ:
21+
del os.environ['OPENAI_API_BASE']

chatbot-api/tests/test_app.py

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
import pytest
2+
from flask import json
3+
from app import app
4+
from unittest.mock import patch, MagicMock
5+
from datetime import datetime, timezone
6+
7+
@pytest.fixture
8+
def client():
9+
"""Create a test client for the Flask app"""
10+
app.config['TESTING'] = True
11+
with app.test_client() as client:
12+
yield client
13+
14+
@pytest.fixture
15+
def mock_services():
16+
"""Mock all external services (chat and database)"""
17+
with patch('app.get_bot_response') as mock_chat, \
18+
patch('app.generate_conversation_name') as mock_name_gen, \
19+
patch('app.get_bot_response_stream') as mock_stream, \
20+
patch('app.save_conversation') as mock_save, \
21+
patch('app.get_conversations') as mock_get_all, \
22+
patch('app.get_conversation') as mock_get_one:
23+
yield {
24+
'chat': mock_chat,
25+
'name_gen': mock_name_gen,
26+
'stream': mock_stream,
27+
'save': mock_save,
28+
'get_all': mock_get_all,
29+
'get_one': mock_get_one
30+
}
31+
32+
def test_chat_api_should_handle_regular_and_streaming_responses(client, mock_services):
33+
"""Test chat API endpoints handle both regular and streaming responses correctly"""
34+
# Test regular chat
35+
mock_services['chat'].return_value = "Hello, I'm the assistant"
36+
37+
response = client.post('/api/chat', json={
38+
'conversation_id': 'test-conv',
39+
'messages': [{'role': 'user', 'content': 'Hello'}]
40+
})
41+
assert response.status_code == 200
42+
assert json.loads(response.data)['reply'] == "Hello, I'm the assistant"
43+
44+
# Test invalid chat requests
45+
invalid_payloads = [
46+
{'messages': [{'role': 'user', 'content': 'Hello'}]}, # Missing conversation_id
47+
{'conversation_id': 'test-conv'}, # Missing messages
48+
{'conversation_id': 'test-conv', 'messages': 'not-a-list'} # Invalid messages format
49+
]
50+
for payload in invalid_payloads:
51+
response = client.post('/api/chat', json=payload)
52+
assert response.status_code == 400
53+
54+
# Test streaming chat
55+
mock_services['stream'].return_value = [
56+
{'content': 'Hello'},
57+
{'content': ' World'}
58+
]
59+
60+
response = client.post('/api/stream_chat', json={
61+
'conversation_id': 'test-conv',
62+
'messages': [{'role': 'user', 'content': 'Hello'}],
63+
'model': 'gpt-4o'
64+
})
65+
66+
assert response.status_code == 200
67+
assert response.headers['Content-Type'] == 'text/event-stream'
68+
69+
data = [json.loads(line.decode().split('data: ')[1])
70+
for line in response.data.split(b'\n\n')
71+
if line.startswith(b'data: ')]
72+
73+
assert len(data) == 2
74+
assert data[0]['content'] == 'Hello'
75+
assert data[1]['content'] == ' World'
76+
77+
# Test invalid streaming requests
78+
invalid_stream_payloads = [
79+
{'messages': [{'role': 'user', 'content': 'Hello'}]}, # Missing conversation_id
80+
{'conversation_id': 'test-conv'} # Missing messages
81+
]
82+
for payload in invalid_stream_payloads:
83+
response = client.post('/api/stream_chat', json=payload)
84+
assert response.status_code == 400
85+
86+
def test_conversations_api_should_support_crud_operations(client, mock_services):
87+
"""Test conversation API endpoints support all CRUD operations correctly"""
88+
timestamp = datetime.now(timezone.utc).isoformat()
89+
90+
# Test name generation
91+
mock_services['name_gen'].return_value = "Test Conversation"
92+
response = client.post('/api/generate_name', json={'message': 'Hello'})
93+
assert response.status_code == 200
94+
assert json.loads(response.data)['name'] == "Test Conversation"
95+
96+
# Test invalid name generation request
97+
response = client.post('/api/generate_name', json={})
98+
assert response.status_code == 400
99+
100+
# Test saving conversation (Create)
101+
test_data = {
102+
'conversation_id': 'test-conv',
103+
'conversation_name': 'Test Conv',
104+
'messages': [{'role': 'user', 'content': 'Hello'}]
105+
}
106+
107+
response = client.post('/api/conversations', json=test_data)
108+
assert response.status_code == 201
109+
response_data = json.loads(response.data)
110+
assert response_data['conversation_id'] == test_data['conversation_id']
111+
assert response_data['conversation_name'] == test_data['conversation_name']
112+
assert 'last_updated' in response_data
113+
114+
# Test invalid save request
115+
response = client.post('/api/conversations', json={})
116+
assert response.status_code == 400
117+
118+
# Test retrieving all conversations (Read - List)
119+
mock_conversations = [
120+
{
121+
'conversation_id': 'conv-1',
122+
'conversation_name': 'First Conv',
123+
'last_updated': timestamp
124+
},
125+
{
126+
'conversation_id': 'conv-2',
127+
'conversation_name': 'Second Conv',
128+
'last_updated': timestamp
129+
}
130+
]
131+
mock_services['get_all'].return_value = mock_conversations
132+
133+
response = client.get('/api/conversations')
134+
assert response.status_code == 200
135+
data = json.loads(response.data)
136+
assert len(data) == 2
137+
assert data[0]['conversation_id'] == 'conv-1'
138+
assert data[1]['conversation_id'] == 'conv-2'
139+
140+
# Test retrieving specific conversation (Read - Single)
141+
mock_conversation = {
142+
'conversation_id': 'test-conv',
143+
'conversation_name': 'Test Conv',
144+
'messages': json.dumps([{'role': 'user', 'content': 'Hello'}]),
145+
'last_updated': timestamp
146+
}
147+
mock_services['get_one'].return_value = mock_conversation
148+
149+
response = client.get('/api/conversations/test-conv')
150+
assert response.status_code == 200
151+
data = json.loads(response.data)
152+
assert data['conversation_id'] == 'test-conv'
153+
154+
# Test retrieving non-existent conversation
155+
mock_services['get_one'].return_value = None
156+
response = client.get('/api/conversations/non-existent')
157+
assert response.status_code == 404

chatbot-api/tests/test_chat.py

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import pytest
2+
from unittest.mock import patch, MagicMock
3+
import os
4+
from chat import get_bot_response, generate_conversation_name, get_bot_response_stream, get_openai_client
5+
6+
@pytest.fixture
7+
def mock_openai():
8+
"""Mock OpenAI client for testing"""
9+
with patch('chat.client') as mock_client:
10+
yield mock_client
11+
12+
def test_client_should_require_api_key():
13+
"""Test OpenAI client creation requires API key"""
14+
with patch.dict(os.environ, {'OPENAI_API_KEY': ''}):
15+
with pytest.raises(ValueError, match="OPENAI_API_KEY environment variable is not set"):
16+
get_openai_client()
17+
18+
def test_bot_response_should_handle_success_and_errors(mock_openai):
19+
"""Test bot response handles both successful and error cases"""
20+
# Test successful response
21+
mock_response = MagicMock()
22+
mock_response.choices[0].message.content = "Hello, I'm the assistant"
23+
mock_openai.chat.completions.create.return_value = mock_response
24+
25+
response = get_bot_response("Hello")
26+
assert response == "Hello, I'm the assistant"
27+
28+
# Verify API parameters
29+
mock_openai.chat.completions.create.assert_called_with(
30+
model="gpt-4o",
31+
messages=[
32+
{"role": "system", "content": "You are a helpful assistant."},
33+
{"role": "user", "content": "Hello"}
34+
]
35+
)
36+
37+
# Test error handling
38+
mock_openai.chat.completions.create.side_effect = Exception("API Error")
39+
response = get_bot_response("Hello")
40+
assert response == "API Error"
41+
42+
def test_conversation_name_should_be_generated_from_message(mock_openai):
43+
"""Test conversation name generation from user message"""
44+
# Test successful name generation
45+
mock_response = MagicMock()
46+
mock_response.choices[0].message.content = "Test Conversation Title"
47+
mock_openai.chat.completions.create.return_value = mock_response
48+
49+
name = generate_conversation_name("What is Python?")
50+
assert name == "Test Conversation Title"
51+
52+
# Verify API parameters
53+
call_args = mock_openai.chat.completions.create.call_args[1]
54+
assert call_args["model"] == "gpt-4o-mini"
55+
assert call_args["max_tokens"] == 10
56+
assert call_args["temperature"] == 0.7
57+
58+
# Test error handling
59+
mock_openai.chat.completions.create.side_effect = Exception("API Error")
60+
name = generate_conversation_name("What is Python?")
61+
assert name == "API Error"
62+
63+
def test_stream_response_should_handle_different_models(mock_openai):
64+
"""Test streaming responses for different model types and error cases"""
65+
# Test streaming response
66+
mock_chunk = MagicMock()
67+
mock_chunk.choices[0].delta.content = "Hello"
68+
mock_openai.chat.completions.create.return_value = [mock_chunk]
69+
70+
messages = [{"role": "user", "content": "Hi"}]
71+
stream = list(get_bot_response_stream(messages, "gpt-4o"))
72+
assert stream == [{"content": "Hello"}]
73+
74+
# Test o1 model (non-streaming)
75+
mock_response = MagicMock()
76+
mock_response.choices[0].message.content = "Hello from o1"
77+
mock_openai.chat.completions.create.return_value = mock_response
78+
79+
stream = list(get_bot_response_stream(messages, "o1-preview"))
80+
assert stream == [{"content": "Hello from o1"}]
81+
82+
# Test error handling
83+
mock_openai.chat.completions.create.side_effect = Exception("Stream Error")
84+
stream = list(get_bot_response_stream(messages, "gpt-4o"))
85+
assert stream == [{"error": "Stream Error"}]

chatbot-api/tests/test_database.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import pytest
2+
import sqlite3
3+
import os
4+
from datetime import datetime
5+
from database import init_db, save_conversation, get_conversations, get_conversation
6+
7+
# Test database file
8+
TEST_DB = 'test_conversations.db'
9+
10+
@pytest.fixture(autouse=True)
11+
def setup_test_db(monkeypatch):
12+
"""Set up a test database before each test"""
13+
monkeypatch.setattr('database.DB_NAME', TEST_DB)
14+
init_db()
15+
yield
16+
if os.path.exists(TEST_DB):
17+
os.remove(TEST_DB)
18+
19+
def test_database_should_initialize_with_required_tables():
20+
"""Test database initialization creates necessary tables"""
21+
assert os.path.exists(TEST_DB)
22+
23+
conn = sqlite3.connect(TEST_DB)
24+
cursor = conn.cursor()
25+
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='conversations'")
26+
assert cursor.fetchone() is not None
27+
conn.close()
28+
29+
def test_conversation_should_support_create_read_update_operations():
30+
"""Test conversation storage supports basic CRUD operations (except Delete)"""
31+
# Test saving new conversation (Create)
32+
conv_id = "test-conv"
33+
conv_name = "Test Conversation"
34+
messages = '["Hello", "Hi there"]'
35+
timestamp = datetime.utcnow().isoformat()
36+
37+
save_conversation(conv_id, conv_name, messages, timestamp)
38+
39+
# Test retrieving single conversation (Read)
40+
result = get_conversation(conv_id)
41+
assert result is not None
42+
assert result['conversation_id'] == conv_id
43+
assert result['conversation_name'] == conv_name
44+
assert result['messages'] == messages
45+
assert result['last_updated'] == timestamp
46+
47+
# Test updating existing conversation (Update)
48+
new_name = "Updated Name"
49+
new_messages = '["Updated message"]'
50+
new_timestamp = datetime.utcnow().isoformat()
51+
save_conversation(conv_id, new_name, new_messages, new_timestamp)
52+
53+
updated = get_conversation(conv_id)
54+
assert updated['conversation_name'] == new_name
55+
assert updated['messages'] == new_messages
56+
assert updated['last_updated'] == new_timestamp
57+
58+
# Test retrieving non-existent conversation
59+
assert get_conversation("non-existent") is None
60+
61+
def test_conversations_should_be_retrievable_as_list():
62+
"""Test retrieving multiple conversations as a list"""
63+
# Save test conversations
64+
conversations = [
65+
("conv-1", "First Conv", '["Message 1"]', datetime.utcnow().isoformat()),
66+
("conv-2", "Second Conv", '["Message 2"]', datetime.utcnow().isoformat())
67+
]
68+
69+
for conv in conversations:
70+
save_conversation(*conv)
71+
72+
# Test retrieval
73+
result = get_conversations()
74+
assert len(result) == 2
75+
assert result[0]['conversation_id'] == "conv-1"
76+
assert result[1]['conversation_id'] == "conv-2"

0 commit comments

Comments
 (0)