Как создать собственную платформу для тестирования кодирования с помощью Node.js

В наши дни все больше и больше компаний используют тесты кодирования при приеме на работу разработчиков программного обеспечения. Такие сайты, как HackerRank, Codility и т. Д., Помогают компаниям проводить эти тесты и оценивать кандидатов на основе производительности их кода при выполнении некоторых тестовых примеров.
В этой статье я опишу, как создать простое веб-приложение для администрирования теста кодирования. Веб-приложение представит пользователям проблему, позволит пользователю отправить свое решение, запустить решение в некоторых предопределенных тестовых случаях и отобразить результаты пользователю.
Мы будем работать с Express.js для веб-сервера, а Python будет языковыми решениями, на которых будут представлены.
Подготовка сцены
Мы начнем с создания пользовательского интерфейса, который будет состоять из области вопросов и области кодирования.
Для этого сначала создадим по два элемента div для каждой области. Затем мы их стилизуем:
<div class="question"> <h2><strong>QUESTION</strong></h2> <span id="questionText"></span> </div> <div class="code-area"> <textarea rows="20" autofocus>def solution(arr):</textarea> </div>
Затем мы стилизуем два div:
.question, .code-area {
padding: 25px;
display: table-cell;
width: 50%;
}
.question {
text-align: left;
position: absolute;
top: 0vh;
}
.code-area {
border-left: 2px solid navy;
}
В приведенном выше CSS мы создаем две ячейки таблицы div (чтобы они могли располагаться рядом). Нам нужна таблица, в которой они будут содержаться. Для этого мы создадим содержащий div:
<div class="container">
<div class="question">
...
</div>
<div class="code-area">
...
</div>
</div>
А затем стилизуем его:
.container {
display: table;
height: 95vh;
width: 95vw;
}
Мы также добавим немного стиля к элементу body, чтобы изменить цвет фона и шрифт:
body {
background-color: aliceblue;
text-align: center;
font-size: 16pt;
font-family: 'Gill Sans', 'Gill Sans MT', Calibri, 'Trebuchet MS', sans-serif;
}
Ранее мы создали элемент textarea, текст которого по умолчанию был def solution(arr):. Дадим этому textarea класс и идентификатор:
<textarea rows="20" class="input" id="code" autofocus>def solution(arr):</textarea>
Давайте также создадим элемент button для выполнения тестов и элемент span для результатов:
... <textarea rows="20" class="input" id="code" autofocus>def solution(arr):</textarea> <br/><br/> <button>RUN TESTS</button> <br/><br/> <span id="results"></span> ...
А теперь давайте добавим стили к элементам:
.input, #results {
height: 60%;
}
.input {
border: 1px solid gray;
width: 90%;
font-family: monospace;
font-size: 12pt;
padding: 10px;
}
button {
width: 150px;
height: 50px;
cursor: pointer;
background-color: lightgreen;
color: white;
font-weight: bolder;
}
На этом наша работа над дизайном пользовательского интерфейса завершена. Веб-страница теперь выглядит так:

Подготовка бэкенда
На другом конце веб-приложения мы начнем с импорта необходимых библиотек:
const bodyParser = require('body-parser');
const cors = require('cors');
const execSync = require('child_process').execSync;
const express = require('express');
const fs = require('fs');
const path = require('path');
Нам нужно:
body-parser, чтобы помочь нам с нашими запросами POSTcorsдля запросов из разных источниковchild_process,fsиpathдля выполнения решения кода пользователя.express, очевидно.
Затем мы создадим приложение Express и настроим его на использование необходимых нам зависимостей:
const app = express();
app.use(cors());
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
И, наконец, мы настроим конечные точки и заставим приложение прослушивать порт 5000:
function testCode(req, res) {
return res.send("Success");
}
app.get('/', (req, res) => {
res.send("Hello world");
});
app.post('/test/', testCode);
app.listen(5000, () =>
console.log(`Listening on port 5000.`),
);
У нас есть две конечные точки выше:
/: для обслуживания интерфейсной страницы и/test/: для тестирования пользовательского кода.
Конечная точка /test/ при посещении вызывает функцию testCode. Эта функция запишет код пользователя в файл Python, а затем запустит другой скрипт Python для проверки кода:
...
const CODE_FOLDER = "code";
function testCode(req, res) {
let code = req.body["code"];
try {
fs.writeFileSync(path.join(__dirname, CODE_FOLDER, "input_code.py"), code);
const proc = execSync("python3 " + path.join(CODE_FOLDER, "tests.py"));
const results = proc.toString();
return res.send(results);
} catch (error) {
console.log("An error occurred");
console.log(error);
return res.send("An error occurred.");
}
}
В приведенном выше коде код, который вводит пользователь, который будет отправлен в качестве параметра запроса с именем code из внешнего интерфейса, будет сохранен в файле с именем input_code.py, затем скрипт Python tests.py будет выполнен с использованием функции execSync узла (которая выполняет консольная команда, ожидает ее вывода и возвращает его). Затем вывод скрипта будет отправлен во внешний интерфейс.
Вот что содержит tests.py скрипт:
from input_code import solution def get_test_cases(): pass def get_expected_outputs(): pass def test_code(): pass if __name__ == '__main__': test_code()
Он содержит три функции:
get_test_cases(): чтобы тестовые примеры запускали код пользователяget_expected_outputs(): чтобы получить ожидаемый результат для каждого тестового примераtest_code(): для проверки решения пользователя на тестовых примерах.
Давайте заполним эти функции:
def get_test_cases():
return {
"SMALL_INPUT": [1, 2, 3],
"LARGE_INPUT": [1, 2, 3] * 1000 + [4],
}
def get_expected_outputs():
return {
"SMALL_INPUT": 3,
"LARGE_INPUT": 4,
}
Эти первые две функции предоставляют нам ввод и вывод некоторых примеров тестов. Они также помечают тестовые примеры “SMALL_INPUT” и “LARGE_INPUT” для идентификации.
Теперь давайте используем их в третьей функции для проверки пользовательского кода:
def test_code():
test_cases = get_test_cases()
expected = get_expected_outputs()
test_cases_count = len(test_cases)
passed_test_cases = 0
failed_test_cases = []
for label in test_cases.keys():
code_result = solution(test_cases[label])
if code_result == expected[label]:
passed_test_cases += 1
else:
failed_test_cases.append(label)
print("Passed", passed_test_cases, "out of", test_cases_count, "test cases.")
if len(failed_test_cases) > 0:
print("Test cases not passed:", ", ".join(failed_test_cases))
С помощью приведенного выше кода мы перебираем каждый тестовый пример, вызываем решение пользователя на входе тестового примера и сравниваем результат пользователя с ожидаемым результатом.
Если результаты совпадают, мы добавляем единицу к количеству пройденных тестов; в противном случае мы добавляем метку неудачного тестового примера в массив.
Наконец, мы печатаем количество пройденных тестовых случаев, а если есть какие-то неудачные, печатаем их метки. Обратите внимание, что код пользователя должен содержать функцию с именем solution(), которая будет передаваться через массив для каждого тестового примера и должна возвращать число.
На этом задняя часть завершена.
Соединение двух концов
Наша следующая задача - подключить переднюю часть к задней.
Для этого мы создадим функцию JavaScript, которая будет вызываться всякий раз, когда пользователь нажимает кнопку RUN TESTS. Эта функция отправит код пользователя в серверную часть и отобразит результаты пользователя.
<script>
function runTests() {
document.getElementById("results").innerHTML = "Running...";
const code = document.getElementById("code").value;
let xhr = new XMLHttpRequest();
xhr.onreadystatechange = () => {
if (xhr.readyState == 4 && xhr.status == 200) {
document.getElementById("results").innerHTML = xhr.responseText;
}
}
xhr.open("POST", "http://localhost:5000/test/");
xhr.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
xhr.send("code=" + code);
}
</script>
Функция сначала получает код пользователя из textarea с идентификатором code, а затем использует AJAX для отправки запроса в конечную точку /test/ серверной части. Затем, когда он получает ответ, он устанавливает результаты в span с идентификатором results.
Давайте добавим этот обработчик событий к кнопке RUN TESTS:
<button onclick="runTests()">RUN TESTS</button>
Тестирование приложения
Чтобы протестировать веб-приложение, давайте добавим простой вопрос в область вопросов:
<div class="question"> <h2><strong>QUESTION</strong></h2> Find the maximum number in an array of integers. <br/> For example, in the array [1, -3, 5], the maximum number is 5. </div>
Вопрос просто просит пользователя найти наибольшее число в массиве. Вот решение проблемы:
def solution(arr): maximum = arr[0] return maximum
Решение возвращает первый элемент в массиве (что явно неверно). Нажатие кнопки RUN TESTS дает нам следующее:

Результаты теста говорят нам, что решение неверное, поскольку оно не проходит ни один тестовый пример. В нем также указано, что имена тестовых примеров не помогли пользователю правильно настроить свой код.
Теперь давайте попробуем со следующим кодом:
def solution(arr):
maximum = 0
for elem in arr:
if elem > maximum:
maximum = elem
return maximum
Это решение просматривает массив и проверяет, какой элемент является наибольшим, сравнивая каждый элемент с ранее известным наибольшим элементом. Скорее всего, это правильное решение, но давайте проверим его, чтобы подтвердить:

Код прошел оба тестовых случая! Наше веб-приложение для тестирования кодирования работает должным образом.
Спасибо, что дошли до этого места. Вы можете найти весь код веб-приложения в моем репозитории GitHub.