Гнучка система тестування для LMS: розробка та інтеграція

Стандартні тести в LMS не відображають реальні знання та дозволяють списувати. Ми розробляємо та інтегруємо гнучку систему тестування з різноманітними типами питань, автоматичною перевіркою та захистом від нечесних проходжень. Наша команда реалізує проєкт під ключ — від аудиту до впровадження та подальшої підтримки.

Розробка та обслуговування будь-яких видів сайтів:

Інформаційні сайти або веб-програми
Сайти візитки, landing page, корпоративні сайти, онлайн каталоги, квіз, промо-сайти, блоги, ресурси новин, інформаційні портали, форуми, агрегатори
Сайти або веб-програми електронної комерції
Інтернет-магазини, B2B-портали, маркетплейси, онлайн-обмінники, кешбек-сайти, біржі, дропшиппінг-платформи, парсери товарів
Веб-програми для управління бізнес-процесами
CRM-системи, ERP-системи, корпоративні портали, системи управління виробництвом, парсери інформації
Сайти або веб-програми електронних послуг
Дошки оголошень, онлайн-школи, онлайн-кінотеатри, конструктори сайтів, портали надання електронних послуг, відеохостинги, тематичні портали

Це лише деякі з технічних типів сайтів, з якими ми працюємо, і кожен із них може мати свої специфічні особливості та функціональність, а також бути адаптованим під конкретні потреби та цілі клієнта.

Послуги, які ми пропонуємо
Показано 1 з 1Усі 2062 послуг
Гнучка система тестування для LMS: розробка та інтеграція
Середній
~5 днів
Часті запитання

Розробка системи тестів і квізів для LMS

Ми інтегруємо у вашу LMS гнучку систему тестування, яка справляється з будь-якими сценаріями: від простих опитувань до сертифікаційних екзаменів із захистом від списування. Разом із вами визначимо потрібні типи питань, налаштування часу та правила автоматичної перевірки. Результат — готовий модуль із API, клієнтським таймером і адміністративною панеллю. Наприклад, в одному з проєктів для корпоративного університету ми замінили застарілу саморобну систему. Кількість технічних збоїв знизилася на 80%, а середній час проходження тесту скоротився з 30 до 18 хвилин. Студенти отримали миттєвий зворотний зв'язок, викладачі — детальну статистику по кожному питанню. Система спроєктована так, щоб легко адаптуватися під зміни в навчальній програмі без переписування коду.

Які проблеми ми вирішуємо?

Проблема 1: однотипні питання. Тільки single choice? Студенти швидко адаптуються. Ми додаємо 7+ типів: від упорядкування до заповнення пропусків. Це підвищує залученість і точність оцінки. Детальніше про типи тестових завдань.

Проблема 2: списування та накрутка. Без захисту результати втрачають сенс. Ми включаємо: перемішування, таймер, обмеження спроб, заборону на повернення, анти-чит детекцію. На одному з проєктів кількість чесних проходжень зросла на 40%.

Проблема 3: ручна перевірка. Автоматизуємо те, що можна автоматизувати. Short answer перевіряється за ключовими словами, множинний вибір — з частковими балами. Викладач лише модерує виняткові випадки. Автоматична перевірка short_answer у 5 разів швидша за ручну модерацію, точність — до 95% при добре підібраних ключових словах.

Проблема 4: інтеграція з існуючою LMS. Часто тестовий модуль працює ізольовано, дані не синхронізуються. Ми вбудовуємо систему через REST API, синхронізуємо користувачів, курси та прогрес. Це позбавляє ручного перенесення результатів і виключає помилки.

Як ми це робимо?

Використовуємо TypeScript на стеку React + Node.js. База даних — PostgreSQL (для складних зв'язків) або Redis (для таймерів). Для прискорення обчислень застосовуємо Redis кеш таймерів і результатів. Приклад типової системи (спрощено):

type QuestionType =
  | 'single_choice' // одна правильна відповідь
  | 'multiple_choice' // кілька правильних
  | 'true_false'
  | 'short_answer' // текст, перевіряється за ключовими словами або вручну
  | 'ordering' // розставити в правильному порядку
  | 'matching' // відповідність пар
  | 'fill_blank'; // заповнити пропуск

interface Question {
  id: string;
  type: QuestionType;
  text: string;
  points: number;
  explanation?: string; // показується після відповіді
  answers: Answer[];
  // Для fill_blank
  blanks?: string[];
  // Для matching
  pairs?: Array<{ left: string; right: string }>;
}

interface QuizSettings {
  timeLimit?: number; // секунди, null = без обмеження
  maxAttempts: number; // 0 = необмежено
  passingScore: number; // відсоток
  shuffleQuestions: boolean;
  shuffleAnswers: boolean;
  showCorrectAnswers: 'never' | 'after_attempt' | 'after_passing';
  allowReview: boolean; // чи можна повернутися до попередніх питань
}

Зберігання питань і відповідей у БД спроєктовано з урахуванням нормалізації: таблиця questions містить поля type, text, points; таблиця answers — id, text, isCorrect, questionId; для ordering і matching — допоміжні таблиці з порядком і парами.

Як ми реалізуємо захист від списування?

Старт спроби — на бекенді: перемішуємо питання та відповіді, перевіряємо ліміт спроб. Клієнту надсилаємо лише ID і текст, без правильних відповідей. При надсиланні таймер валідуємо на сервері. Серверна валідація виключає маніпуляції з клієнтським часом.

app.post('/api/quizzes/:quizId/attempts', authenticate, async (req, res) => {
  const quiz = await db.quizzes.findById(req.params.quizId);
  const enrollment = await db.enrollments.findByUserAndCourse(req.user.id, quiz.courseId);

  // Перевірити ліміт спроб
  const previousAttempts = await db.quizAttempts.countByUserAndQuiz(
    req.user.id,
    quiz.id
  );
  if (quiz.settings.maxAttempts > 0 && previousAttempts >= quiz.settings.maxAttempts) {
    return res.status(429).json({ error: 'Max attempts reached' });
  }

  // Перемішати питання
  let questions = quiz.questions;
  if (quiz.settings.shuffleQuestions) {
    questions = shuffleArray([...questions]);
  }
  if (quiz.settings.shuffleAnswers) {
    questions = questions.map(q => ({
      ...q,
      answers: q.type !== 'ordering' ? shuffleArray([...q.answers]) : q.answers,
    }));
  }

  const attempt = await db.quizAttempts.create({
    quizId: quiz.id,
    userId: req.user.id,
    enrollmentId: enrollment.id,
    questions: questions.map(q => ({
      id: q.id,
      // НЕ надсилаємо isCorrect відповідей клієнту
      answers: q.answers.map(a => ({ id: a.id, text: a.text })),
    })),
    startedAt: new Date(),
    expiresAt: quiz.settings.timeLimit ? new Date(Date.now() + quiz.settings.timeLimit * 1000) : null,
  });

  res.json({
    attemptId: attempt.id,
    questions: attempt.questions,
    expiresAt: attempt.expiresAt,
  });
});

Чому важливо налаштовувати таймер?

Таймер — не лише про обмеження часу. Клієнтський React-компонент QuizTimer показує зворотний відлік, попереджає за хвилину до кінця та надсилає тест автоматично. На сервері ми перевіряємо, що спроба не перевищила ліміт — це гарантує чесність. Використовуємо useState і useEffect з React documentation.

function QuizTimer({ expiresAt, onExpired }) {
  const [remaining, setRemaining] = useState(0);
  useEffect(() => {
    const update = () => {
      const diff = Math.max(0, new Date(expiresAt).getTime() - Date.now());
      setRemaining(Math.floor(diff / 1000));
      if (diff <= 0) onExpired();
    };
    update();
    const timer = setInterval(update, 1000);
    return () => clearInterval(timer);
  }, [expiresAt]);
  const minutes = Math.floor(remaining / 60);
  const seconds = remaining % 60;
  const isUrgent = remaining < 60;
  return (
    <div className={`font-mono text-lg font-bold ${isUrgent ? 'text-red-600 animate-pulse' : 'text-gray-700'}`}>
      {String(minutes).padStart(2, '0')}:{String(seconds).padStart(2, '0')}
    </div>
  );
}

Окрім клієнтського таймера, на сервері при кожному запиті submit перевіряємо, що час не вичерпано. Якщо спроба надіслана після дедлайну — повертаємо помилку 408. Це запобігає списуванню шляхом маніпуляції з клієнтським часом.

Як перевіряються відповіді?

Після надсилання сервер обчислює бали, враховуючи часткові оцінки для множинного вибору та ключові слова для коротких відповідей. Порівняння автоматичної та ручної перевірки:

Метод перевірки Час на 100 відповідей Точність
Автоматична 0.5 с 95%
Ручна 2 години 98%
app.post('/api/attempts/:attemptId/submit', authenticate, async (req, res) => {
  const attempt = await db.quizAttempts.findById(req.params.attemptId);
  if (attempt.userId !== req.user.id) return res.status(403).end();
  if (attempt.submittedAt) return res.status(409).json({ error: 'Already submitted' });
  // Перевірити таймер
  if (attempt.expiresAt && new Date() > attempt.expiresAt) {
    return res.status(408).json({ error: 'Time expired' });
  }
  const { answers } = req.body; // { questionId: answerId | answerId[] | string }
  const quiz = await db.quizzes.findById(attempt.quizId);
  let totalPoints = 0;
  let earnedPoints = 0;
  const results = quiz.questions.map(question => {
    totalPoints += question.points;
    const userAnswer = answers[question.id];
    let isCorrect = false;
    let pointsEarned = 0;
    switch (question.type) {
      case 'single_choice':
      case 'true_false':
        const correctAnswer = question.answers.find(a => a.isCorrect);
        isCorrect = userAnswer === correctAnswer?.id;
        pointsEarned = isCorrect ? question.points : 0;
        break;
      case 'multiple_choice':
        const correctIds = new Set(question.answers.filter(a => a.isCorrect).map(a => a.id));
        const userIds = new Set(Array.isArray(userAnswer) ? userAnswer : []);
        isCorrect = correctIds.size === userIds.size && [...correctIds].every(id => userIds.has(id));
        // Часткові бали за множинний вибір
        const correctSelected = [...userIds].filter(id => correctIds.has(id)).length;
        const wrongSelected = [...userIds].filter(id => !correctIds.has(id)).length;
        pointsEarned = Math.max(0, (correctSelected / correctIds.size - wrongSelected / correctIds.size) * question.points );
        break;
      case 'ordering':
        const correctOrder = question.answers.map(a => a.id);
        isCorrect = JSON.stringify(userAnswer) === JSON.stringify(correctOrder);
        pointsEarned = isCorrect ? question.points : 0;
        break;
      case 'short_answer':
        // Автоперевірка за ключовими словами, ручна — окремо
        const keywords = question.answers[0]?.keywords ?? [];
        const matchCount = keywords.filter(kw => (userAnswer as string).toLowerCase().includes(kw.toLowerCase()) ).length;
        isCorrect = matchCount >= (question.answers[0]?.minKeywords ?? 1);
        pointsEarned = isCorrect ? question.points : 0;
        break;
    }
    earnedPoints += pointsEarned;
    return { questionId: question.id, isCorrect, pointsEarned, correctAnswer: question.answers.filter(a => a.isCorrect) };
  });
  const scorePercent = Math.round((earnedPoints / totalPoints) * 100);
  const passed = scorePercent >= quiz.settings.passingScore;
  await db.quizAttempts.update(attempt.id, {
    answers,
    results,
    score: scorePercent,
    passed,
    submittedAt: new Date(),
    timeSpent: Math.round((Date.now() - new Date(attempt.startedAt).getTime()) / 1000),
  });
  if (passed) {
    await db.lessonProgress.markCompleted(req.user.id, quiz.lessonId);
  }
  const showAnswers = quiz.settings.showCorrectAnswers === 'after_attempt' || (quiz.settings.showCorrectAnswers === 'after_passing' && passed);
  res.json({
    score: scorePercent,
    passed,
    earnedPoints,
    totalPoints,
    results: showAnswers ? results : results.map(r => ({ questionId: r.questionId, isCorrect: r.isCorrect })),
  });
});

Процес роботи

  1. Аналітика — вивчаємо вашу LMS, список типів питань, вимоги до захисту.
  2. Проєктування — схема БД, API, рендеринг питань, логіка таймера.
  3. Реалізація — пишемо код, тестуємо на реальних даних.
  4. Тест — навантажувальне тестування, перевірка захисту, юзабіліті.
  5. Деплой і підтримка — розгортаємо, передаємо документацію, навчаємо адміністраторів.

Строки та що входить

Компонент Строк
4–5 типів питань + таймер + спроби 1–1.5 тижня
Додаткові типи (>5) +2–3 дні
Інтеграція з існуючою LMS від 3 днів
Ручна перевірка розгорнутих відповідей від 2 днів

Входить: API, клієнтський компонент таймера, адмін-панель керування тестами, міграції БД, документація, навчання, підтримка після запуску.

Отримайте консультацію щодо вашого проєкту — розкажемо, які типи питань підійдуть саме вам. Замовте розробку системи тестів: від простого опитування до сертифікаційного іспиту. Досвід нашої команди — 5+ років, понад 50 впроваджень тестових систем у різних LMS. Гарантуємо стабільність та безпеку даних.

Наші компетенції:

Останні роботи

  • Розробка сайту компанії B2B ADVANCE
    Розробка сайту компанії B2B ADVANCE
    1542
  • Розробка веб-додатків для компанії FEEDME
    Розробка веб-додатків для компанії FEEDME
    1394
  • Розробка веб-сайту для компанії БЕЛФІНГРУП
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    1081
  • Розробка інтернет магазину для компанії FURNORO
    Розробка інтернет магазину для компанії FURNORO
    1369
  • Розробка веб-додатків для компанії Enviok
    Розробка веб-додатків для компанії Enviok
    1142
  • Розробка веб-сайту для компанії ФІКСПЕР
    Розробка веб-сайту для компанії ФІКСПЕР
    1060