Как сделать тесты для моей текстовой игры на Питоне?

У меня есть своя игра на питоне, текстовая. И чем больше я в неё добавляю контента, тем сложнее мне её тестить. Я решил задуматься над автоматическими тестами, но я ничего не знаю про них. Буду рад если дадите ссылки на литературу на эту тему или расскажите мне про это.

(Если вам нужна моя игра, то вот репозиторий https://github.com/Saharoc-game/Batle-boss)


Ответы (1 шт):

Автор решения: Oleg

Как уже написали в комментариях вы можете применить Unit тестирование для своих классов, вот пример unit теста для вашего класса Boss:

class TestBoss(unittest.TestCase):

    # Тест для метода __init__ (конструктора)
    # Используем @patch для временной подмены random.choice и random.randint
    @patch('__main__.random.randint') # Путь к функции randint, которую мокируем
    @patch('__main__.random.choice')  # Путь к функции choice, которую мокируем
    def test_init(self, mock_choice, mock_randint):
        # Указываем, какие значения должны возвращать мокированные функции
        # при их вызове во время этого теста
        mock_choice.return_value = 55 # Boss.hp должен стать 55
        mock_randint.return_value = 4 # Boss.magic должен стать 4

        boss = Boss()

        # Проверяем, что атрибуты установились в соответствии с возвращаемыми значениями моков
        self.assertEqual(boss.hp, 55)
        self.assertEqual(boss.magic, 4)
        # Проверяем, что hp_max равен начальному hp
        self.assertEqual(boss.hp_max, 55)

        # Можно также проверить, что функции random были вызваны
        mock_choice.assert_called_once_with([50, 55, 60])
        mock_randint.assert_called_once_with(3, 4)

    # Тест для метода attack
    # Мокируем только random.randint(0, 5)
    @patch('__main__.random.randint')
    def test_attack(self, mock_randint):
        # Создаем экземпляр босса для теста (init тоже будет использовать рандом, но это не
        # мешает тесту attack, так как мы мокируем только тот randint, что внутри attack)
        # Если бы нам нужно было жестко контролировать init тоже, мы бы мокировали и там.
        # Для простоты теста attack, мы этого не делаем.
        boss = Boss()

        # --- Тестовый случай 1: Нет брони, нет убитых боссов ---
        # Мокируем random.randint(0, 5) так, чтобы он вернул 3
        mock_randint.return_value = 3 # x = 3

        bosses_killed = 0
        armor_defense = 0
        # Ожидаемый урон: ((3 - 3 * (0 / 100)) + 0) // 1 = (3 - 0 + 0) // 1 = 3
        expected_damage = 3

        actual_damage = boss.attack(bosses_killed, armor_defense)
        self.assertEqual(actual_damage, expected_damage, "Тест attack 1 не пройден")
        # Проверяем, что randint был вызван с нужными аргументами
        mock_randint.assert_called_with(0, 5)
        # Важно: reset_mock() сбрасывает счетчики вызовов и возвращаемые значения мока
        # перед следующим тестовым случаем, если мок используется несколько раз в одном тесте.
        # В данном случае, mock_randint используется один раз на вызов attack.
        # Если бы мы вызывали attack несколько раз в этом тесте, это было бы полезно.
        # Для clarity, лучше разделить на отдельные тесты, как ниже.

    @patch('__main__.random.randint')
    def test_attack_with_armor(self, mock_randint):
        boss = Boss()
        # --- Тестовый случай 2: Есть броня, нет убитых боссов ---
        mock_randint.return_value = 4 # x = 4

        bosses_killed = 0
        armor_defense = 50 # 50% защиты
        # Ожидаемый урон: ((4 - 4 * (50 / 100)) + 0) // 1 = (4 - 4 * 0.5 + 0) // 1 = (4 - 2) // 1 = 2
        expected_damage = 2

        actual_damage = boss.attack(bosses_killed, armor_defense)
        self.assertEqual(actual_damage, expected_damage, "Тест attack 2 не пройден")
        mock_randint.assert_called_with(0, 5)

if __name__ == '__main__':
    unittest.main(argv=['first-arg-is-ignored'], exit=False)
→ Ссылка