final tests implemented#48
Conversation
There was a problem hiding this comment.
Pull request overview
This PR implements comprehensive unit tests for the exam-related services (ExamStudentService, ExamQuestionService, and ExamService) and makes the score field optional in the Exam_Student model to allow exam assignments without immediate scoring. The changes also include cleanup of unused DTO fields and improvements to seed data generation.
- Added comprehensive unit test suites covering CRUD operations and special functions for all three exam services
- Made the
scorefield optional inExam_Studentmodel to support exam assignment workflow - Removed unused
teacher_idandhead_teacher_idfields fromGenerateExamDto - Enhanced seed data with difficulty-based probability function for more realistic test data
- Cleaned up trailing whitespace in service files
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| src/modules/exam_related/exam_student/exam_student.service.spec.ts | Added comprehensive unit tests for ExamStudentService covering create, findAll, findOne, update, and remove operations with transaction mocking |
| src/modules/exam_related/exam_student/dto/create-exam_student.dto.ts | Made score field optional with @IsOptional() decorator to align with schema changes |
| src/modules/exam_related/exam_question/exam_question.service.ts | Removed trailing whitespace for code cleanliness |
| src/modules/exam_related/exam_question/exam_question.service.spec.ts | Added comprehensive unit tests including CRUD operations and the listMostUsedQuestions feature |
| src/modules/exam_related/exam/exam.service.spec.ts | Added extensive unit tests covering exam creation, generation logic with edge cases, and the listGeneratedExamsBySubject feature |
| src/modules/exam_related/exam/dto/generated-exam.dto.ts | Removed unused teacher_id and head_teacher_id fields that weren't used by the generation logic |
| prisma/seeds/seeds_Exam.ts | Added getCorrectProbability function to vary answer correctness based on exam difficulty level |
| prisma/schema.prisma | Changed score field in Exam_Student model from required to optional (nullable) |
| prisma/migrations/20251213195700_score_field_optional_in_exam_student_model/migration.sql | Database migration to make the score column nullable in Exam_Student table |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
|
||
|
|
There was a problem hiding this comment.
The test is missing proper spacing after the closing brace. There should be a blank line after the closing brace of this test for better readability and consistency with other tests in the file.
| 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); | ||
| }); |
There was a problem hiding this comment.
All test descriptions in this file are written in Spanish (e.g., "debe crear un examen y asociar preguntas", "debe insertar una nueva combinación válida", "debe lanzar error si no hay suficientes preguntas"). These should be written in English to maintain consistency with the codebase conventions.
| }), | ||
| ).rejects.toThrow(BadRequestException); | ||
| }); | ||
| // ============================================================ |
There was a problem hiding this comment.
There's an extra blank line here that's unnecessary. Consider removing it for consistency with the rest of the codebase.
| expect(result[0].name).toBe('Parcial Matemática'); | ||
| }); | ||
|
|
||
|
|
There was a problem hiding this comment.
There's an extra blank line here that's unnecessary. Consider removing it for consistency with the rest of the codebase.
|
|
||
|
|
There was a problem hiding this comment.
There are extra blank lines (109) that serve no purpose and reduce code cleanliness. Consider removing these empty lines to improve code readability.
| // 🧪 CREATE | ||
| // ===================================================== | ||
|
|
||
| it('create debe lanzar error si el examen no existe', async () => { |
There was a problem hiding this comment.
The test description is in Spanish ("debe lanzar error si el examen no existe") which is inconsistent with the codebase convention that appears to use English for code. Test descriptions should use English to maintain consistency with other test files and make the codebase more accessible to international developers.
| it('create debe lanzar error si el examen no existe', async () => { | |
| it('should throw an error if the exam does not exist', async () => { |
| 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 }); | ||
| }); |
There was a problem hiding this comment.
All test descriptions in this file are written in Spanish (e.g., "debe asignar el examen", "debe retornar examenes asignados", "debe buscar por clave compuesta"). These should be written in English to maintain consistency with the codebase conventions, as the rest of the code (variable names, comments, etc.) is in English.
| 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' }, | ||
| }, | ||
| }, | ||
| ]); | ||
| }); |
There was a problem hiding this comment.
All test descriptions in this file are written in Spanish (e.g., "debe crear una relación exam_question", "debe retornar relaciones con exam y question"). These should be written in English to maintain consistency with the codebase conventions.
| } | ||
|
|
||
| function getCorrectProbability(difficulty: string): number { | ||
| switch(difficulty.toLowerCase()){ |
There was a problem hiding this comment.
There's a missing space after the switch keyword. The correct syntax is switch (difficulty.toLowerCase()) with a space between switch and the opening parenthesis, following standard JavaScript/TypeScript formatting conventions.
| switch(difficulty.toLowerCase()){ | |
| switch (difficulty.toLowerCase()) { |
No description provided.