Skip to content

Commit 7302819

Browse files
committed
Tests for Penn Mobile Games backend.
1 parent 41bee13 commit 7302819

5 files changed

Lines changed: 708 additions & 0 deletions

File tree

backend/tests/games/__init__.py

Whitespace-only changes.
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
from io import StringIO
2+
from unittest import mock
3+
4+
from django.core.management import call_command
5+
from django.test import TestCase
6+
from django.utils import timezone
7+
8+
from games.models import Game
9+
10+
11+
# 190 words spanning lengths 3–7 to exercise all freq buckets
12+
MOCK_BOARD = [["a", "b", "c", "d", "e"]] * 5
13+
MOCK_SEED = "garden"
14+
MOCK_SOLS = (
15+
["cat", "bat", "rat", "hat", "mat", "sat", "fat", "pat", "vat", "lat"] * 10 # 100 × len-3
16+
+ ["cats", "bats", "rats", "hats", "mats"] * 10 # 50 × len-4
17+
+ ["table", "cable", "fable", "sable"] * 5 # 20 × len-5
18+
+ ["garden"] * 10 # 10 × len-6
19+
+ ["gardens"] * 10 # 10 × len-7
20+
) # total: 190
21+
22+
23+
def mock_generate_good_game():
24+
return MOCK_BOARD, MOCK_SEED, MOCK_SOLS
25+
26+
27+
class TestGenerateGameCommand(TestCase):
28+
@mock.patch(
29+
"games.management.commands.generate_game.generate_good_game", mock_generate_good_game
30+
)
31+
def test_creates_game_for_today(self):
32+
call_command("generate_game")
33+
self.assertTrue(Game.objects.filter(date=timezone.localdate()).exists())
34+
35+
@mock.patch(
36+
"games.management.commands.generate_game.generate_good_game", mock_generate_good_game
37+
)
38+
def test_saves_correct_board_seed_and_words(self):
39+
call_command("generate_game")
40+
game = Game.objects.get(date=timezone.localdate())
41+
self.assertEqual(MOCK_BOARD, game.board)
42+
self.assertEqual(MOCK_SEED, game.seed)
43+
self.assertEqual(MOCK_SOLS, game.possible_words)
44+
45+
@mock.patch(
46+
"games.management.commands.generate_game.generate_good_game", mock_generate_good_game
47+
)
48+
def test_computes_word_freqs_by_length(self):
49+
call_command("generate_game")
50+
game = Game.objects.get(date=timezone.localdate())
51+
# Django JSONField round-trips integer keys as strings
52+
expected = {"3": 0, "4": 0, "5": 0, "6": 0, "7": 0, "8": 0}
53+
for w in MOCK_SOLS:
54+
expected[str(len(w))] += 1
55+
self.assertEqual(expected, game.freqs)
56+
57+
@mock.patch(
58+
"games.management.commands.generate_game.generate_good_game", mock_generate_good_game
59+
)
60+
def test_running_twice_updates_same_record(self):
61+
call_command("generate_game")
62+
call_command("generate_game")
63+
self.assertEqual(1, Game.objects.filter(date=timezone.localdate()).count())
64+
65+
@mock.patch(
66+
"games.management.commands.generate_game.generate_good_game", mock_generate_good_game
67+
)
68+
def test_prints_success_message(self):
69+
out = StringIO()
70+
call_command("generate_game", stdout=out)
71+
self.assertIn("Game generated successfully", out.getvalue())
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
from collections import Counter
2+
3+
from django.test import SimpleTestCase
4+
5+
from games.generator import (
6+
MIN_LEN,
7+
MAX_LEN,
8+
MIN_TOTAL_WORDS,
9+
SIX_LETTER_WORDS,
10+
WORD_SET,
11+
generate_game,
12+
generate_good_game,
13+
neighbors,
14+
solve_game,
15+
)
16+
17+
18+
class TestNeighbors(SimpleTestCase):
19+
"""Tests the neighbors() grid adjacency helper (used on a 5x5 board, n=4)."""
20+
21+
def test_corner_has_three_neighbors(self):
22+
result = list(neighbors(4, 0, 0))
23+
self.assertEqual(3, len(result))
24+
25+
def test_edge_has_five_neighbors(self):
26+
# top edge, not a corner
27+
result = list(neighbors(4, 2, 0))
28+
self.assertEqual(5, len(result))
29+
30+
def test_center_has_eight_neighbors(self):
31+
result = list(neighbors(4, 2, 2))
32+
self.assertEqual(8, len(result))
33+
34+
def test_all_neighbors_within_bounds(self):
35+
for x in range(5):
36+
for y in range(5):
37+
for xx, yy in neighbors(4, x, y):
38+
self.assertGreaterEqual(xx, 0)
39+
self.assertLessEqual(xx, 4)
40+
self.assertGreaterEqual(yy, 0)
41+
self.assertLessEqual(yy, 4)
42+
43+
def test_no_self_in_neighbors(self):
44+
for x in range(5):
45+
for y in range(5):
46+
self.assertNotIn((x, y), list(neighbors(4, x, y)))
47+
48+
49+
class TestGenerateGame(SimpleTestCase):
50+
def setUp(self):
51+
self.game, self.seed = generate_game()
52+
53+
def test_board_is_5x5(self):
54+
self.assertEqual(5, len(self.game))
55+
for row in self.game:
56+
self.assertEqual(5, len(row))
57+
58+
def test_all_cells_are_single_lowercase_letters(self):
59+
for row in self.game:
60+
for cell in row:
61+
self.assertEqual(1, len(cell))
62+
self.assertTrue(cell.isalpha())
63+
self.assertEqual(cell, cell.lower())
64+
65+
def test_seed_is_six_letter_word(self):
66+
self.assertEqual(6, len(self.seed))
67+
68+
def test_seed_comes_from_word_list(self):
69+
self.assertIn(self.seed, SIX_LETTER_WORDS)
70+
71+
def test_seed_letters_present_on_board(self):
72+
flat = [cell for row in self.game for cell in row]
73+
board_counts = Counter(flat)
74+
for letter, count in Counter(self.seed).items():
75+
self.assertGreaterEqual(
76+
board_counts[letter],
77+
count,
78+
f"Letter '{letter}' from seed not sufficiently present on board",
79+
)
80+
81+
82+
class TestSolveGame(SimpleTestCase):
83+
def setUp(self):
84+
self.game, _ = generate_game()
85+
self.sols = solve_game(self.game)
86+
87+
def test_all_words_in_word_set(self):
88+
for word in self.sols:
89+
self.assertIn(word, WORD_SET, f"'{word}' is not a valid word")
90+
91+
def test_all_words_within_length_limits(self):
92+
for word in self.sols:
93+
self.assertGreaterEqual(len(word), MIN_LEN)
94+
self.assertLessEqual(len(word), MAX_LEN)
95+
96+
def test_no_duplicate_words(self):
97+
self.assertEqual(len(self.sols), len(set(self.sols)))
98+
99+
def test_results_sorted_by_length_desc_then_alpha(self):
100+
for i in range(len(self.sols) - 1):
101+
a, b = self.sols[i], self.sols[i + 1]
102+
if len(a) == len(b):
103+
self.assertLessEqual(a, b, f"Alphabetical order violated: {a!r} before {b!r}")
104+
else:
105+
self.assertGreater(
106+
len(a), len(b), f"Length order violated: {a!r} (len {len(a)}) before {b!r} (len {len(b)})"
107+
)
108+
109+
def test_empty_like_board_returns_only_valid_words(self):
110+
# A board filled with a single rare letter should return no spurious words
111+
board = [["z"] * 5 for _ in range(5)]
112+
result = solve_game(board)
113+
for word in result:
114+
self.assertIn(word, WORD_SET)
115+
116+
117+
class TestGenerateGoodGame(SimpleTestCase):
118+
"""End-to-end test for the full game generation pipeline."""
119+
120+
@classmethod
121+
def setUpClass(cls):
122+
super().setUpClass()
123+
cls.game, cls.seed, cls.sols = generate_good_game()
124+
125+
def test_board_is_5x5(self):
126+
self.assertEqual(5, len(self.game))
127+
for row in self.game:
128+
self.assertEqual(5, len(row))
129+
130+
def test_meets_minimum_word_count(self):
131+
self.assertGreaterEqual(len(self.sols), MIN_TOTAL_WORDS)
132+
133+
def test_all_solutions_are_valid_words(self):
134+
for word in self.sols:
135+
self.assertIn(word, WORD_SET)
136+
137+
def test_seed_comes_from_word_list(self):
138+
self.assertIn(self.seed, SIX_LETTER_WORDS)

backend/tests/games/test_models.py

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import datetime
2+
3+
from django.contrib.auth import get_user_model
4+
from django.db import IntegrityError
5+
from django.test import TestCase
6+
from django.utils import timezone
7+
8+
from games.models import Game, LeaderboardEntry
9+
from games.serializers import GameDetailSerializer, GameSerializer
10+
11+
12+
User = get_user_model()
13+
14+
DATE = datetime.date(2024, 3, 15)
15+
BOARD = [["a", "b", "c", "d", "e"]] * 5
16+
POSSIBLE_WORDS = ["cat", "dog", "fog", "log", "lag"]
17+
SEED = "garden"
18+
19+
20+
class TestGameModel(TestCase):
21+
def test_str_representation(self):
22+
game = Game.objects.create(date=DATE, board=BOARD, possible_words=POSSIBLE_WORDS, seed=SEED)
23+
self.assertEqual(str(DATE), str(game))
24+
25+
def test_get_today_returns_todays_game(self):
26+
game = Game.objects.create(
27+
date=timezone.localdate(), board=BOARD, possible_words=POSSIBLE_WORDS, seed=SEED
28+
)
29+
self.assertEqual(game, Game.get_today())
30+
31+
def test_get_today_returns_none_when_no_game(self):
32+
self.assertIsNone(Game.get_today())
33+
34+
def test_get_today_ignores_other_dates(self):
35+
Game.objects.create(date=DATE, board=BOARD, possible_words=POSSIBLE_WORDS, seed=SEED)
36+
self.assertIsNone(Game.get_today())
37+
38+
def test_freqs_field_round_trips(self):
39+
freqs = {"3": 10, "4": 8, "5": 5, "6": 2, "7": 1, "8": 0}
40+
game = Game.objects.create(
41+
date=DATE, board=BOARD, possible_words=POSSIBLE_WORDS, seed=SEED, freqs=freqs
42+
)
43+
game.refresh_from_db()
44+
self.assertEqual(freqs, game.freqs)
45+
46+
def test_freqs_defaults_to_empty_dict(self):
47+
game = Game.objects.create(date=DATE, board=BOARD, possible_words=POSSIBLE_WORDS, seed=SEED)
48+
self.assertEqual({}, game.freqs)
49+
50+
51+
class TestLeaderboardEntryModel(TestCase):
52+
def setUp(self):
53+
self.user1 = User.objects.create_user("user1", "user1@seas.upenn.edu", "pass")
54+
self.user2 = User.objects.create_user("user2", "user2@seas.upenn.edu", "pass")
55+
self.game = Game.objects.create(
56+
date=DATE, board=BOARD, possible_words=POSSIBLE_WORDS, seed=SEED
57+
)
58+
59+
def test_create_entry_stores_all_fields(self):
60+
entry = LeaderboardEntry.objects.create(
61+
game=self.game, user=self.user1, score=300, words_found=3
62+
)
63+
self.assertEqual(300, entry.score)
64+
self.assertEqual(3, entry.words_found)
65+
self.assertEqual(self.game, entry.game)
66+
self.assertEqual(self.user1, entry.user)
67+
self.assertIsNotNone(entry.submitted_at)
68+
69+
def test_unique_constraint_same_user_same_game(self):
70+
LeaderboardEntry.objects.create(game=self.game, user=self.user1, score=300, words_found=3)
71+
with self.assertRaises(IntegrityError):
72+
LeaderboardEntry.objects.create(game=self.game, user=self.user1, score=400, words_found=4)
73+
74+
def test_different_users_same_game_allowed(self):
75+
LeaderboardEntry.objects.create(game=self.game, user=self.user1, score=300, words_found=3)
76+
LeaderboardEntry.objects.create(game=self.game, user=self.user2, score=400, words_found=4)
77+
self.assertEqual(2, LeaderboardEntry.objects.count())
78+
79+
def test_same_user_different_games_allowed(self):
80+
game2 = Game.objects.create(
81+
date=datetime.date(2024, 3, 16), board=BOARD, possible_words=POSSIBLE_WORDS, seed=SEED
82+
)
83+
LeaderboardEntry.objects.create(game=self.game, user=self.user1, score=300, words_found=3)
84+
LeaderboardEntry.objects.create(game=game2, user=self.user1, score=400, words_found=4)
85+
self.assertEqual(2, LeaderboardEntry.objects.count())
86+
87+
def test_default_ordering_by_score_descending(self):
88+
LeaderboardEntry.objects.create(game=self.game, user=self.user1, score=200, words_found=2)
89+
LeaderboardEntry.objects.create(game=self.game, user=self.user2, score=500, words_found=5)
90+
entries = list(LeaderboardEntry.objects.all())
91+
self.assertEqual(500, entries[0].score)
92+
self.assertEqual(200, entries[1].score)
93+
94+
def test_tiebreaker_fewer_words_ranks_higher(self):
95+
LeaderboardEntry.objects.create(game=self.game, user=self.user1, score=300, words_found=5)
96+
LeaderboardEntry.objects.create(game=self.game, user=self.user2, score=300, words_found=2)
97+
entries = list(LeaderboardEntry.objects.all())
98+
self.assertEqual(self.user2, entries[0].user)
99+
self.assertEqual(2, entries[0].words_found)
100+
101+
def test_cascade_delete_on_game_delete(self):
102+
LeaderboardEntry.objects.create(game=self.game, user=self.user1, score=300, words_found=3)
103+
self.game.delete()
104+
self.assertEqual(0, LeaderboardEntry.objects.count())
105+
106+
def test_cascade_delete_on_user_delete(self):
107+
LeaderboardEntry.objects.create(game=self.game, user=self.user1, score=300, words_found=3)
108+
self.user1.delete()
109+
self.assertEqual(0, LeaderboardEntry.objects.count())
110+
111+
112+
class TestGameSerializer(TestCase):
113+
def setUp(self):
114+
self.game = Game.objects.create(
115+
date=DATE, board=BOARD, possible_words=POSSIBLE_WORDS, seed=SEED,
116+
freqs={"3": 5, "4": 3},
117+
)
118+
119+
def test_public_serializer_exposes_expected_fields(self):
120+
data = GameSerializer(self.game).data
121+
self.assertIn("date", data)
122+
self.assertIn("board", data)
123+
self.assertIn("possible_words", data)
124+
125+
def test_public_serializer_hides_seed_and_freqs(self):
126+
data = GameSerializer(self.game).data
127+
self.assertNotIn("seed", data)
128+
self.assertNotIn("freqs", data)
129+
130+
def test_detail_serializer_exposes_seed_and_freqs(self):
131+
data = GameDetailSerializer(self.game).data
132+
self.assertIn("seed", data)
133+
self.assertIn("freqs", data)
134+
self.assertEqual(SEED, data["seed"])
135+
self.assertEqual({"3": 5, "4": 3}, data["freqs"])

0 commit comments

Comments
 (0)