wanderlust-chatbot / tests /test_intent_classifier.py
Kiriten892's picture
test: add comprehensive test coverage across all 4 layers
fc7d464
Raw
History Blame Contribute Delete
7.78 kB
"""
Unit tests for IntentClassifier.
Covers:
- train() with adequate data → returns accuracy float
- train() with small dataset (< min_per_class threshold) → trains on full set,
returns {"accuracy": None, ...}
- train() with only 1 class → raises ValueError
- train() with mismatched texts/labels length → raises ValueError
- predict() on untrained classifier → returns keyword_only fallback dict
- predict() on trained classifier → returns dict with intent/confidence/method keys
"""
import warnings
import pytest
from app.models.intent_classifier.model import IntentClassifier
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _make_dataset(n_per_class: int, classes=("find_hotel", "find_flight")):
"""Generate a synthetic balanced dataset."""
texts, labels = [], []
class_texts = {
"find_hotel": [
"I need a hotel in Hanoi",
"book a room for me",
"looking for accommodation",
"need a place to stay tonight",
"where can I find a hostel",
"cheap resort near the beach",
"hotel with breakfast included",
"book homestay",
"rent a villa please",
"want to book a lodge",
],
"find_flight": [
"I want to book a flight",
"find me cheap airfare",
"flights from Hanoi to HCMC",
"looking for airline tickets",
"when is the next plane",
"book a ticket to Paris",
"search for flights",
"best airline for this route",
"I need to fly tomorrow",
"airfare prices to Tokyo",
],
"plan_trip": [
"help me plan a trip",
"create an itinerary for 7 days",
"plan vacation to Europe",
"I want to organize my holiday",
"can you make a travel plan",
"schedule a trip for me",
"arrange my travel",
"I need a week-long travel plan",
"draft a tour plan",
"plan a 3-day trip to Da Nang",
],
}
for cls in classes:
pool = class_texts.get(cls, [f"sample {cls} {i}" for i in range(20)])
for i in range(n_per_class):
texts.append(pool[i % len(pool)])
labels.append(cls)
return texts, labels
# ---------------------------------------------------------------------------
# train() — normal path
# ---------------------------------------------------------------------------
class TestIntentClassifierTrain:
def test_train_adequate_dataset_returns_accuracy_float(self):
"""Normal sized dataset: train_test_split fires, accuracy is a float."""
clf = IntentClassifier()
texts, labels = _make_dataset(n_per_class=8)
result = clf.train(texts, labels)
assert clf.is_trained is True
assert "accuracy" in result
assert isinstance(result["accuracy"], float)
assert 0.0 <= result["accuracy"] <= 1.0
def test_train_returns_report_dict(self):
"""Result dict should also contain a 'report' key."""
clf = IntentClassifier()
texts, labels = _make_dataset(n_per_class=8)
result = clf.train(texts, labels)
assert "report" in result
# ---- _can_split guard ----
def test_train_small_dataset_no_split_accuracy_is_none(self):
"""Dataset too small for stratified split → trains on full set,
accuracy == None with a UserWarning."""
clf = IntentClassifier()
# Only 1 sample per class → can_split=False
texts, labels = _make_dataset(n_per_class=1)
with warnings.catch_warnings(record=True) as caught:
warnings.simplefilter("always")
result = clf.train(texts, labels)
assert clf.is_trained is True
assert result["accuracy"] is None
warning_msgs = [str(w.message) for w in caught if issubclass(w.category, UserWarning)]
assert any("full set" in m.lower() or "too small" in m.lower() for m in warning_msgs), \
f"Expected UserWarning about dataset size, got: {warning_msgs}"
def test_train_exactly_threshold_triggers_split(self):
"""When dataset just meets the minimum threshold, split should be attempted."""
clf = IntentClassifier()
# 2 classes, min_total_needed = max(7, 2*2) = 7 → 8 samples with 4 each = can_split
texts, labels = _make_dataset(n_per_class=4)
result = clf.train(texts, labels)
# Either accuracy is a float (split succeeded) or None (edge case allowed)
assert result["accuracy"] is None or isinstance(result["accuracy"], float)
# ---- validation errors ----
def test_train_single_class_raises_value_error(self):
"""Only 1 class → ValueError (need at least 2)."""
clf = IntentClassifier()
texts = ["book a hotel"] * 5
labels = ["find_hotel"] * 5
with pytest.raises(ValueError, match="at least 2 intent classes"):
clf.train(texts, labels)
def test_train_length_mismatch_raises_value_error(self):
"""texts and labels length mismatch → ValueError."""
clf = IntentClassifier()
with pytest.raises(ValueError, match="length mismatch"):
clf.train(["text1", "text2"], ["label1"])
# ---------------------------------------------------------------------------
# predict() — untrained classifier
# ---------------------------------------------------------------------------
class TestIntentClassifierPredict:
def test_predict_untrained_returns_keyword_only(self):
"""Untrained classifier falls back to keyword_only method."""
clf = IntentClassifier()
result = clf.predict("I want to book a hotel")
assert "intent" in result
assert "confidence" in result
assert "method" in result
assert result["method"] == "keyword_only"
def test_predict_result_has_required_keys(self):
"""predict() always returns a dict with intent / confidence / method."""
clf = IntentClassifier()
# Train minimally so the ML path is exercised
texts, labels = _make_dataset(n_per_class=8)
clf.train(texts, labels)
result = clf.predict("I need to find a flight to Paris")
assert set(result.keys()) >= {"intent", "confidence", "method"}
def test_predict_confidence_between_0_and_1(self):
"""confidence is always in [0, 1]."""
clf = IntentClassifier()
texts, labels = _make_dataset(n_per_class=8)
clf.train(texts, labels)
result = clf.predict("book a room please")
assert 0.0 <= result["confidence"] <= 1.0
def test_predict_intent_is_string(self):
"""intent field is always a non-empty string."""
clf = IntentClassifier()
result = clf.predict("hello")
assert isinstance(result["intent"], str)
assert len(result["intent"]) > 0
def test_predict_with_language_hint(self):
"""Passing language hint doesn't crash the classifier."""
clf = IntentClassifier()
result = clf.predict("khách sạn Hà Nội", language="vi")
assert "intent" in result
def test_predict_empty_string_returns_fallback(self):
"""Empty text should return a fallback dict without raising."""
clf = IntentClassifier()
result = clf.predict("")
assert "intent" in result
# Should gracefully produce some result (keyword_only or fallback)
assert result["method"] in ("keyword_only", "ml_model", "keyword_fallback", "ood_rejector")