-
Notifications
You must be signed in to change notification settings - Fork 0
Sprint3
Based on the official guide “Sprint 3 – Master Document”.
- Backlog and Ceremony Evidence
- Business Plan
- Usability test results
- User manual
- Application deployment
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.
| 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 |
| 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 |
| 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. |
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
What does it do?
- Renders components in a testing environment
- Simulates user interactions
- Focuses on behavior, not implementation
What is it? Schema validation library
What does it do?
- Validates input data
- Transforms data automatically
- Provides clear error messages
- Integrates with TypeScript
npm install --save-dev jest @testing-library/react @testing-library/jest-dom @testing-library/user-event jest-environment-jsdom @types/jestconst 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)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: () => '/'
}))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íficoStudents (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)
})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)
})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')
})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)
})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))
})
- ✅ 78 passing tests
- ✅ 8 test suites
- ✅ Runtime: < 1 second
- ✅ Coverage: 100% in critical modules
| 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% | ✅ |
Here you can find instructions on how to use the Shifu platform: https://youtu.be/yz3ox2CnXGE