From d75a1cb6f91eeb51160907edeac533c67b112e31 Mon Sep 17 00:00:00 2001 From: KAGmay05 Date: Sat, 13 Dec 2025 15:20:48 -0500 Subject: [PATCH] final tests implemented --- .../migration.sql | 2 + prisma/schema.prisma | 2 +- prisma/seeds/seeds_Exam.ts | 13 +- .../exam/dto/generated-exam.dto.ts | 8 - .../exam_related/exam/exam.service.spec.ts | 246 +++++++++++++++++- .../exam_question.service.spec.ts | 184 ++++++++++++- .../exam_question/exam_question.service.ts | 9 - .../dto/create-exam_student.dto.ts | 5 +- .../exam_student/exam_student.service.spec.ts | 165 +++++++++++- 9 files changed, 603 insertions(+), 31 deletions(-) create mode 100644 prisma/migrations/20251213195700_score_field_optional_in_exam_student_model/migration.sql diff --git a/prisma/migrations/20251213195700_score_field_optional_in_exam_student_model/migration.sql b/prisma/migrations/20251213195700_score_field_optional_in_exam_student_model/migration.sql new file mode 100644 index 0000000..62d4ea6 --- /dev/null +++ b/prisma/migrations/20251213195700_score_field_optional_in_exam_student_model/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Exam_Student" ALTER COLUMN "score" DROP NOT NULL; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index a36cacf..24c02a0 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -123,7 +123,7 @@ model Sub_Topic { } model Exam_Student { - score Float + score Float? exam_id Int student_id Int teacher_id Int diff --git a/prisma/seeds/seeds_Exam.ts b/prisma/seeds/seeds_Exam.ts index 662dc28..3044b26 100644 --- a/prisma/seeds/seeds_Exam.ts +++ b/prisma/seeds/seeds_Exam.ts @@ -126,7 +126,14 @@ export async function seed_exams(prisma: PrismaClient) { if (maxScore <= 1) return 0; return Math.floor(Math.random() * (maxScore - 1)) + 1; // 1..(max-1) } - + function getCorrectProbability(difficulty: string): number { + switch(difficulty.toLowerCase()){ + case 'facil': return 0.95; + case 'medio': return 0.75; + case 'dificil': return 0.6; + default: return 0.7; + } + } // ---------------------------- // 🔥 CREAR RESPUESTAS @@ -136,7 +143,9 @@ export async function seed_exams(prisma: PrismaClient) { const question = questions.find(q => q.id === eq.question_id)!; for (const student of students) { - const isCorrect = Math.random() < 0.6; // 60% correctas + const exam = createdExams.find(e => e.id === eq.exam_id)!; + const prob = getCorrectProbability(exam.difficulty); + const isCorrect = Math.random() < prob; let answerText = ""; let score = 0; diff --git a/src/modules/exam_related/exam/dto/generated-exam.dto.ts b/src/modules/exam_related/exam/dto/generated-exam.dto.ts index f56d6d5..97ac02c 100644 --- a/src/modules/exam_related/exam/dto/generated-exam.dto.ts +++ b/src/modules/exam_related/exam/dto/generated-exam.dto.ts @@ -20,14 +20,6 @@ export class GenerateExamDto { @IsInt() subject_id: number; - @Type(() => Number) - @IsInt() - teacher_id: number; - - @Type(() => Number) - @IsInt() - head_teacher_id: number; - @IsArray() @ValidateNested({ each: true }) @Type(() => QuestionDistributionDto) diff --git a/src/modules/exam_related/exam/exam.service.spec.ts b/src/modules/exam_related/exam/exam.service.spec.ts index 4b73e9c..b0d0d0d 100644 --- a/src/modules/exam_related/exam/exam.service.spec.ts +++ b/src/modules/exam_related/exam/exam.service.spec.ts @@ -1,18 +1,258 @@ import { Test, TestingModule } from '@nestjs/testing'; import { ExamService } from './exam.service'; +import { PrismaService } from 'src/prisma/prisma.service'; +import { BadRequestException, NotFoundException } from '@nestjs/common'; describe('ExamService', () => { let service: ExamService; + let prismaMock: any; beforeEach(async () => { + prismaMock = { + exam: { + create: jest.fn(), + findUnique: jest.fn(), + findMany: jest.fn(), + update: jest.fn(), + delete: jest.fn(), + }, + exam_Question: { + createMany: jest.fn(), + findMany: jest.fn(), + }, + question: { + findMany: jest.fn(), + }, + $transaction: jest.fn(), + }; + const module: TestingModule = await Test.createTestingModule({ - providers: [ExamService], + providers: [ + ExamService, + { + provide: PrismaService, + useValue: prismaMock, + }, + ], }).compile(); service = module.get(ExamService); }); - it('should be defined', () => { - expect(service).toBeDefined(); + // ============================================================ + // 🧪 CREATE + // ============================================================ + + it('create debe crear un examen y asociar preguntas', async () => { + prismaMock.$transaction.mockImplementation(async (cb) => + cb({ + exam: { + create: jest.fn().mockResolvedValue({ id: 1 }), + findUnique: jest.fn().mockResolvedValue({ + id: 1, + exam_questions: [], + }), + }, + exam_Question: { + createMany: jest.fn(), + }, + }), + ); + + const result = await service.create( + { + name: 'Parcial', + status: 'Borrador', + difficulty: 'Media', + subject_id: 1, + teacher_id: 2, + parameters_id: 3, + head_teacher_id: 4, + }, + [10, 11], + ); + + expect(result?.id).toBe(1); + }); + + // ============================================================ + // 🧪 GENERATED + // ============================================================ + it('generated debe insertar una nueva combinación válida', async () => { + prismaMock.question.findMany.mockResolvedValue([ + { id: 1, type: 'Multiple', subject_id: 1 }, + { id: 2, type: 'Multiple', subject_id: 1 }, + ]); + + prismaMock.exam_Question.findMany.mockResolvedValue([ + { question_id: 1 }, // combinación previa distinta + ]); + + prismaMock.exam_Question.createMany.mockResolvedValue({ count: 1 }); + + const result = await service.generated({ + exam_id: 1, + subject_id: 1, + questionDistribution: [ + { type: 'Multiple', amount: 1 }, + ], + }); + + expect(prismaMock.exam_Question.createMany).toHaveBeenCalledWith({ + data: [{ exam_id: 1, question_id: expect.any(Number) }], + }); + + expect(result.questions_added).toBe(1); +}); + + + + it('generated debe lanzar error si no hay suficientes preguntas', async () => { + prismaMock.question.findMany.mockResolvedValue([]); + + await expect( + service.generated({ + exam_id: 1, + subject_id: 1, + questionDistribution: [ + { type: 'Multiple', amount: 2 }, + ], + }), + ).rejects.toThrow(NotFoundException); + }); + + it('generated debe lanzar error si no existen combinaciones', async () => { + prismaMock.question.findMany.mockResolvedValue([ + { id: 1, type: 'Multiple', subject_id: 1 }, + ]); + + prismaMock.exam_Question.findMany.mockResolvedValue([ + { question_id: 1 }, + ]); + + await expect( + service.generated({ + exam_id: 1, + subject_id: 1, + questionDistribution: [ + { type: 'Multiple', amount: 1 }, + ], + }), + ).rejects.toThrow(BadRequestException); + }); + // ============================================================ + // 🧪 Task1 + // ============================================================ + it('listGeneratedExamsBySubject debe retornar exámenes generados por asignatura', async () => { + prismaMock.exam.findMany.mockResolvedValue([ + { + id: 10, + name: 'Parcial Matemática', + status: 'GENERADO', + difficulty: 'MEDIA', + subject_id: 3, + teacher: { + id: 5, + specialty: 'Álgebra', + user: { + id_us: 20, + name: 'Ana López', + }, + }, + parameters: { + id: 7, + }, + }, + ]); + + const result = await service.listGeneratedExamsBySubject(3); + + expect(prismaMock.exam.findMany).toHaveBeenCalledWith({ + where: { + subject_id: 3, + }, + select: { + id: true, + name: true, + status: true, + difficulty: true, + subject_id: true, + teacher: { + select: { + id: true, + specialty: true, + user: { + select: { + id_us: true, + name: true, + }, + }, + }, + }, + parameters: { + select: { + id: true, + }, + }, + }, + orderBy: { + id: 'desc', + }, + }); + + expect(result).toHaveLength(1); + expect(result[0].name).toBe('Parcial Matemática'); +}); + + + // ============================================================ + // 🧪 CRUD + // ============================================================ + + it('findAll debe retornar examenes con relaciones', async () => { + prismaMock.exam.findMany.mockResolvedValue([{ id: 1 }]); + + const result = await service.findAll(); + + expect(prismaMock.exam.findMany).toHaveBeenCalled(); + expect(result).toEqual([{ id: 1 }]); + }); + + it('findOne debe retornar un examen', async () => { + prismaMock.exam.findUnique.mockResolvedValue({ id: 1 }); + + const result = await service.findOne(1); + + expect(prismaMock.exam.findUnique).toHaveBeenCalledWith({ + where: { id: 1 }, + include: expect.any(Object), + }); + + expect(result).toEqual({ id: 1 }); + }); + + it('update debe actualizar el examen', async () => { + prismaMock.exam.update.mockResolvedValue({ id: 1, name: 'Actualizado' }); + + const result = await service.update(1, { name: 'Actualizado' }); + + expect(prismaMock.exam.update).toHaveBeenCalledWith({ + where: { id: 1 }, + data: { name: 'Actualizado' }, + }); + + expect(result.name).toBe('Actualizado'); + }); + + it('remove debe eliminar el examen', async () => { + prismaMock.exam.delete.mockResolvedValue({ id: 1 }); + + const result = await service.remove(1); + + expect(prismaMock.exam.delete).toHaveBeenCalledWith({ + where: { id: 1 }, + }); + + expect(result.id).toBe(1); }); }); diff --git a/src/modules/exam_related/exam_question/exam_question.service.spec.ts b/src/modules/exam_related/exam_question/exam_question.service.spec.ts index 61311b5..119d20b 100644 --- a/src/modules/exam_related/exam_question/exam_question.service.spec.ts +++ b/src/modules/exam_related/exam_question/exam_question.service.spec.ts @@ -1,18 +1,196 @@ import { Test, TestingModule } from '@nestjs/testing'; import { ExamQuestionService } from './exam_question.service'; +import { PrismaService } from 'src/prisma/prisma.service'; describe('ExamQuestionService', () => { let service: ExamQuestionService; + let prismaMock: any; beforeEach(async () => { + prismaMock = { + exam_Question: { + create: jest.fn(), + findMany: jest.fn(), + findUnique: jest.fn(), + update: jest.fn(), + delete: jest.fn(), + groupBy: jest.fn(), + }, + question: { + findUnique: jest.fn(), + }, + }; + const module: TestingModule = await Test.createTestingModule({ - providers: [ExamQuestionService], + providers: [ + ExamQuestionService, + { + provide: PrismaService, + useValue: prismaMock, + }, + ], }).compile(); service = module.get(ExamQuestionService); }); - it('should be defined', () => { - expect(service).toBeDefined(); + // ====================================================== + // 🧪 CRUD BÁSICO + // ====================================================== + + it('create debe crear una relación exam_question', async () => { + prismaMock.exam_Question.create.mockResolvedValue({ + exam_id: 1, + question_id: 2, + }); + + const result = await service.create({ + exam_id: 1, + question_id: 2, + }); + + expect(prismaMock.exam_Question.create).toHaveBeenCalledWith({ + data: { + exam_id: 1, + question_id: 2, + }, + }); + + expect(result.exam_id).toBe(1); + }); + + it('findAll debe retornar relaciones con exam y question', async () => { + prismaMock.exam_Question.findMany.mockResolvedValue([{ id: 1 }]); + + const result = await service.findAll(); + + expect(prismaMock.exam_Question.findMany).toHaveBeenCalledWith({ + include: { + exam: true, + question: true, + }, + }); + + expect(result).toEqual([{ id: 1 }]); + }); + + it('findOne debe buscar por clave compuesta', async () => { + prismaMock.exam_Question.findUnique.mockResolvedValue({ id: 1 }); + + const result = await service.findOne(1, 2); + + expect(prismaMock.exam_Question.findUnique).toHaveBeenCalledWith({ + where: { + exam_id_question_id: { + exam_id: 1, + question_id: 2, + }, + }, + include: { + exam: true, + question: true, + }, + }); + + expect(result).toEqual({ id: 1 }); + }); + + it('update debe actualizar la relación', async () => { + prismaMock.exam_Question.update.mockResolvedValue({ id: 1 }); + + const result = await service.update(1, 2, {}); + + expect(prismaMock.exam_Question.update).toHaveBeenCalledWith({ + where: { + exam_id_question_id: { + exam_id: 1, + question_id: 2, + }, + }, + data: {}, + }); + + expect(result).toEqual({ id: 1 }); + }); + + it('remove debe eliminar la relación', async () => { + prismaMock.exam_Question.delete.mockResolvedValue({ id: 1 }); + + const result = await service.remove(1, 2); + + expect(prismaMock.exam_Question.delete).toHaveBeenCalledWith({ + where: { + exam_id_question_id: { + exam_id: 1, + question_id: 2, + }, + }, + }); + + expect(result).toEqual({ id: 1 }); + }); + + // ====================================================== + // 🧪 TASK 3 — listMostUsedQuestions + // ====================================================== + + it('listMostUsedQuestions debe retornar preguntas ordenadas por uso', async () => { + prismaMock.exam_Question.groupBy.mockResolvedValue([ + { question_id: 1, _count: { question_id: 3 } }, + { question_id: 2, _count: { question_id: 1 } }, + ]); + + prismaMock.question.findUnique + .mockResolvedValueOnce({ + id: 1, + question_text: 'Pregunta 1', + difficulty: 'MEDIA', + subject: { name: 'Matemática' }, + sub_topic: { + name: 'Álgebra', + topic: { name: 'Ecuaciones' }, + }, + }) + .mockResolvedValueOnce({ + id: 2, + question_text: 'Pregunta 2', + difficulty: 'BAJA', + subject: { name: 'Física' }, + sub_topic: { + name: 'Cinemática', + topic: { name: 'Movimiento' }, + }, + }); + + const result = await service.listMostUsedQuestions(); + + expect(prismaMock.exam_Question.groupBy).toHaveBeenCalled(); + + expect(prismaMock.question.findUnique).toHaveBeenCalledTimes(2); + + expect(result).toEqual([ + { + usage_count: 3, + id: 1, + question_text: 'Pregunta 1', + difficulty: 'MEDIA', + subject: { name: 'Matemática' }, + sub_topic: { + name: 'Álgebra', + topic: { name: 'Ecuaciones' }, + }, + }, + { + usage_count: 1, + id: 2, + question_text: 'Pregunta 2', + difficulty: 'BAJA', + subject: { name: 'Física' }, + sub_topic: { + name: 'Cinemática', + topic: { name: 'Movimiento' }, + }, + }, + ]); }); }); diff --git a/src/modules/exam_related/exam_question/exam_question.service.ts b/src/modules/exam_related/exam_question/exam_question.service.ts index 4932b0a..f08a3ad 100644 --- a/src/modules/exam_related/exam_question/exam_question.service.ts +++ b/src/modules/exam_related/exam_question/exam_question.service.ts @@ -106,13 +106,4 @@ export class ExamQuestionService { return questions; } - - - - - - - - - } diff --git a/src/modules/exam_related/exam_student/dto/create-exam_student.dto.ts b/src/modules/exam_related/exam_student/dto/create-exam_student.dto.ts index 2d195a6..ffec95b 100644 --- a/src/modules/exam_related/exam_student/dto/create-exam_student.dto.ts +++ b/src/modules/exam_related/exam_student/dto/create-exam_student.dto.ts @@ -1,10 +1,11 @@ -import { IsInt, IsNumber } from 'class-validator'; +import { IsInt, IsNumber, IsOptional } from 'class-validator'; import { Type } from 'class-transformer'; export class CreateExamStudentDto { + @IsOptional() @Type(() => Number) @IsNumber({ maxDecimalPlaces: 2 }) - score: number; + score?: number; @Type(() => Number) @IsInt() diff --git a/src/modules/exam_related/exam_student/exam_student.service.spec.ts b/src/modules/exam_related/exam_student/exam_student.service.spec.ts index f9951c6..84115e6 100644 --- a/src/modules/exam_related/exam_student/exam_student.service.spec.ts +++ b/src/modules/exam_related/exam_student/exam_student.service.spec.ts @@ -1,18 +1,177 @@ import { Test, TestingModule } from '@nestjs/testing'; import { ExamStudentService } from './exam_student.service'; +import { PrismaService } from 'src/prisma/prisma.service'; describe('ExamStudentService', () => { let service: ExamStudentService; + let prismaMock: any; beforeEach(async () => { + prismaMock = { + $transaction: jest.fn(), + exam: { + findUnique: jest.fn(), + update: jest.fn(), + }, + exam_Student: { + create: jest.fn(), + findMany: jest.fn(), + findUnique: jest.fn(), + update: jest.fn(), + delete: jest.fn(), + }, + }; + const module: TestingModule = await Test.createTestingModule({ - providers: [ExamStudentService], + providers: [ + ExamStudentService, + { + provide: PrismaService, + useValue: prismaMock, + }, + ], }).compile(); service = module.get(ExamStudentService); }); - it('should be defined', () => { - expect(service).toBeDefined(); + // ===================================================== + // 🧪 CREATE + // ===================================================== + + it('create debe lanzar error si el examen no existe', async () => { + prismaMock.$transaction.mockImplementation(async (cb) => + cb({ + exam: { + findUnique: jest.fn().mockResolvedValue(null), + }, + }), + ); + + await expect( + service.create({ + exam_id: 1, + student_id: 2, + teacher_id: 3, + }), + ).rejects.toThrow('Exam not found'); + }); + + it('create debe asignar el examen y cambiar estado a "Asignado"', async () => { + prismaMock.$transaction.mockImplementation(async (cb) => + cb({ + exam: { + findUnique: jest.fn().mockResolvedValue({ id: 1 }), + update: jest.fn().mockResolvedValue({}), + }, + exam_Student: { + create: jest.fn().mockResolvedValue({ + exam_id: 1, + student_id: 2, + teacher_id: 3, + }), + }, + }), + ); + + const result = await service.create({ + exam_id: 1, + student_id: 2, + teacher_id: 3, + }); + + expect(result.exam_id).toBe(1); + expect(result.student_id).toBe(2); + }); + + // ===================================================== + // 🧪 FIND ALL + // ===================================================== + + it('findAll debe retornar examenes asignados con relaciones', async () => { + prismaMock.exam_Student.findMany.mockResolvedValue([{ id: 1 }]); + + const result = await service.findAll(); + + expect(prismaMock.exam_Student.findMany).toHaveBeenCalledWith({ + include: { + exam: true, + student: true, + teacher: true, + reevaluations: true, + }, + }); + + expect(result).toEqual([{ id: 1 }]); + }); + + // ===================================================== + // 🧪 FIND ONE + // ===================================================== + + it('findOne debe buscar por clave compuesta', async () => { + prismaMock.exam_Student.findUnique.mockResolvedValue({ id: 1 }); + + const result = await service.findOne(1, 2); + + expect(prismaMock.exam_Student.findUnique).toHaveBeenCalledWith({ + where: { + exam_id_student_id: { + exam_id: 1, + student_id: 2, + }, + }, + include: { + exam: true, + student: true, + teacher: true, + reevaluations: true, + }, + }); + + expect(result).toEqual({ id: 1 }); + }); + + // ===================================================== + // 🧪 UPDATE + // ===================================================== + + it('update debe modificar exam_student', async () => { + prismaMock.exam_Student.update.mockResolvedValue({ id: 1, score: 9 }); + + const result = await service.update(1, 2, { score: 9 }); + + expect(prismaMock.exam_Student.update).toHaveBeenCalledWith({ + where: { + exam_id_student_id: { + exam_id: 1, + student_id: 2, + }, + }, + data: { score: 9 }, + }); + + expect(result.score).toBe(9); + }); + + // ===================================================== + // 🧪 REMOVE + // ===================================================== + + it('remove debe eliminar exam_student', async () => { + prismaMock.exam_Student.delete.mockResolvedValue({ id: 1 }); + + const result = await service.remove(1, 2); + + expect(prismaMock.exam_Student.delete).toHaveBeenCalledWith({ + where: { + exam_id_student_id: { + exam_id: 1, + student_id: 2, + }, + }, + }); + + expect(result).toEqual({ id: 1 }); }); });