293 lines
9.2 KiB
Markdown
293 lines
9.2 KiB
Markdown
# Testing Patterns
|
|
|
|
**Analysis Date:** 2026-05-07
|
|
|
|
## Test Framework
|
|
|
|
**Runner:**
|
|
- Django's built-in `TestCase` from `django.test`
|
|
- NO pytest or pytest-django detected in `requirements.txt`
|
|
- Tests run via: `python manage.py test`
|
|
|
|
**Assertion Library:**
|
|
- Django's `TestCase` built-in assertions (inherited from `unittest.TestCase`)
|
|
- `self.assertEqual()`, `self.assertTrue()`, `self.assertRaises()`, etc.
|
|
|
|
**Run Commands:**
|
|
```bash
|
|
python manage.py test # Run all tests
|
|
python manage.py test app_name # Run tests for specific app
|
|
python manage.py test app_name.tests.TestClassName # Run specific test class
|
|
```
|
|
|
|
**Coverage:**
|
|
- No `.coveragerc` or coverage configuration found
|
|
- No coverage requirement enforced
|
|
|
|
## Test File Organization
|
|
|
|
**Location:**
|
|
- Each Django app has a `tests.py` file in app root: `{app_name}/tests.py`
|
|
- Pattern: co-located with models, serializers, views (one file per app)
|
|
|
|
**Naming:**
|
|
- Single file: `tests.py`
|
|
- Test classes inherit from `django.test.TestCase`
|
|
- Test methods prefixed with `test_`: `test_audio()`, `test_model_creation()`, `test_view_response()`
|
|
|
|
**File Structure:**
|
|
```
|
|
app_name/
|
|
├── models.py
|
|
├── serializers.py
|
|
├── views.py
|
|
├── urls.py
|
|
└── tests.py # All tests for this app
|
|
```
|
|
|
|
## Current Test Coverage Status
|
|
|
|
**Honest Assessment: MINIMAL**
|
|
|
|
Test files examined show very sparse coverage:
|
|
- `userapp/tests.py`: 4 lines (empty TestCase class)
|
|
- `aiapp/tests.py`: 24 lines (only 1 actual test: `test_audio()`)
|
|
- `card/tests.py`: 3 lines (empty)
|
|
- `device_interaction/tests.py`: 3 lines (empty)
|
|
- `achievement_app/tests.py`: 3 lines (empty)
|
|
|
|
**Total active test methods: 1** (audio synthesis in `aiapp/tests.py`)
|
|
|
|
This represents **critical test coverage gap** — almost all API endpoints, models, and serializers are untested.
|
|
|
|
## Test Structure
|
|
|
|
**Suite Organization from `aiapp/tests.py`:**
|
|
```python
|
|
from django.test import TestCase
|
|
from django.urls import reverse
|
|
from .audio.AudioService import get_audio_service
|
|
|
|
class YourModelTest(TestCase):
|
|
|
|
def test_audio(self):
|
|
# Setup (minimal)
|
|
audio_ser = get_audio_service()
|
|
audio_ser.synthesize_speech('你好,你是谁啊')
|
|
|
|
# Commented-out template tests below (never implemented)
|
|
# def test_model_creation(self):
|
|
# def test_view_response(self):
|
|
```
|
|
|
|
**Patterns:**
|
|
- `setUp()` method not used; tests don't create fixtures
|
|
- No tearDown() cleanup observed
|
|
- No assertion calls in active test (test_audio just calls synthesize_speech without checking result)
|
|
|
|
## Mocking
|
|
|
|
**Framework:** No mocking library detected (no `unittest.mock`, `responses`, `pytest-mock` in requirements.txt)
|
|
|
|
**Current Practice:**
|
|
- Tests that DO exist (test_audio) call real external services (AudioService.synthesize_speech)
|
|
- No mocking observed for:
|
|
- Aliyun API calls (SMS, OSS, NLS)
|
|
- Volcengine RTC token generation
|
|
- Tencent audio service
|
|
- Kimi AI API
|
|
- Redis cache operations
|
|
- Database operations (use real test DB)
|
|
|
|
**Risk:** Tests that call production services (Aliyun, Kimi, Volcengine) require:
|
|
- Valid credentials in .env
|
|
- Network access to external services
|
|
- Potential cost (SMS sends, API calls)
|
|
|
|
## Database Testing
|
|
|
|
**Test Database:**
|
|
- Django uses a separate test database (by default: test_qy_lty or suffixed with `test_`)
|
|
- No custom settings for test DB observed
|
|
- No fixtures or data factories defined
|
|
|
|
**Patterns:**
|
|
- Models have no test data factories (no factory_boy or similar)
|
|
- No `setUp()` creating test objects
|
|
- Manual creation would be required: `Model.objects.create(...)`
|
|
|
|
## Fixtures and Test Data
|
|
|
|
**Test Data:**
|
|
- NO fixture files found (.json, .yaml, .fixture)
|
|
- NO factory definitions (factory_boy not in requirements)
|
|
- NO model-level test data builders
|
|
|
|
**How to Add:**
|
|
If implementing tests, would need:
|
|
```python
|
|
def setUp(self):
|
|
self.user = ParadiseUser.objects.create(
|
|
username='testuser',
|
|
email='test@example.com'
|
|
)
|
|
self.device = Device.objects.create(
|
|
device_type=...,
|
|
batch=...,
|
|
mac_address='AA:BB:CC:DD:EE:FF'
|
|
)
|
|
```
|
|
|
|
## WebSocket Consumer Testing
|
|
|
|
**Current State:** NO WebSocket consumer tests
|
|
|
|
**Channels provides `channels.testing` package** for async testing, but:
|
|
- No `conftest.py` or pytest configuration
|
|
- No async test runner configured
|
|
- Would require adding pytest + pytest-asyncio + pytest-django
|
|
|
|
**If implementing WebSocket tests, would require:**
|
|
```python
|
|
from channels.testing import WebsocketCommunicator
|
|
from device_interaction.consumers import DeviceConsumer
|
|
|
|
async def test_device_consumer_connect():
|
|
communicator = WebsocketCommunicator(DeviceConsumer.as_asgi(), '/ws/device/')
|
|
connected, subprotocol = await communicator.connect()
|
|
assert connected
|
|
```
|
|
|
|
## External Service Testing
|
|
|
|
**Services integrated WITHOUT mocking in current codebase:**
|
|
- `aiapp.audio.AudioService` — calls real Aliyun/Tencent/Volcengine
|
|
- Aliyun SMS (send_sms in userapp/utils.py)
|
|
- Aliyun OSS (file uploads)
|
|
- Kimi API (AI chat)
|
|
- Volcengine RTC token generation
|
|
- WeChat OAuth (django-allauth)
|
|
|
|
**No test doubles, stubs, or mocks exist** — tests would hit production services.
|
|
|
|
## API Endpoint Testing
|
|
|
|
**Current State:** NO API tests
|
|
|
|
**Endpoints that should be tested but are NOT:**
|
|
- User authentication: `/api/user/register/`, `/api/user/mac-login/`, `/api/user/phone-login/`
|
|
- AI chat: `/api/ai/chat/{bot_id}/`, `/api/ai/multi-chat/`
|
|
- Device binding: `/api/device/bind/`, `/api/device/bind-status/`
|
|
- Card operations: `/api/card/scan/`, `/api/card/use/`, `/api/card/batches/generate/`
|
|
- Achievement tracking: `/api/achievement/user-achievements/`
|
|
- WebSocket: `/ws/device/` connection and message handling
|
|
|
|
**If implementing, would use DRF test client:**
|
|
```python
|
|
from rest_framework.test import APIClient
|
|
|
|
class UserAuthTest(TestCase):
|
|
def setUp(self):
|
|
self.client = APIClient()
|
|
|
|
def test_mac_login(self):
|
|
response = self.client.post('/api/user/mac-login/', {
|
|
'mac_address': 'AA:BB:CC:DD:EE:FF'
|
|
})
|
|
self.assertEqual(response.status_code, 200)
|
|
self.assertIn('token', response.json()['data'])
|
|
```
|
|
|
|
## Test Coverage Gaps
|
|
|
|
**CRITICAL UNTESTED AREAS:**
|
|
|
|
1. **Authentication layer** — RedisTokenAuthentication never tested
|
|
- Token generation in userapp/utils.py:generate_token()
|
|
- Token validation in userapp/authentication.py
|
|
- Device MAC login flow
|
|
- Phone verification login
|
|
|
|
2. **Device control semantics** — "last-bind-wins" ordering never verified
|
|
- UserDevice.Meta.ordering = ['-bound_at'] not tested
|
|
- Multiple users binding same device — control resolution untested
|
|
- Hardcoded test MAC (AA:BB:CC:DD:EE:FF) skip logic untested
|
|
|
|
3. **WebSocket messaging** — All consumer methods untested
|
|
- DeviceConsumer.connect() authentication
|
|
- Message routing in receive()
|
|
- Group messaging (device_{user_id})
|
|
- Disconnect and heartbeat (device:last_seen:{mac})
|
|
|
|
4. **Serializer validation** — No validation tests
|
|
- All custom validators in serializers.py files untested
|
|
- Foreign key constraints untested
|
|
- Unique field constraints untested
|
|
|
|
5. **External service integration** — No mocking
|
|
- Aliyun SMS send (send_sms)
|
|
- Aliyun OSS upload
|
|
- Kimi AI API calls
|
|
- Volcengine RTC token generation
|
|
- Tencent audio service
|
|
|
|
6. **Async task processing** — No tests
|
|
- Affinity rule evaluation (coming in P2)
|
|
- Device heartbeat refresh (5-min TTL)
|
|
- Daily affinity counter aggregation
|
|
|
|
7. **Model business logic** — No tests
|
|
- AffinityRule.get_solo() singleton pattern
|
|
- Device code generation (generate_device_code)
|
|
- User achievement unlock logic
|
|
- Card batch generation logic
|
|
|
|
## Recommendations for Test Implementation
|
|
|
|
**Phase 1 - Setup (High Priority):**
|
|
- Add pytest + pytest-django + pytest-asyncio to requirements.txt
|
|
- Create conftest.py for shared fixtures
|
|
- Create factory definitions (factory_boy) for common models
|
|
- Mock external services (responses, unittest.mock)
|
|
|
|
**Phase 2 - Unit Tests (High Priority):**
|
|
- Test authentication (token generation, validation, expiry)
|
|
- Test device binding semantics (last-bind-wins)
|
|
- Test serializer validation
|
|
- Test model methods (AffinitySetting.get_solo, generate_device_code)
|
|
|
|
**Phase 3 - Integration Tests (Medium Priority):**
|
|
- Test full auth flows (MAC login, phone login, registration)
|
|
- Test API endpoints with mocked external services
|
|
- Test WebSocket consumer connection and messaging
|
|
- Test card batch generation workflow
|
|
|
|
**Phase 4 - E2E Tests (Lower Priority):**
|
|
- Test device binding → WebSocket connection → message exchange flow
|
|
- Test achievement unlock on user action flow
|
|
- Test affinity rule trigger and log creation
|
|
|
|
**Minimum Coverage Target for Production:**
|
|
- Authentication/authorization: 80%+
|
|
- Device control logic: 100% (critical for device binding semantics)
|
|
- Serializer validation: 70%+
|
|
- API endpoints: 60%+ (focus on public-facing, critical flows)
|
|
|
|
---
|
|
|
|
*Testing analysis: 2026-05-07*
|
|
|
|
## Test Execution Example
|
|
|
|
Current working test (from `aiapp/tests.py`):
|
|
```bash
|
|
$ python manage.py test aiapp.tests.YourModelTest.test_audio
|
|
```
|
|
|
|
This test:
|
|
1. Gets AudioService instance
|
|
2. Calls synthesize_speech with Chinese text
|
|
3. Returns (no assertions — test just checks for uncaught exceptions)
|
|
|
|
This represents the ONLY active test in the codebase and does not verify behavior, only that the call completes.
|