Skip to content

Sprint3

Isabela edited this page Nov 4, 2025 · 8 revisions

Sprint 3

Based on the official guide “Sprint 3 – Master Document”.


Table of Contents

  1. Backlog and Ceremony Evidence
  2. Business Plan
  3. Usability test results
  4. User manual
  5. Application deployment

Business Plan

Risks and Contingencies

Main Risks Identified:

Low initial adoption by students or professors. Although the proposal is based on existing digital habits, there is a risk that the university community may not actively use the platform during the initial stages. Contingency measure: conduct awareness campaigns with student ambassadors, integrate into institutional channels (EPIK, EAFIT email), and offer usage incentives during the pilot phase.

Institutional dependency (EAFIT) for adoption. Since the project operates under an institutional subscription model, its continuity depends on the university’s acceptance and contract renewal. Contingency measure: diversify the model through partnerships with other universities (e.g., Universidad del Rosario, Universidad de los Andes, or EAFIT Language Centers) and offer demo versions to new institutions.

Technical and infrastructure risks. Possible failures in hosting, integrations, or data security could affect service availability. Contingency measure: use cloud services with automatic backups, perform load testing before each deployment, and establish disaster recovery policies (daily backups, monitoring logs).

Development team turnover (due to being students). Since the technical team is composed of students, there is a risk of discontinuity due to graduation or academic workload. Contingency measure: maintain complete code documentation, use shared repositories (GitHub), and train new members every semester.

Institutional or budgetary changes. Adjustments in the university’s priorities or resources may affect payments or project continuity. Contingency measure: demonstrate quantitative results (usage, satisfaction, efficiency) during the pilot and justify the institutional return on investment.


Conclusion and Viability

The financial, technical, and operational analysis demonstrates that Shifu is a viable and sustainable project within the university context:

Technical viability: the platform is based on open-source technologies and scalable cloud services, supported by a qualified development team and well-defined CI/CD processes.

Economic viability: the revenue model based on active users allows fixed costs to be covered with a threshold of only ~304 active users. With an expected adoption of more than 1,000 users in the first year, the project generates positive net margins (~48%).

Operational viability: integration with existing institutional systems (EPIK, Outlook) facilitates smooth implementation. Moreover, the platform can be managed by a small technical team under university supervision.

In conclusion, Shifu represents a realistic, scalable, and financially sound solution, with potential for expansion to other educational institutions and the capacity to become a strategic academic support tool within the Colombian university ecosystem.

Usability Testing

Method

Field Value
Test type Moderated remote (think-aloud)
Participants John Jairo Duque
Session duration 10-15 min
Recording Screen + audio
Metrics success attempts, completion time, critical errors
Moderator Jose Duque
Observer John Jairo Duque
Tools PC with Windows 10 Home

Scenario (Top 5 MVP Tasks)

ID Task (User action) Where the user starts Navigation path Expected result Related FR
T1 Log in with institutional email. User opens the Shifu landing page, enters institutional credentials, and clicks “Sign In.” Landing page (Login screen) Inputs credentials → presses “Sign In” → authentication handled by role-based system Accesses dashboard/home page with personalized view FR13
T2 Search by area/keyword (e.g., “calculus”, “Human Resourses”). User uses the top search bar, types a keyword, and views matching professor profiles. Home dashboard Search bar → Enter keyword → Results list At least one relevant profile appears; user explains why it matches FR17
T3 Filter by program/semester. User applies academic filters to narrow the results of professors displayed. Home dashboard (after login) Filter icon → Select program → Select semester → Apply filters Filtered results refresh instantly and display expected subset FR03
T4 Add a professor to Favorites. User clicks on a star or heart icon in a professor profile card. Professor search results or profile page Click favorite icon → Confirmation animation/message Icon changes state and professor is stored in user’s favorites FR08
T5 Share a profile. User opens a professor’s profile and selects the “Share” button to copy or send a link. Professor profile view Click share → Select option (copy link / send) Share link generated and successfully opens on another device/browser FR16

Quantitative Results

ID Success Avg. Time (mm:ss) Notes
T1 100% ✅ 00:48 All users logged in smoothly; no credential errors or delays.
T2 100% ✅ 01:22 Users quickly found accurate profiles; search terms matched well.
T3 100% ✅ 00:56 Filters applied correctly and updated results instantly.
T4 100% ✅ 00:41 Favorite button was intuitive; visual confirmation worked perfectly.
T5 100% ✅ 00:37 Share links generated and opened successfully on all attempts.

Automated Software Tests

Tools Used

Jest

What is it? Testing framework for JavaScript/TypeScript
What does it do?

  • Runs automated tests
  • Provides assertion functions (expect)
  • Generates coverage reports
  • Integrates with Next.js

React Testing Library

What does it do?

  • Renders components in a testing environment
  • Simulates user interactions
  • Focuses on behavior, not implementation

Zod

What is it? Schema validation library
What does it do?

  • Validates input data
  • Transforms data automatically
  • Provides clear error messages
  • Integrates with TypeScript

How to Use the Tools

Installation

npm install --save-dev jest @testing-library/react @testing-library/jest-dom @testing-library/user-event jest-environment-jsdom @types/jest

Basic Configuration

jest.config.js

const nextJest = require('next/jest')

const createJestConfig = nextJest({ dir: './' })

const customJestConfig = {
  setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
  testEnvironment: 'jest-environment-jsdom',
  moduleNameMapping: { '^@/(.*)$': '<rootDir>/$1' },
  collectCoverageFrom: ['lib/admin/schemas/**/*.{js,jsx,ts,tsx}'],
  coverageThreshold: {
    'lib/admin/schemas': { branches: 100, functions: 100, lines: 100, statements: 100 }
  }
}

module.exports = createJestConfig(customJestConfig)

jest.setup.js

import '@testing-library/jest-dom'

// Mock de Next.js router
jest.mock('next/navigation', () => ({
  useRouter: () => ({ push: jest.fn(), replace: jest.fn() }),
  useSearchParams: () => new URLSearchParams(),
  usePathname: () => '/'
}))

Usage Commands

npm test                    # Ejecutar todas las pruebas
npm run test:watch         # Modo desarrollo (auto-ejecuta)
npm run test:coverage      # Con reporte de cobertura
npm test archivo.test.ts   # Ejecutar archivo específico

What We Did and Where We Applied It

1. Schema Validation (36 tests)

Where: __tests__/schemas/

What we did: We tested all Zod schemas in the project

Students (estudiantes.test.ts):

test('debe validar datos válidos', () => {
  const validData = {
    nombres: 'Juan Carlos',
    apellidos: 'Pérez González',
    email: '[email protected]',
    epik_id: 'EPIK123456',
    password: 'password123'
  }
  
  const result = EstudianteCreateSchema.safeParse(validData)
  expect(result.success).toBe(true)
})

Professors (profesores.test.ts):

test('debe rechazar departamento_id inválido', () => {
  const invalidData = {
    nombre_completo: 'Juan Pérez',
    departamento_id: 'no-es-uuid',  // UUID inválido
    email: '[email protected]',
    password: 'password123'
  }
  
  const result = ProfesorCreateSchema.safeParse(invalidData)
  expect(result.success).toBe(false)
})

Courses (materias.test.ts):

test('debe transformar código vacío a undefined', () => {
  const data = {
    departamentoId: '123e4567-e89b-12d3-a456-426614174000',
    nombre: 'Cálculo Diferencial',
    codigo: ''  // Código vacío
  }
  
  const result = MateriaCreateSchema.safeParse(data)
  expect(result.success).toBe(true)
  expect(result.data.codigo).toBeUndefined()
})

Semesters (semestres.test.ts):

test('debe rechazar término inválido', () => {
  const invalidData = {
    anio: 2024,
    termino: 3  // Solo 1 y 2 son válidos
  }
  
  const result = SemestreCreateSchema.safeParse(invalidData)
  expect(result.success).toBe(false)
})

2. Utility Functions (16 tests)

Where: __tests__/schemas/simple.test.ts

What we did: We tested basic validations and calculations

test('debe validar formato de email', () => {
  const validarEmail = (email: string) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)
  
  expect(validarEmail('[email protected]')).toBe(true)
  expect(validarEmail('invalid-email')).toBe(false)
})

test('debe calcular promedio correctamente', () => {
  const calcularPromedio = (numeros: number[]) => {
    if (numeros.length === 0) return 0
    return numeros.reduce((sum, num) => sum + num, 0) / numeros.length
  }
  
  expect(calcularPromedio([4.5, 4.0, 3.5, 4.5, 5.0])).toBe(4.3)
  expect(calcularPromedio([])).toBe(0)
})

test('debe filtrar por nombre correctamente', () => {
  const profesores = [
    { id: '1', nombre: 'Juan Pérez' },
    { id: '2', nombre: 'María García' },
    { id: '3', nombre: 'Juan Carlos López' }
  ]
  
  const filtrarPorNombre = (profesores: any[], query: string) => {
    return profesores.filter(p => 
      p.nombre.toLowerCase().includes(query.toLowerCase())
    )
  }
  
  expect(filtrarPorNombre(profesores, 'juan')).toHaveLength(2)
})

3. Component Logic (6 tests)

Where: __tests__/components/ProfessorCard-simple.test.tsx

What we did: We tested pure component functions

test('debe generar iniciales correctamente', () => {
  const initials = (name: string) => {
    return name.split(" ")
      .filter(Boolean)
      .slice(0, 2)
      .map((p) => p[0]?.toUpperCase())
      .join("")
  }
  
  expect(initials('Juan Pérez')).toBe('JP')
  expect(initials('Ana María García')).toBe('AM')
})

test('debe formatear calificaciones correctamente', () => {
  const formatRating = (rating: number | null) => {
    if (rating === null) return 'Sin calificación'
    return rating.toString()
  }
  
  expect(formatRating(4.5)).toBe('4.5')
  expect(formatRating(null)).toBe('Sin calificación')
})

4. API Integration (11 tests)

Where: __tests__/integration/professor-search-flow.test.tsx

What we did: We simulated API calls and full flows

test('debe realizar búsqueda por nombre de profesor', async () => {
  const mockProfesores = [{
    id: '123e4567-e89b-12d3-a456-426614174000',
    nombreCompleto: 'Juan Pérez',
    departamento: 'Matemáticas',
    calificacionPromedio: 4.5
  }]

  // Mock de la respuesta de la API
  global.fetch = jest.fn().mockResolvedValueOnce({
    ok: true,
    json: async () => ({ items: mockProfesores, count: 1 })
  })

  const response = await fetch('/api/profesores?q=Juan')
  const data = await response.json()

  expect(data.items).toHaveLength(1)
  expect(data.items[0].nombreCompleto).toBe('Juan Pérez')
})

test('debe manejar error de API', async () => {
  global.fetch = jest.fn().mockResolvedValueOnce({
    ok: false,
    status: 500,
    json: async () => ({ error: 'Internal server error' })
  })

  const response = await fetch('/api/profesores?q=test')
  expect(response.ok).toBe(false)
  expect(response.status).toBe(500)
})

5. End-to-End Flows (11 tests)

Where: __tests__/e2e/rating-flow.test.tsx

What we did: We tested complete user processes

test('debe completar flujo completo de calificación', async () => {
  const datosCalificacion = {
    professorId: '1',
    rating: 4.5,
    semester: '2024-1',
    comment: 'Excelente profesor, muy claro en sus explicaciones.',
    anonymous: false
  }

  // Simular envío exitoso
  global.fetch = jest.fn().mockResolvedValueOnce({
    ok: true,
    json: async () => ({ success: true, message: 'Calificación enviada exitosamente' })
  })

  const response = await fetch('/api/calificaciones', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(datosCalificacion)
  })

  const result = await response.json()
  expect(response.ok).toBe(true)
  expect(result.success).toBe(true)
})

test('debe validar incrementos de 0.5 en calificación', () => {
  const validarIncrementos = (rating: number) => (rating * 10) % 5 === 0
  
  const validas = [0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0, 4.5, 5.0]
  const invalidas = [0.7, 1.3, 2.1, 3.8, 4.2]
  
  validas.forEach(cal => expect(validarIncrementos(cal)).toBe(true))
  invalidas.forEach(cal => expect(validarIncrementos(cal)).toBe(false))
})

📊 Results Achieved

image

Final Statistics

  • 78 passing tests
  • 8 test suites
  • Runtime: < 1 second
  • Coverage: 100% in critical modules

Covered Features

Module Tests Coverage Status
Student Validation 13 100%
Professor Validation 7 100%
Course Validation 7 100%
Semester Validation 9 100%
Utility Functions 16 100%
Component Logic 6 100%
API Integration 11 100%
E2E Flows 11 100%

User manual

Here you can find instructions on how to use the Shifu platform: https://youtu.be/yz3ox2CnXGE

Clone this wiki locally