Skip to content

Commit 8dd24ba

Browse files
Merge pull request #1226 from NASA-IMPACT/1195-implement-unit-test-for-forms-on-the-frontend
Implement unit test for forms on the frontend
2 parents 24fb7eb + 73a2dbc commit 8dd24ba

File tree

10 files changed

+903
-9
lines changed

10 files changed

+903
-9
lines changed

compose/local/django/Dockerfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ RUN apt-get update && apt-get install --no-install-recommends -y \
5252
&& wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add - \
5353
&& apt-get update \
5454
&& apt-get install -y postgresql-15 postgresql-client-15 \
55+
&& apt-get install -y chromium chromium-driver \
5556
# cleaning up unused files
5657
&& apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false \
5758
&& rm -rf /var/lib/apt/lists/*
Lines changed: 283 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,283 @@
1+
# Frontend Testing Methodologies for Django Projects
2+
## Overview
3+
4+
This document outlines testing methodologies for Django projects with HTML forms and JavaScript enhancements, focusing on Python-based testing solutions. While going through the codebase, I can see it is primarily a JavaScript-heavy frontend that uses plain HTML forms enhanced with JavaScript/jQuery rather than server-rendered Django forms. Django forms are being used only in the admin panel of the project.
5+
6+
## Primary Testing Tools
7+
8+
### 1. Selenium with Python (Chosen)
9+
10+
#### Capabilities
11+
- Full browser automation
12+
- JavaScript execution support
13+
- Real DOM interaction
14+
- Cross-browser testing
15+
- Modal dialog handling
16+
- AJAX request testing
17+
- File upload testing
18+
- DataTables interaction
19+
20+
#### Implementation
21+
```python
22+
from selenium import webdriver
23+
from selenium.webdriver.common.by import By
24+
from selenium.webdriver.support.ui import WebDriverWait
25+
from selenium.webdriver.support import expected_conditions as EC
26+
27+
class TestCollectionDetail:
28+
def setup_method(self):
29+
self.driver = webdriver.Chrome()
30+
self.wait = WebDriverWait(self.driver, 10)
31+
32+
def test_title_change_modal(self):
33+
# Example test for title change modal
34+
self.driver.get("/collections/1/")
35+
title_button = self.wait.until(
36+
EC.element_to_be_clickable((By.ID, "change-title-btn"))
37+
)
38+
title_button.click()
39+
40+
modal = self.wait.until(
41+
EC.visibility_of_element_located((By.ID, "title-modal"))
42+
)
43+
44+
form = modal.find_element(By.TAG_NAME, "form")
45+
input_field = form.find_element(By.NAME, "title")
46+
input_field.send_keys("New Title")
47+
form.submit()
48+
49+
# Wait for AJAX completion
50+
self.wait.until(
51+
EC.text_to_be_present_in_element((By.ID, "collection-title"), "New Title")
52+
)
53+
```
54+
55+
#### Pros
56+
- Complete end-to-end testing
57+
- Real browser interaction
58+
- JavaScript support
59+
- Comprehensive API
60+
- Strong community support
61+
62+
#### Drawbacks
63+
- Slower execution
64+
- Browser dependencies
65+
- More complex setup
66+
- Can be flaky with timing issues
67+
68+
### 2. pytest-django with django-test-client
69+
70+
#### Capabilities
71+
- Form submission testing
72+
- Response validation
73+
- Header verification
74+
- Status code checking
75+
- Session handling
76+
- Template rendering testing
77+
78+
#### Implementation
79+
```python
80+
import pytest
81+
from django.urls import reverse
82+
83+
@pytest.mark.django_db
84+
class TestCollectionForms:
85+
def test_collection_create(self, client):
86+
url = reverse('collection_create')
87+
data = {
88+
'title': 'Test Collection',
89+
'division': 'division1',
90+
'workflow_status': 'active'
91+
}
92+
response = client.post(url, data)
93+
assert response.status_code == 302 # Redirect after success
94+
95+
# Verify creation
96+
response = client.get(reverse('collection_detail', kwargs={'pk': 1}))
97+
assert 'Test Collection' in response.content.decode()
98+
```
99+
100+
#### Pros
101+
- Fast execution
102+
- No browser dependency
103+
- Simpler setup
104+
- Integrated with Django
105+
106+
#### Drawbacks
107+
- **No JavaScript support (Dealbreaker)**
108+
- Limited DOM interaction
109+
- Can't test real user interactions
110+
111+
### 3. Playwright for Python
112+
113+
#### Capabilities
114+
- Modern browser automation
115+
- Async/await support
116+
- Network interception
117+
- Mobile device emulation
118+
- Automatic waiting
119+
- Screenshot and video capture
120+
121+
#### Implementation
122+
```python
123+
from playwright.sync_api import sync_playwright
124+
125+
def test_modal_form_submission():
126+
with sync_playwright() as p:
127+
browser = p.chromium.launch()
128+
page = browser.new_page()
129+
130+
page.goto("/collections/")
131+
132+
# Click button to open modal
133+
page.click("#add-collection-btn")
134+
135+
# Fill form in modal
136+
page.fill("#title-input", "New Collection")
137+
page.fill("#division-input", "Division A")
138+
139+
# Submit form
140+
page.click("#submit-btn")
141+
142+
# Wait for success message
143+
success_message = page.wait_for_selector(".toast-success")
144+
assert "Collection created" in success_message.text_content()
145+
146+
browser.close()
147+
```
148+
149+
#### Pros
150+
- Modern API design
151+
- Better stability than Selenium
152+
- Built-in async support
153+
- Powerful debugging tools
154+
155+
#### Drawbacks
156+
- **Newer tool, smaller community (Dealbreaker)**
157+
- Additional system dependencies
158+
- Learning curve for async features
159+
160+
161+
### 4. Beautiful Soup with Requests
162+
A combination for testing HTML structure and content.
163+
164+
**Capabilities:**
165+
- HTML parsing and validation
166+
- Content extraction
167+
- Structure verification
168+
- Link checking
169+
- Form field validation
170+
- Template testing
171+
172+
**Pros:**
173+
- Lightweight solution
174+
- Flexible HTML parsing
175+
- No browser dependency
176+
- Fast execution
177+
- Simple API
178+
- Low resource usage
179+
180+
**Drawbacks:**
181+
- **No JavaScript support (Dealbreaker)**
182+
- Limited interaction testing
183+
- No visual testing
184+
- Basic functionality only
185+
- No real browser simulation
186+
187+
## Feature Comparison Table
188+
189+
| Feature | Selenium | Django Test Client | Playwright | Beautiful Soup |
190+
|---------------------------|----------|-------------------|------------|----------------|
191+
| JavaScript Support | ✅ Yes | ❌ No | ✅ Yes | ❌ No |
192+
| Setup Complexity | 🟡 Medium | 🟢 Low | 🟡 Medium | 🟢 Low |
193+
| Execution Speed | 🔴 Slow | 🟢 Fast | 🟡 Medium | 🟢 Fast |
194+
| Modal Testing | ✅ Yes | ❌ No | ✅ Yes | ❌ No |
195+
| AJAX Testing | ✅ Yes | ❌ No | ✅ Yes | ❌ No |
196+
| Cross-browser Testing | ✅ Yes | ❌ No | ✅ Yes | ❌ No |
197+
| Real User Interaction | ✅ Yes | ❌ No | ✅ Yes | ❌ No |
198+
| Documentation Quality | ✅ Excellent| ✅ Good | ✅ Good | ✅ Good |
199+
| Community Support | ✅ Large | ✅ Large | 🟡 Growing | ✅ Large |
200+
201+
## Testing Strategy Recommendations
202+
203+
1. **Primary Testing Tool**: Selenium with Python
204+
- Best suited for your JavaScript-heavy interface
205+
- Handles modals and AJAX naturally
206+
- Extensive documentation and community support
207+
208+
2. **Test Coverage Areas**:
209+
- Modal form interactions
210+
- AJAX submissions
211+
- DataTables functionality
212+
- Form validation
213+
- Success/error messages
214+
- URL routing
215+
- DOM updates
216+
217+
## Implementation Steps
218+
219+
1. Add testing dependencies to requirements file `requirements/local.txt`:
220+
```text
221+
# Testing Dependencies
222+
selenium>=4.15.2
223+
pytest-xdist>=3.3.1
224+
pytest-cov>=4.1.0
225+
```
226+
227+
2. Update Dockerfile `compose/local/django/Dockerfile` to install Chrome and ChromeDriver:
228+
```dockerfile
229+
# Install Chrome and ChromeDriver for Selenium tests
230+
RUN apt-get update && apt-get install -y \
231+
chromium \
232+
chromium-driver \
233+
&& rm -rf /var/lib/apt/lists/*
234+
```
235+
236+
3. Rebuild Docker container to apply changes:
237+
```bash
238+
docker-compose -f local.yml build django
239+
```
240+
241+
4. Create test directory structure:
242+
```bash
243+
mkdir -p tests/frontend
244+
touch tests/frontend/__init__.py
245+
touch tests/frontend/base.py
246+
touch tests/frontend/test_setup.py
247+
```
248+
249+
5. Create base test classes:
250+
```python
251+
import pytest
252+
from selenium import webdriver
253+
254+
class BaseUITest:
255+
@pytest.fixture(autouse=True)
256+
def setup_class(self):
257+
self.driver = webdriver.Chrome()
258+
yield
259+
self.driver.quit()
260+
261+
def login(self):
262+
# Common login logic
263+
pass
264+
```
265+
266+
6. Organize tests by feature:
267+
```python
268+
class TestCollectionManagement(BaseUITest):
269+
def test_create_collection(self):
270+
pass
271+
272+
def test_edit_collection(self):
273+
pass
274+
275+
class TestURLPatterns(BaseUITest):
276+
def test_add_include_pattern(self):
277+
pass
278+
```
279+
280+
7. Run tests:
281+
```bash
282+
docker-compose -f local.yml run --rm django pytest tests/frontend/test_setup.py -v
283+
```

requirements/local.txt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,10 @@ pytest==8.0.0 # https://github.com/pytest-dev/pytest
1313
pytest-sugar==1.0.0 # https://github.com/Frozenball/pytest-sugar
1414
types-requests # maybe instead, we should add `mypy --install-types` to the dockerfile?
1515
types-xmltodict
16-
coverage
16+
pytest-xdist>=3.3.1
17+
pytest-cov>=4.1.0
18+
selenium>=4.15.2 # Selenium (Frontend Testing)
19+
coverage==7.4.1
1720

1821
# Documentation
1922
# ------------------------------------------------------------------------------

sde_collections/tests/frontend/__init__.py

Whitespace-only changes.
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import shutil
2+
3+
import pytest
4+
from django.contrib.staticfiles.testing import StaticLiveServerTestCase
5+
from selenium import webdriver
6+
from selenium.webdriver.chrome.options import Options
7+
from selenium.webdriver.chrome.service import Service
8+
from selenium.webdriver.support.ui import WebDriverWait
9+
10+
from .mixins import AuthenticationMixin
11+
12+
13+
class BaseTestCase(StaticLiveServerTestCase, AuthenticationMixin):
14+
"""Base class for all frontend tests using Selenium."""
15+
16+
@classmethod
17+
def setUpClass(cls):
18+
super().setUpClass()
19+
20+
# Verify ChromeDriver and Chromium are available
21+
chromedriver_path = shutil.which("chromedriver")
22+
chromium_path = shutil.which("chromium")
23+
24+
if not chromedriver_path:
25+
pytest.fail("ChromeDriver not found. Please ensure chromium-driver is installed.")
26+
if not chromium_path:
27+
pytest.fail("Chromium not found. Please ensure chromium is installed.")
28+
29+
# Set up Chrome options
30+
chrome_options = Options()
31+
chrome_options.add_argument("--headless")
32+
chrome_options.add_argument("--no-sandbox")
33+
chrome_options.add_argument("--disable-dev-shm-usage")
34+
chrome_options.binary_location = chromium_path
35+
36+
try:
37+
service = Service(executable_path=chromedriver_path)
38+
cls.driver = webdriver.Chrome(service=service, options=chrome_options)
39+
cls.driver.set_window_size(1920, 1080)
40+
cls.driver.implicitly_wait(10)
41+
cls.wait = WebDriverWait(cls.driver, 10)
42+
43+
except Exception as e:
44+
pytest.fail(f"Failed to initialize ChromeDriver: {str(e)}")
45+
46+
@classmethod
47+
def tearDownClass(cls):
48+
if hasattr(cls, "driver"):
49+
cls.driver.quit()
50+
super().tearDownClass()

0 commit comments

Comments
 (0)