Как создать собственную платформу для тестирования кодирования с помощью 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, чтобы помочь нам с нашими запросами POST
  • cors для запросов из разных источников
  • 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.