Розробка системи тестів і квізів для 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 })),
});
});
Процес роботи
- Аналітика — вивчаємо вашу LMS, список типів питань, вимоги до захисту.
- Проєктування — схема БД, API, рендеринг питань, логіка таймера.
- Реалізація — пишемо код, тестуємо на реальних даних.
- Тест — навантажувальне тестування, перевірка захисту, юзабіліті.
- Деплой і підтримка — розгортаємо, передаємо документацію, навчаємо адміністраторів.
Строки та що входить
| Компонент | Строк |
|---|---|
| 4–5 типів питань + таймер + спроби | 1–1.5 тижня |
| Додаткові типи (>5) | +2–3 дні |
| Інтеграція з існуючою LMS | від 3 днів |
| Ручна перевірка розгорнутих відповідей | від 2 днів |
Входить: API, клієнтський компонент таймера, адмін-панель керування тестами, міграції БД, документація, навчання, підтримка після запуску.
Отримайте консультацію щодо вашого проєкту — розкажемо, які типи питань підійдуть саме вам. Замовте розробку системи тестів: від простого опитування до сертифікаційного іспиту. Досвід нашої команди — 5+ років, понад 50 впроваджень тестових систем у різних LMS. Гарантуємо стабільність та безпеку даних.







