Начало CS50P в этом месяце казалось немного в обратном направлении, будь честен. Я уже поставил Django CRM, скрипт камеры который мне отправляет письмо, новостной дайджест, пару скребков и инструментов PDF, два проекта boot.dev (bookbot, webflyx). CS50P это «введение в Python» курс. Я уже знаю что такое for цикл.
Я сделал это в любом случае, и я рад что сделал, четыре недели в.
Что базовые курсы реально проверяют
Не можешь ли ты написать for цикл. Можешь ли ты написать тот которого другой человек, или check50, или будущее я через шесть месяцев, может реально прочитать и доверять. Конвенция файла test_ CS50P, реально писать реальные pytest тесты вместо глазирования выхода в терминале, была вещь что немного меня смутила. Я писал «реальные» скрипты какое-то время и тестировал в основном ничего из них.
Первый тестовый файл который я написал, технически прошел и доказал ничего
Задание той недели имело меня писать маленькую функцию проверки ввода, проверяя что пластинчатый код следует набору правил форматирования, и писать тесты рядом с ним. Мой первый проход на тестовом файле выглядел так:
from project import is_valid
def test_valid():
assert is_valid("CS50P")
def test_invalid():
assert not is_valid("cs50p")Два теста. Оба прошли. Зелёные галочки, чувствовал себя отлично примерно четыре минуты, прямо до того как check50 запустил собственные скрытые тесты против моей функции is_valid и провалился на входе который я никогда на самом деле не пробовал: пластинка с числом в середине, как AB12CD, которую моя функция неправильно приняла.
:( is_valid rejects plates with a letter after a digit
expected "False", not "True"Реальная проблема, раз я посмотрел
# project.py, сломанная версия
def is_valid(s):
return s[:2].isalpha() and s[2:].isalnum()Проверяет что это начинается буквами и остальное буквенно-цифровое. Не проверяет реальный порядок какой правила требуют, что раз число показывается, ничего но цифры не могут следовать. AB12CD полностью проходит эту проверку: буквы в начале, буквенно-цифровое для остальное, технически правда, неправильный ответ.
Мои собственные два теста не поймали это потому что я только тестировал один очевидно-валидный случай и один очевидно-невалидный. Ни один не был где-либо рядом с реальной гранью, то есть именно где реальный баг скрывался.
Что я реально гуглил
«pytest test edge cases how many test cases is enough» привел меня, в конце концов, к идее table-driven или параметризированных тестов, написание списка случаев вперёд (ввод, ожидаемый результат) вместо одного assert за раз, специфически так чтобы добавление нового граничного случая было просто добавление одной строки вместо написания целой новой функции.
import pytest
from project import is_valid
@pytest.mark.parametrize("plate, expected", [
("CS50P", True),
("cs50p", False),
("AB12CD", False),
("A1", False),
("", False),
])
def test_is_valid(plate, expected):
assert is_valid(plate) == expectedПять случаев вместо двух, и написание их как таблица сделало зазор в моей собственной мысли явным до того как check50 должен был указать это мне. AB12CD было прямо там раз я был заставлен реально список граничных случаев вместо выбора двух что чувствовалась представительной.
Исправленная функция
# project.py, исправленная версия
def is_valid(s):
seen_digit = False
for ch in s:
if ch.isdigit():
seen_digit = True
elif seen_digit:
return False
return len(s) >= 2 and s[:2].isalpha()Идет через строку один раз, и момент буква показывается после что число уже видели, это невалидно. Прошло мои расширенные локальные тесты и скрытые check50 на следующей отправке.
Скрипт переименования, переосмотрен
Помниш скрипт переименования фото который я писал какое-то время назад, тот что разбился на коллизию имени файла первый раз я запустил его? Я пошел назад и посмотрел на него после лекции CS50P об исключениях, и я прошлый обернул ничего в блок try, просто позволил ему упасть и исправил баг прочитав traceback после. Работало, технически. Не совсем рекомендуемый подход.

Идя назад со свежим взглядом, исправление не было даже об исключениях реально, это было что я никогда один раз не рассмотрел что должно случиться когда цель переименования уже существует, только когда-либо обнаруживал это смотря на вещь падение. try/except FileExistsError вокруг реального вызова os.rename, падение назад к суффиксу нумерации, сделало бы первую версию правильной в день один вместо правильной в четвёртый проход отладки.
Маленький тангент о том как долго я боролся с видео лекции вместо кода
Честно потерял примерно сорок минут убежден что был баг в самом pytest, потому что мой параметризированный тест держал отчет неправильного ожидаемого значения в сообщении отказа, показывая False когда я был уверен я написал True для той строки. Не было бага. Я неправильно посчитал строки при добавлении нового случая в середину списка и сместил каждое последующее ожидаемое значение на один, поэтому строка четыре пластинка проверялась против строки пять ожидаемого ответа. Сообщение об ошибке pytest было полностью правильно весь раз. Я просто не верил этому пока я не напечатал весь список параметризации и не посчитал вручную как это был тест правописания. Таблице-ориентированные тесты только так же надежны как таблица, что очевидно в задней части и не было очевидно совсем в минуту тридцать смотрения на красный X.
Что реально это время другое
Я не учу синтаксис. Я уже имею большинство это, почти случайно, от просто построения вещей которых нужно работать. Что CS50P реально дает мне это материал никто не указывает напрямую до курс делает: надлежащее использование библиотеки вместо копировать-вставить Stack Overflow пока это не компилируется, реальная дисциплина тестирования, код который читает как он был написан для человека а не просто для интерпретатора принять.
Это странный вид смирения. bookbot и webflyx оба работали, отправились, делали вещь они были должны делать. Ни один из них не выжил бы code review от кого-то кто реально знал что они делают, и я только действительно верю это сейчас что я видел что альтернатива выглядит как, таблице-ориентированные тесты включены.
Что я буду по-разному делать вперёд
Написать таблицу граничных случаев перед написанием функции, не после. Каждый баг я попал в этом курсе был сидя в зазоре между «два случая я подумал тестировать» и «случай что check50 реально попробовал». Список граничных случаев сначала превратил бы этот зазор в что-то я ловлю сам, вместо чего скрипт оценки ловит мне.
Я также остановлюсь доверяя зелёному тесту запуск как доказательство чего-либо само по себе. Два прошедших теста рассказали мне ничего полезного об is_valid, потому что два случая я выбрал были оба удобно далеко от реальной границы правила. Набор тестов только как хорош как случаи кто-то реально подумал написать, мои включены, и «это прошло» намного более слабое предложение чем звучит как первый раз ты это печатаешь.
Буду ли я реально продолжать писать тесты для каждого одноразового скрипта после курс кончается это отдельный вопрос. Спроси мене в несколько месяцев. Мой честный угадай прямо сейчас это: для скрипта камеры и новостного дайджеста, вероятно да, потому что те запускаются без присмотра и я не буду там заметить молчаливый отказ. Для одноразового скребка я запускаю один раз и выбрасываю, вероятно всё ещё нет, и я не уверен это реально неправильный зов.