Skip to content

Commit 7ffb182

Browse files
Add pytest test suites for Null Object, Specification, and Singleton
- 34 tests covering all three contributed patterns - 100% code coverage on all three pattern modules - Tests include edge cases, composite specifications, and singleton isolation
1 parent b5f0262 commit 7ffb182

3 files changed

Lines changed: 293 additions & 0 deletions

File tree

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import pytest
2+
3+
from patterns.behavioral.null_object import Customer, NullCustomer, CustomerRepository
4+
5+
6+
@pytest.fixture
7+
def repository():
8+
return CustomerRepository()
9+
10+
11+
@pytest.fixture
12+
def real_customer():
13+
return Customer("Ahmed", "ahmed@example.com")
14+
15+
16+
@pytest.fixture
17+
def null_customer():
18+
return NullCustomer()
19+
20+
21+
# --- Customer Tests ---
22+
23+
24+
def test_customer_is_not_null(real_customer):
25+
assert real_customer.is_null() is False
26+
27+
28+
def test_customer_has_correct_name(real_customer):
29+
assert real_customer.name == "Ahmed"
30+
31+
32+
def test_customer_has_correct_email(real_customer):
33+
assert real_customer.email == "ahmed@example.com"
34+
35+
36+
def test_customer_send_notification(real_customer, capsys):
37+
real_customer.send_notification("Hello")
38+
captured = capsys.readouterr()
39+
assert "Sending 'Hello' to Ahmed at ahmed@example.com" in captured.out
40+
41+
42+
# --- NullCustomer Tests ---
43+
44+
45+
def test_null_customer_is_null(null_customer):
46+
assert null_customer.is_null() is True
47+
48+
49+
def test_null_customer_has_default_name(null_customer):
50+
assert null_customer.name == "N/A"
51+
52+
53+
def test_null_customer_has_default_email(null_customer):
54+
assert null_customer.email == "N/A"
55+
56+
57+
def test_null_customer_send_notification_does_nothing(null_customer, capsys):
58+
null_customer.send_notification("Hello")
59+
captured = capsys.readouterr()
60+
assert captured.out == ""
61+
62+
63+
# --- Repository Tests ---
64+
65+
66+
def test_repository_returns_real_customer(repository):
67+
customer = repository.get_customer(1)
68+
assert customer.is_null() is False
69+
assert customer.name == "Ahmed"
70+
71+
72+
def test_repository_returns_null_for_missing(repository):
73+
customer = repository.get_customer(999)
74+
assert customer.is_null() is True
75+
76+
77+
def test_repository_null_customer_is_safe(repository):
78+
customer = repository.get_customer(999)
79+
customer.send_notification("Test") # Should not raise any exception
80+
81+
82+
def test_loop_over_mixed_customers(repository, capsys):
83+
for customer_id in [1, 2, 999]:
84+
repository.get_customer(customer_id).send_notification("Hi")
85+
captured = capsys.readouterr()
86+
assert "Ahmed" in captured.out
87+
assert "Sara" in captured.out
88+
assert captured.out.count("Sending") == 2 # Only 2 real customers
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import pytest
2+
3+
from patterns.behavioral.specification import (
4+
User,
5+
UserSpecification,
6+
SuperUserSpecification,
7+
Product,
8+
PriceBelowSpecification,
9+
InCategorySpecification,
10+
InStockSpecification,
11+
)
12+
13+
14+
# --- Fixtures ---
15+
16+
17+
@pytest.fixture
18+
def normal_user():
19+
return User(super_user=False)
20+
21+
22+
@pytest.fixture
23+
def super_user():
24+
return User(super_user=True)
25+
26+
27+
@pytest.fixture
28+
def products():
29+
return [
30+
Product("Python Book", 45.0, "books", True),
31+
Product("Laptop", 1200.0, "electronics", True),
32+
Product("Headphones", 80.0, "electronics", False),
33+
Product("Notebook", 5.0, "stationery", True),
34+
]
35+
36+
37+
# --- User Specification Tests ---
38+
39+
40+
def test_user_specification_with_user(normal_user):
41+
spec = UserSpecification()
42+
assert spec.is_satisfied_by(normal_user) is True
43+
44+
45+
def test_user_specification_with_non_user():
46+
spec = UserSpecification()
47+
assert spec.is_satisfied_by("not a user") is False
48+
49+
50+
def test_super_user_specification_true(super_user):
51+
spec = SuperUserSpecification()
52+
assert spec.is_satisfied_by(super_user) is True
53+
54+
55+
def test_super_user_specification_false(normal_user):
56+
spec = SuperUserSpecification()
57+
assert spec.is_satisfied_by(normal_user) is False
58+
59+
60+
def test_user_and_super_user_combined(normal_user, super_user):
61+
spec = UserSpecification().and_specification(SuperUserSpecification())
62+
assert spec.is_satisfied_by(normal_user) is False
63+
assert spec.is_satisfied_by(super_user) is True
64+
65+
66+
# --- Product Specification Tests ---
67+
68+
69+
def test_price_below(products):
70+
spec = PriceBelowSpecification(100)
71+
result = [p for p in products if spec.is_satisfied_by(p)]
72+
assert len(result) == 3
73+
assert all(p.price < 100 for p in result)
74+
75+
76+
def test_in_category(products):
77+
spec = InCategorySpecification("electronics")
78+
result = [p for p in products if spec.is_satisfied_by(p)]
79+
assert len(result) == 2
80+
assert all(p.category == "electronics" for p in result)
81+
82+
83+
def test_in_stock(products):
84+
spec = InStockSpecification()
85+
result = [p for p in products if spec.is_satisfied_by(p)]
86+
assert len(result) == 3
87+
assert all(p.in_stock for p in result)
88+
89+
90+
# --- Composite Specification Tests ---
91+
92+
93+
def test_and_specification(products):
94+
cheap_and_available = PriceBelowSpecification(100).and_specification(
95+
InStockSpecification()
96+
)
97+
result = [p for p in products if cheap_and_available.is_satisfied_by(p)]
98+
assert [p.name for p in result] == ["Python Book", "Notebook"]
99+
100+
101+
def test_or_specification(products):
102+
cheap_or_electronics = PriceBelowSpecification(100).or_specification(
103+
InCategorySpecification("electronics")
104+
)
105+
result = [p for p in products if cheap_or_electronics.is_satisfied_by(p)]
106+
assert len(result) == 4 # All products match
107+
108+
109+
def test_not_specification(products):
110+
not_in_stock = InStockSpecification().not_specification()
111+
result = [p for p in products if not_in_stock.is_satisfied_by(p)]
112+
assert len(result) == 1
113+
assert result[0].name == "Headphones"
114+
115+
116+
def test_complex_combination(products):
117+
electronics_out_of_stock = InCategorySpecification(
118+
"electronics"
119+
).and_specification(InStockSpecification().not_specification())
120+
result = [p for p in products if electronics_out_of_stock.is_satisfied_by(p)]
121+
assert len(result) == 1
122+
assert result[0].name == "Headphones"

tests/creational/test_singleton.py

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import pytest
2+
3+
from patterns.creational.singleton import Singleton, AppConfig
4+
5+
6+
@pytest.fixture(autouse=True)
7+
def reset_singleton():
8+
"""Reset singleton instances before each test to ensure test isolation."""
9+
Singleton._instance = None
10+
AppConfig._instance = None
11+
AppConfig._initialized = False
12+
yield
13+
Singleton._instance = None
14+
AppConfig._instance = None
15+
AppConfig._initialized = False
16+
17+
18+
# --- Singleton Base Tests ---
19+
20+
21+
def test_singleton_same_instance():
22+
s1 = Singleton()
23+
s2 = Singleton()
24+
assert s1 is s2
25+
26+
27+
def test_singleton_only_one_instance():
28+
instances = [Singleton() for _ in range(10)]
29+
assert all(inst is instances[0] for inst in instances)
30+
31+
32+
# --- AppConfig Tests ---
33+
34+
35+
def test_appconfig_is_singleton():
36+
config1 = AppConfig()
37+
config2 = AppConfig()
38+
assert config1 is config2
39+
40+
41+
def test_appconfig_default_settings():
42+
config = AppConfig()
43+
assert config.get("database_url") == "localhost:5432"
44+
assert config.get("debug") is False
45+
assert config.get("language") == "en"
46+
47+
48+
def test_appconfig_set_and_get():
49+
config = AppConfig()
50+
config.set("language", "ar")
51+
assert config.get("language") == "ar"
52+
53+
54+
def test_appconfig_shared_state():
55+
config1 = AppConfig()
56+
config2 = AppConfig()
57+
config1.set("theme", "dark")
58+
assert config2.get("theme") == "dark"
59+
60+
61+
def test_appconfig_missing_key_returns_none():
62+
config = AppConfig()
63+
assert config.get("nonexistent_key") is None
64+
65+
66+
def test_appconfig_initialized_only_once():
67+
config1 = AppConfig()
68+
config1.set("language", "ar")
69+
config2 = AppConfig()
70+
assert config2.get("language") == "ar" # Not reset to "en"
71+
72+
73+
def test_appconfig_add_new_setting():
74+
config = AppConfig()
75+
config.set("max_connections", 100)
76+
assert config.get("max_connections") == 100
77+
78+
79+
def test_appconfig_overwrite_setting():
80+
config = AppConfig()
81+
assert config.get("debug") is False
82+
config.set("debug", True)
83+
assert config.get("debug") is True

0 commit comments

Comments
 (0)