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.