CLI-проект для тестирования кода алгоритмических задач на Python. Поддерживает несколько видов тестирования (в зависимости от формата задачи) и загрузку задач с некоторых онлайн-ресурсов.
- Возможности
- Установка
- Пример использования
- Команды
- Структура проекта
- Решение
- Тестировщики
- Примеры решений
- Загрузка задач с
LeetCode
иCodeforces
. - Тестирование как единицы (функции, класса), так и через поток ввода-вывода.
- Конфигурация тестирования (можно настроить генерацию тестовых случаев, свою валидацию теста, свой скрипт выполнения теста).
- Удобный ввод тестовых случаев через текстовый файл.
-
Клонируйте репозиторий:
git clone https://github.com/vladisnut/codetester.git cd codetester
-
Создайте виртуальное окружение (опционально):
python -m venv venv source venv/bin/activate # Linux/Mac venv\Scripts\activate # Windows
-
Установите зависимости:
pip install -r requirements.txt
-
Загрузим любую задачу с LeetCode, например, с ID=1 (вместо ID можно указать slug (
two-sum
) или ссылку на задачу).python main.py load -s leetcode 1
-
В папке
solutions
была создана новая папка. Это новое решение, созданное для загруженной задачи. В этой папке в файлеsolution.py
появилась заготовка для решения задачи. Можем дописать код решения задачи. -
В этой же папке в файле
tests.txt
находятся тестовые случаи, которые загрузились вместе с задачей. Можно добавить собственные тесты. Форматы их записей описан здесь. -
Запустим тест решения (по умолчанию будет запущено последнее изменённое решение).
python main.py test
-
Готово! Таким образом можно загружать и тестировать задачи. Чтобы узнать больше возможностей, читайте далее.
Создает папку в solutions
с базовой структурой файлов решения.
create [solution] [--template|-t <name>]
Параметр | Тип | Обязательный | Описание | Допустимые значения |
---|---|---|---|---|
solution |
Позиционный | Нет | Имя решения | Допустимое имя папки, по умолчанию: значение параметра конфигурации SOLUTION_DEFAULT_NAME |
template |
Именованный | Нет | Имя шаблона скрипта для решения задачи | Имя файла из папки assets/templates/solution , по умолчанию: значение параметра конфигурации MAIN_FUNCTION_NAME |
Загружает данные задачи с внешнего ресурса и создает для нее решение.
Если параметр конфигурации CREATE_NEW_SOLUTIONS = True
, то будет создано новое решение
с slug'ом задачи, иначе с именем main
.
load <slug> [--source|-s <leetcode|codeforces>] [--open|-o]
Параметр | Тип | Обязательный | Описание | Допустимые значения |
---|---|---|---|---|
slug |
Позиционный | Да | Slug, ID или URL задачи | URL задачи, если source не задан, иначе slug, ID, URL задачи или !daily для ежедневной задачи LeetCode |
source |
Именованный | Нет | Имя ресурса с задачей | leetcode , codeforces |
open |
Именованный | Нет | Открыть задачу в браузере | Флаг (не требует значения) |
Запускает тесты решения.
test [solution] [--time|-t] [--debug|-d]
Параметр | Тип | Обязательный | Описание | Допустимые значения |
---|---|---|---|---|
solution |
Позиционный | Нет | Имя решения. Если значение не задано и параметр конфигурации LAUNCH_LAST_MODIFIED_SOLUTION = True , то будет запущено последнее измененное решение |
Имя существующего решения (имя одной из папок в solutions ) |
time |
Именованный | Нет | Показывать время выполнения каждого теста | Флаг (не требует значения) |
debug |
Именованный | Нет | Режим отладки, при котором тестировщики могут выводить дополнительную информацию | Флаг (не требует значения) |
Корень проекта имеет следующие файлы папки:
Имя файла/папки | Описание |
---|---|
main.py |
Основной запускающий скрипт |
config.py |
Конфигурация проекта |
assets/ |
Шаблоны скриптов для решения |
solutions/ |
Содержит решения, каждая папка – отдельное решение |
src/ |
Содержит исходный код проекта |
Структура исходного кода:
.
├── api/ # Модули для работы с API (LeetCode, Codeforces)
├── commands/ # Пакет доступных CLI-команд
├── sources/ # Пакет для работы с задачами из определенных ресурсов
├── nodes/ # Пакет для работы с Node-классами (для задач с LeetCode)
├── testing/ # Тестирование решений
│ ├── results/ # Пакет с классами для работы с результатами тестов
│ └── testers/ # Пакет с тестировщиками
└── utils/ # Утилиты
Решение — это папка, содержащая все необходимые компоненты для работы с конкретной задачей,
включая её условие, тесты и код решения. Решения хранятся в папке solutions
.
Типичное решение имеет следующую структуру:
Имя файла | Обязательный | Описание |
---|---|---|
data.json |
Нет | Метаданные задачи (название, сложность, теги, исходный ресурс и т. д.) в формате JSON |
description.md |
Нет | Условие задачи в формате Markdown (генерируется автоматически при загрузке с внешнего ресурса) |
settings.py |
Нет | Конфигурация тестирования |
solution.py |
Да | Основной исполняемый скрипт с алгоритмом решения задачи |
tests.txt |
Нет | Набор тестовых случаев (входные данные и ожидаемые результаты). Формат ввода зависит от тестировщика |
Как видно из таблицы, для тестирования решения достаточно одного файла
solution.py
, однако в этом случае оно не содержит ни одного тестового случая.
Конфигурация тестирования решения в файле settings.py
имеет следующие параметры:
Параметр | Описание | Случай применения | Допустимые значения |
---|---|---|---|
TARGET |
Цель (функция, класс, метод класса), которую нужно тестировать. Если не указана, то будет выбрана автоматически исходя из анализа файла solution.py |
Когда автоматический анализ выбирает не ту цель | Строка с именем любой функции, класса или метода класса (в формате ClassName.methodName ) из solution.py |
TESTER |
Тестировщик – класс, тестирующий код | Когда автоматический анализ даёт не того тестировщика | Строка с именем любого тестировщика из пакета src.testing.testers (class , function , method , stream ) |
RUNNER |
Пользовательская функция запуска теста (определяет как будет запускаться тест) | Когда нужно запускать тест по-своему | Функция |
VALIDATOR |
Пользовательский валидатор результата теста (функция, которая будет выдавать вердикт: прошел тест или нет) | Когда нужно по-своему настроить проверку результата работы кода | Функция |
TESTS |
Набор тестовых случаев, задаваемый программно (ручные и генеративные) | Когда ручного ввода в текстовом файле недостаточно | Последовательность тестовых случаев, представленных словарями |
Note
Наличие значения любого из параметров необязательно: они лишь позволяют настроить тестирование под свои нужды.
Обычно тест запускается так, как этого хочет выбранный тестировщик.
Однако можно запустить его иначе, по-своему обращаясь с целью тестирования (функция или класс).
Например, можно взять результат работы одного метода класса и передать его в другой.
Ниже представлена аннотация пользовательской функции запуска теста.
Чтобы он был активирован, его нужно присвоить переменной RUNNER
.
def runner(target: type | Callable, args: Sequence) -> Any:
"""
Пользовательская настройка запуска тестируемой цели.
:param target: Тестируемая цель (функция или класс).
:param args: Аргументы теста.
:returns: Результат выполнения кода.
"""
pass
RUNNER = runner
Пример
def runner(target: type | Callable, args: Sequence) -> Any:
"""
Сериализация и десериализация аргумента.
"""
obj = target()
s = obj.serialize(args[0])
result = obj.deserialize(s)
return result
Иногда результат выполнения кода может иметь несколько допустимых ответов, например, не зависеть от порядка элементов.
В таком случае пригодится написание своего валидатора, который будет сам выносить вердикт теста.
Ниже представлена аннотация пользовательского валидатора результата теста.
Аналогично, чтобы он был активирован, его нужно присвоить переменной VALIDATOR
.
def validator(
args_before: Sequence,
args_after: Sequence,
expected: Any,
result: Any
) -> bool:
"""
Валидация результата теста.
:param args_before: Аргументы теста до выполнения кода.
:param args_after: Аргументы теста после выполнения кода.
:param expected: Ожидаемый результат.
:param result: Результат выполнения кода.
:returns: True, если тест пройден, иначе False.
"""
pass
VALIDATOR = validator
Пример
def validator(
args_before: Sequence,
args_after: Sequence,
expected: Any,
result: Any
) -> bool:
"""
Убрать зависимость от порядка элементов.
"""
return sorted(expected) == sorted(result)
Тестовые случаи можно создавать следующими способами:
- Вводить вручную в текстовом файле
tests.txt
- Вводить программно в
setting.py
- Писать функции, которые их генерируют в
setting.py
Формат аргументов тестов и ожидаемых значений зависит от тестировщика,
поэтому формат тестовых случаев нужно смотреть в для каждого из тестировщиков в отдельности
(это касается ввода тестовых случаев как в tests.txt
, так и в setting.py
).
Рассмотрим лишь как в целом вводить тестовые случаи программно.
Они хранятся в переменной TESTS
. Каждый тестовый случай – словарь.
Всего есть два формата (ручной и генеративный):
def generator() -> tuple[Sequence, Any]:
"""
Генерация тестового случая.
:returns: Кортеж из двух элементов:
последовательность аргументов теста, ожидаемое значение.
"""
pass
TESTS = [
# Ручная запись тестового случая:
{
'args': [], # Аргументы теста.
'expected': None # Ожидаемое значение (не обязательно).
},
# Генерация тестовых случаев:
{
'generator': generator, # Функция, возвращающая тестовый случай.
'count': 100 # Количество вызовов функции (по умолчанию 1).
},
]
Тестировщик | Описание | Назначение |
---|---|---|
function | Выполняет функцию с заданными аргументами | Тестирование функции |
method | Выполняет метод класса с заданными аргументами | Тестирование метода класса |
class | Выполняет команды над классом, создавая его объект и вызывая его методы | Тестирование класса и его методов |
stream | Записывает входные данные в стандартный поток ввода, запускает функцию, считывает выходные данные с потока вывода | Тестирование модуля через стандартный поток ввода-вывода |
Используется, когда нужно тестировать одну функцию / один метод класса.
Формат в tests.txt
:
- Каждый аргумент находится в отдельной строке
- Наборы тестовых данных разделяются хотя бы одной пустой строкой
- Все значения должны быть типами данных, допустимыми форматом JSON
№ аргумента | Обязательный | Описание |
---|---|---|
1 .. N | Да | Аргумент теста |
N + 1 | Нет | Ожидаемый результат |
Пример
def remove_element(nums: List[int], val: int) -> int:
Тестовый случай 1:
Ввод: nums = [3,2,2,3]
, val = 3
Вывод: 2
Тестовый случай 2:
Ввод: nums = [0,1,2,2,3,0,4,2]
, val = 2
Вывод: 5
Формат записи тестовых случаев в tests.txt
:
[3,2,2,3]
3
2
[0,1,2,2,3,0,4,2]
2
5
Используется, когда нужно создавать объект тестируемого класса и вызывать его методы.
Формат в tests.txt
:
- Каждый аргумент находится в отдельной строке
- Наборы тестовых данных разделяются хотя бы одной пустой строкой
- Все значения должны быть типами данных, допустимыми форматом JSON
№ аргумента | Обязательный | Описание |
---|---|---|
1 | Да | Список строковых команд (первая – название класса, остальные – названия вызываемых методов) |
2 | Да | Списки аргументов каждой команды |
3 | Нет | Список ожидаемых результатов для каждой команды |
Пример
class LRUCache:
def __init__(self, capacity: int):
def get(self, key: int) -> int:
def put(self, key: int, value: int) -> None:
Тестовый случай:
Аргумент | Значение |
---|---|
Список команд | ["LRUCache","put","put","get","put","get","put","get","get","get"] |
Список аргументов команд | [[2],[1,1],[2,2],[1],[3,3],[2],[4,4],[1],[3],[4]] |
Список ожидаемых значений команд | [null,null,null,1,null,-1,null,-1,3,4] |
Как бы это выглядело в коде:
obj = LRUCache(2) # Создание объекта
obj.put(1, 1) # Ожидается, что вернет None
obj.put(2, 2) # Ожидается, что вернет None
obj.get(1) # Ожидается, что вернет 1
obj.put(3, 3) # Ожидается, что вернет None
obj.get(2) # Ожидается, что вернет -1
obj.put(4, 4) # Ожидается, что вернет None
obj.get(1) # Ожидается, что вернет -1
obj.get(3) # Ожидается, что вернет 3
obj.get(4) # Ожидается, что вернет 4
Формат записи тестовых случаев в tests.txt
:
["LRUCache","put","put","get","put","get","put","get","get","get"]
[[2],[1,1],[2,2],[1],[3,3],[2],[4,4],[1],[3],[4]]
[null,null,null,1,null,-1,null,-1,3,4]
Используется, когда нужно тестировать код используя операции ввода-вывода.
Формат в tests.txt
:
- Аргументы можно писать как в одной строке, так и разделив их на несколько строк.
- Входные и выходные данные разделяются минимум одной пустой строкой. Наборы тестовых данных разделяются каждой второй последовательностью, состоящей минимум из одной пустой строки. То есть нечетная последовательность пустых строк разделяет входные и выходные данные, а четная – тестовые случаи. Для удобства входные и выходные данные можно разделять одной пустой строкой, а тестовые случаи – тремя.
№ аргумента | Обязательный | Описание |
---|---|---|
1 | Да | Строка входных данных |
2 | Нет | Строка ожидаемых выходных данных |
Пример
Задача:
Вычислить сумму элементов для каждой из заданных последовательностей.
Формат входных данных:
Первая строка содержит целое число N — количество последовательностей.
Далее следуют N блоков данных, каждый из которых состоит из двух строк:
M — длина последовательности (целое число).
Последовательность чисел — строка из M чисел, разделённых пробелами.
Формат выходных данных:
N строк, где каждая строка содержит сумму чисел соответствующей последовательности.
Тестовый случай 1:
Ввод:
3
5
1 2 3 4 5
3
10 20 30
4
-1 5 0 -3
Вывод:
15
60
1
Тестовый случай 2:
Ввод:
2
2
100 -100
1
42
Вывод:
0
42
Формат записи тестовых случаев в tests.txt
:
3
5
1 2 3 4 5
3
10 20 30
4
-1 5 0 -3
15
60
1
2
2
100 -100
1
42
0
42
Классы, наследованные от класса Node
(односвязный список ListNode
, бинарное дерево BinaryTreeNode
и N-дерево NTreeNode
)
могут конвертироваться в список (объект Python типа list
) и обратно.
Формат хранения данных в виде списка аналогичен тому как это сделано в LeetCode.
В тестовых случаях объекты Node
должны быть представлены списками,
так как вводить их таком формате куда проще, чем работать с объектами.
Когда они передаются в тестируемый модуль, они становятся объектами Node
,
а когда выходят от туда, то снова становятся списками.
Примеры
1. Односвязный список.
- В формате списка:
[1, 2, 3, 4]
- Схематичный вид:
1 -> 2 -> 3 -> 4
2. Бинарное дерево.
В формате списка:
[1, 2, 3, 4, 5, null, 8, null, null, 6, 7, 9]
Схематичный вид:
(1)
/ \
(2) (3)
/ \ \
(4) (5) (8)
/ \ /
(6) (7) (9)
Алгоритм конвертации можно посмотреть здесь.
3. N-дерево.
В формате списка:
[1, null, 2, 3, 4, 5, null, null, 6, 7, 8, null, 9, null, null, 10, 11]
Схематичный вид:
_____________(1)______________
/ | | \
(2) __(3)__ (4) (5)
/ | \ |
(6) (7) (8) (9)
/ \
(10) (11)
Алгоритм конвертации можно посмотреть здесь.
Примеры решений с комментариями можно найти в папке solutions
.