La regla de los puntos y tu primer test
Al terminar: Escribir la regla de los puntos en Swift y tu primer test, hasta llegar a 4 tests.
Al terminar, el paquete Torneo tendrá la regla de los puntos, puntosPor(golesAFavor:golesEnContra:), un programa de consola, torneo-cli, que la usa, y cuatro tests que pasan. Es el mismo punto al que llegaste en Kotlin con :torneo, ahora en Swift.
Importa porque es la primera vez que escribes código de verdad en el paquete y la primera vez que lo pruebas con Swift Testing, la librería de tests que usarás en todo el curso. Y porque aprendes la receta para añadir una pieza (un programa de consola) a un paquete, que otras lecciones repiten más corta.
Temas: funciones con if que devuelve un valor, el for con rangos, tu primer test con Swift Testing y un objetivo ejecutable.
Funciones que devuelven un valor
Una función es un trozo de código con nombre que recibe unos datos (los parámetros), hace algo con ellos y devuelve un resultado. Sirve para no repetir código y para ponerle nombre a una idea: «los puntos de un marcador». En Swift se escribe así:
func nombre(etiqueta: Tipo) -> Tipo { … }:funcdeclara una función; lo que va tras la flecha->es el tipo del valor que devuelve.- Etiqueta de argumento: en
puntosPor(golesAFavor: 2, golesEnContra: 1),golesAFavor:es una etiqueta. Se escribe en la llamada y es parte del nombre de la función,puntosPor(golesAFavor:golesEnContra:). En Funciones y switch las verás a fondo. - Control de acceso:
publichace visible algo desde otros módulos;internal(el valor por defecto) solo dentro de su módulo (un módulo es cada objetivo del paquete:Torneoes uno, y el programa de consola que crearás luego es otro);private, solo dentro de su tipo o fichero. Sinpublic, el programa de consola de más abajo no veríapuntosPor. ifque devuelve valor:if … { 3 } else if … { 1 } else { 0 }como cuerpo de la función: el valor de la rama elegida es el resultado, sinreturn. En Kotlin escribiste esto con unwhen; el equivalente en Swift esswitch, que verás en Funciones y switch.
Las cuatro piezas juntas, con otra función:
// public: se podrá usar desde otro módulo (sin él, solo dentro del suyo)
// func: declara una función; notaPor es su nombre
// aciertos: y fallos: son las etiquetas; cada una lleva su tipo tras los dos puntos
// -> String: el tipo del valor que devuelve
public func notaPor(aciertos: Int, fallos: Int) -> String {
if aciertos > fallos { // primera condición
"aprobado" // el valor de esta rama es el resultado: sin return
} else if aciertos == fallos { // solo se mira si la primera fue falsa
"raspado"
} else { // ninguna de las anteriores se cumplió
"suspenso"
}
}
print(notaPor(aciertos: 7, fallos: 3)) // al llamar se escriben las etiquetas
print(notaPor(aciertos: 2, fallos: 2))Al ejecutarlo en tu Playground, la consola enseña:
aprobado
raspadoEl for con rangos
Un bucle repite un trozo de código. for x in 1...3 { … } lo repite una vez por cada valor de un rango (1...3 es el rango cerrado: del 1 al 3, los dos incluidos; lleva tres puntos). Dentro de las llaves, x vale cada vez un número distinto. El for no lleva paréntesis, y "\(x)" inserta el valor de x dentro de un texto. Un for dentro de otro funciona como las casillas de un tablero: el de fuera elige la fila y el de dentro recorre todas sus columnas, y repite el de dentro entero por cada vuelta del de fuera.
let filas = 2 // let: este valor no cambia
for fila in 1...filas { // 1...2: del 1 al 2, los dos incluidos
for columna in 1...3 { // el for de dentro se repite entero por cada fila
print("casilla \(fila)-\(columna)") // \(…) mete el valor dentro del texto
}
}La consola enseña seis líneas, una por casilla:
casilla 1-1
casilla 1-2
casilla 1-3
casilla 2-1
casilla 2-2
casilla 2-3Kotlin frente a Swift
Estas son líneas del Kotlin que escribiste en Condiciones: if y when y en Bucles y tu primer test, con su equivalente:
| Kotlin | Swift |
|---|---|
for (jornada in 1..totalDeJornadas) | for jornada in 1...totalDeJornadas |
fun puntosPor(…): Int = when { … } | func puntosPor(…) -> Int { if … } |
./gradlew :torneo:test | xcrun swift test |
Cinco diferencias que se notan a la primera: fun se escribe func; el tipo de retorno va tras una flecha ->; el rango cerrado lleva tres puntos (1...3); la interpolación es \(…) en vez de $…; y el for no lleva paréntesis.
Tu primer test con Swift Testing
Si quieres leer más sobre Swift Testing, está en la documentación de Apple.
Un test es una función que llama a tu código y comprueba el resultado. Si el resultado no es el esperado, el test falla y te avisa; así sabes que una regla sigue bien cada vez que tocas algo. En Swift Testing: @Test marca la función (una macro, una etiqueta que le pide al compilador escribir código por ti) y #expect(condición) comprueba una condición. No hay assertEquals: escribes la expresión con ==. No hay que instalar nada: Swift Testing viene dentro del Swift de Xcode (por eso import Testing funciona sin tocar Package.swift).
import Testing
@Test func dosMasDosSonCuatro() {
#expect(2 + 2 == 4) // si la condición es falsa, la prueba falla
}Las pruebas se agrupan en un tipo. Piezas nuevas:
struct PuntosTests { … }: Swift Testing agrupa las pruebas en un tipo, y aquí es unstruct, un tipo que guarda datos y funciones juntos (lo verás a fondo en struct frente a class). Cada@Test funcdentro es una prueba.@testable import Torneo: importaTorneoen el test y, además, deja ver lo que no espublic. Aquí bastaría unimport Torneonormal, porquepuntosPorespublic, pero los tests de todo el curso empiezan con esta línea y conviene que la veas desde el primero.
Así se ve un fichero de pruebas, en un paquete imaginario Calculadora con una función doble(_:) que multiplica por dos:
import Testing // la librería de pruebas
@testable import Calculadora // el módulo que se prueba, también lo que no es public
struct DobleTests { // un tipo que agrupa pruebas relacionadas
@Test func dobleDeDos() { // una prueba: una función marcada con @Test
#expect(doble(2) == 4) // se compara con ==; si sale falso, la prueba falla
}
@Test func dobleDeCero() { // otra prueba del mismo grupo, con su propio nombre
#expect(doble(0) == 0)
}
}Y una prueba con varios casos: @Test(arguments: […]) se ejecuta una vez por cada elemento de la lista. Aquí cada elemento es una tupla (un grupo de valores entre paréntesis, como (1, 2)) y sus valores llegan como los parámetros de la función, en el mismo orden:
// La lista de casos: cada fila es una tupla (entrada, esperado).
@Test(arguments: [(1, 2), (5, 10), (-3, -6)])
// Los valores de cada tupla llegan a los parámetros en el mismo orden.
func dobleDeVarios(entrada: Int, esperado: Int) {
#expect(doble(entrada) == esperado) // se ejecuta tres veces, una por fila
}Reto · Ejercicio: la tarifa del polideportivo
En tu Playground, escribe una función, tarifa, que reciba una edad con la etiqueta edad y devuelva cuántos euros cuesta la entrada: 3 si tiene menos de 18 años, 2 si tiene 65 o más y 6 en otro caso, sin escribir return. Recorre las edades de 16 a 18 e imprime la tarifa de cada una, y después la de 70 años. Lo que debe salir:
16 años: 3 €
17 años: 3 €
18 años: 6 €
70 años: 2 €Ver solución
// Menores de 18: 3 €. Desde 65: 2 €. El resto: 6 €.
func tarifa(edad: Int) -> Int {
if edad < 18 {
3
} else if edad >= 65 {
2
} else {
6
}
}
for edad in 16...18 {
print("\(edad) años: \(tarifa(edad: edad)) €")
}
print("70 años: \(tarifa(edad: 70)) €")El if es la expresión de la función: cada rama es un valor y no lleva return. El for con 16...18 recorre tres edades, y la de 70 se imprime aparte.
Paso · Escribe la regla de los puntos
Objetivo: que la librería Torneo tenga puntosPor(…) y que las pruebas de plantilla se sustituyan por las tuyas, cuatro en total. Aún no hay programa de consola: llega en el paso siguiente.
1 · Prepara los ficheros. Borra Sources/Torneo/Torneo.swift y Tests/TorneoTests/TorneoTests.swift: son plantillas de relleno. Crea Sources/Torneo/Puntos.swift y Tests/TorneoTests/PuntosTests.swift con el menú de Xcode que viste en Tu primer paquete de Swift (clic derecho sobre la carpeta → New File from Template… → Swift File) o, en la Terminal, desde ~/Marcador-Swift/Torneo:
touch Sources/Torneo/Puntos.swift Tests/TorneoTests/PuntosTests.swift2 · La función. Intenta escribirla tú: puntosPor, una función pública que recibe los goles con las etiquetas golesAFavor y golesEnContra, y devuelve 3 si ganas, 1 si empatas y 0 si pierdes.
Intenta primero. Cuando termines, compara con la solución.Mostrar el código
// `public`: visible desde fuera del módulo Torneo (el programa de consola la usará).
// `golesAFavor` y `golesEnContra` son las etiquetas: se escriben al llamar.
// `-> Int` dice que devuelve un entero.
public func puntosPor(golesAFavor: Int, golesEnContra: Int) -> Int {
// El `if` es la expresión: cada rama es un valor y no lleva `return`.
if golesAFavor > golesEnContra {
3 // gana: tres puntos
} else if golesAFavor == golesEnContra {
1 // empata: uno
} else {
0 // pierde: ninguno
}
}Compila con xcrun swift build desde Torneo: debe terminar con Build complete!.
3 · Tres pruebas sueltas. Intenta escribirlas tú: un grupo de pruebas, PuntosTests, con tres: que 2-1 da 3 puntos, que 1-1 y 0-0 dan 1, y que 0-3 da 0. Lo que debes ver al final de xcrun swift test: Test run with 3 tests in 1 suite passed.
Intenta primero. Cuando termines, compara con la solución.Mostrar el código
import Testing // la librería de pruebas (Swift Testing)
@testable import Torneo // trae nuestro módulo, también lo que no es `public`
// Un tipo que agrupa las pruebas.
struct PuntosTests {
// `@Test` marca la función como prueba.
@Test func victoriaDaTresPuntos() {
// `#expect` falla la prueba si la condición no es verdadera.
#expect(puntosPor(golesAFavor: 2, golesEnContra: 1) == 3)
}
@Test func empateDaUnPunto() {
#expect(puntosPor(golesAFavor: 1, golesEnContra: 1) == 1)
#expect(puntosPor(golesAFavor: 0, golesEnContra: 0) == 1)
}
@Test func derrotaNoDaPuntos() {
#expect(puntosPor(golesAFavor: 0, golesEnContra: 3) == 0)
}
}Ejecuta xcrun swift test desde Torneo: la línea final de Swift Testing debe decir Test run with 3 tests in 1 suite passed.
4 · Una prueba con varios casos. Intenta escribirla tú: dentro de PuntosTests, debajo de la tercera prueba y antes de la } que cierra el struct, una sola prueba que se ejecute con cuatro casos, cada uno con los goles a favor, los goles en contra y los puntos esperados: 3-0 da 3, 2-2 da 1, 0-1 da 0 y 5-4 da 3. Lo que debes ver al final de xcrun swift test: Test run with 4 tests in 1 suite passed.
Intenta primero. Cuando termines, compara con la solución.Mostrar el código
// La misma prueba, una vez por cada fila de la lista.
// Cada fila es una tupla: (golesAFavor, golesEnContra, puntos esperados).
@Test(arguments: [(3, 0, 3), (2, 2, 1), (0, 1, 0), (5, 4, 3)])
func tablaDePuntos(golesAFavor: Int, golesEnContra: Int, esperados: Int) {
#expect(puntosPor(golesAFavor: golesAFavor, golesEnContra: golesEnContra) == esperados)
}Ejecuta xcrun swift test otra vez: la línea final debe decir Test run with 4 tests in 1 suite passed. Son cuatro porque la prueba con varios casos cuenta como una aunque se ejecute con cuatro.
5 · Provoca un fallo. Un test que nunca ha fallado no demuestra que funcione. En Puntos.swift, cambia el 3 de la primera rama por un 4 y ejecuta xcrun swift test. Las pruebas de victoria fallan, y la salida dice qué valor tenía cada lado:
Test victoriaDaTresPuntos() recorded an issue at PuntosTests.swift:9:9: Expectation failed: (puntosPor(golesAFavor: 2, golesEnContra: 1) → 4) == 3
Test run with 4 tests in 1 suite failed after 0.049 seconds with 3 issuesEl número de línea y el tiempo serán los de tu fichero. Devuelve el 4 a 3 y comprueba que vuelven a pasar las cuatro.
Paso · Añade torneo-cli: un objetivo ejecutable
Objetivo: un programa de consola dentro del mismo paquete, torneo-cli, que usa la librería Torneo, y que se arranca con xcrun swift run torneo-cli.
Es la receta para añadir una pieza al paquete; las lecciones que necesiten otra la repetirán más corta.
1 · Qué añades y por qué ahora. Hasta ahora Torneo es una librería: código que otros usan, pero que no se puede «encender». Un objetivo ejecutable (executable target) es un objetivo con un main.swift, el punto de entrada, que sí se arranca. Hace el papel del Main.kt de tu módulo :torneo, con application { mainClass… } en Gradle. Lo necesitas ahora para ver salir por pantalla lo que escribas en las lecciones siguientes, sin esperar a la app de iPhone.
2 · La carpeta, antes que el manifiesto. Cada objetivo vive en una carpeta con su nombre. Créala primero, desde Torneo:
mkdir Sources/torneo-cli
touch Sources/torneo-cli/main.swift(Si editas Package.swift antes, SwiftPM se queja de que no encuentra la carpeta: error: 'torneo': Source files for target torneo-cli should be located under 'Sources/torneo-cli', or a custom sources path can be set with the 'path' property in Package.swift. Es lo que pasa cuando la hoja nombra una pieza que no está en la caja.)
3 · El producto, en products. Abre Package.swift. Dentro de products: [ … ] hay solo el .library(; añade una línea nueva justo después del ), que lo cierra:
.library(
name: "Torneo",
targets: ["Torneo"]
),
], .library(
name: "Torneo",
targets: ["Torneo"]
),
// Un producto ejecutable: lo que se arranca con `swift run torneo-cli`.
.executable(name: "torneo-cli", targets: ["torneo-cli"]),
],4 · El objetivo, en targets. Dentro de targets: [ … ], después del ), que cierra el .target( de Torneo y antes del .testTarget(, añade el objetivo:
.target(
name: "Torneo"
),
.testTarget( .target(
name: "Torneo"
),
// Un objetivo ejecutable: se compila con los ficheros de Sources/torneo-cli.
.executableTarget(
name: "torneo-cli",
dependencies: ["Torneo"] // puede usar la librería Torneo
),
.testTarget(.executableTarget es un objetivo que se puede arrancar, con su main.swift. dependencies: ["Torneo"] le da acceso a la librería: es el mismo tipo de dependencia que ya tiene TorneoTests. Cada cosa en su sitio: el producto dice qué se ofrece, el objetivo dice con qué ficheros se hace.
5 · Escribe main.swift. El fichero de un objetivo ejecutable lleva el código suelto, sin función main(), y se ejecuta de arriba abajo. import Torneo da acceso a la librería. Lo demás, los dos for con rango y el for dentro de otro, es lo que ya probaste en tu Playground.
Intenta escribirlo tú: guarda, sin que puedan cambiar, el nombre del torneo, «Copa Barrio», y el total de jornadas, 3; imprime el nombre, después una línea por jornada y, por último, los puntos que se lleva el local en cada marcador posible de 0 a 2 goles por equipo. Debe salir lo mismo que en tu Kotlin de Bucles y tu primer test:
Torneo: Copa Barrio
Jornada 1 de 3
Jornada 2 de 3
Jornada 3 de 3
0-0 -> 1 puntos para el local
0-1 -> 0 puntos para el local
0-2 -> 0 puntos para el local
1-0 -> 3 puntos para el local
1-1 -> 1 puntos para el local
1-2 -> 0 puntos para el local
2-0 -> 3 puntos para el local
2-1 -> 3 puntos para el local
2-2 -> 1 puntos para el localIntenta primero. Cuando termines, compara con la solución.Mostrar el código
// Trae la librería Torneo: da acceso a `puntosPor`.
import Torneo
// `let`: valores que no cambian.
let nombreDelTorneo = "Copa Barrio"
let totalDeJornadas = 3
print("Torneo: \(nombreDelTorneo)")
// `1...totalDeJornadas` es un rango cerrado: de 1 a 3, los dos incluidos.
for jornada in 1...totalDeJornadas {
print("Jornada \(jornada) de \(totalDeJornadas)")
}
// Un `for` dentro de otro: por cada marcador local, todos los del visitante.
for golesLocal in 0...2 {
for golesVisitante in 0...2 {
let puntos = puntosPor(golesAFavor: golesLocal, golesEnContra: golesVisitante)
print("\(golesLocal)-\(golesVisitante) -> \(puntos) puntos para el local")
}
}6 · Compila y ejecuta.
xcrun swift build
xcrun swift run torneo-cliDebes ver Build complete! y, tras unas líneas de compilación, el torneo: Torneo: Copa Barrio, las tres jornadas y los nueve marcadores.
Tres fallos habituales, con el mensaje que da el compilador:
- La carpeta no existe (paso 2 al revés): el error de
Source files for target torneo-cli should be located under 'Sources/torneo-cli'. - Falta
publicenpuntosPor:main.swiftno la ve y dicecannot find 'puntosPor' in scope. - Estás en otra carpeta:
error: Could not find Package.swift in this directory or any of its parent directories.. Hazcd ~/Marcador-Swift/Torneo.
Si algo falla y no sabes por qué, escribe pwd y ls: el error más común de esta lección es ejecutar la orden en la carpeta equivocada.
Punto de control
- Existe
~/Marcador-Swift/Torneo, conPackage.swift,SourcesyTests;Sourcestiene las carpetasTorneo(conPuntos.swift) ytorneo-cli(conmain.swift), yTests/TorneoTestssolo tienePuntosTests.swift. - Desde
~/Marcador-Swift/Torneo,xcrun swift buildtermina conBuild complete!. xcrun swift run torneo-cliempieza conTorneo: Copa Barrio, sigue conJornada 1 de 3,Jornada 2 de 3yJornada 3 de 3, y acaba con los nueve marcadores, de0-0 -> 1 puntos para el locala2-2 -> 1 puntos para el local.xcrun swift testtermina conTest run with 4 tests in 1 suite passed(ignora la líneaExecuted 0 testsde XCTest). El tiempo varía según la máquina.- Provocaste el fallo del paso 5: cambiaste el
3de la primera rama depuntosPorpor un4, viste fallar pruebas conxcrun swift testy lo devolviste a su sitio. - Sabes decir para qué sirve
@Testy qué comprueba#expect, y por qué una prueba con cuatro casos cuenta como un solo test.
Comprueba lo que entendiste
4 preguntas · necesitas 3 para completar la lección
1.¿Qué comprueba
#expect(puntosPor(golesAFavor: 2, golesEnContra: 1) == 3)dentro de una función marcada con@Test?2.Una función de Swift tiene este cuerpo:
if x > 0 { "positivo" } else { "no positivo" }, sinreturn. ¿Qué pasa?3.¿Qué recorre
for numero in 1...3 { … }?4.Una prueba lleva
@Test(arguments: [(3, 0, 3), (2, 2, 1), (0, 1, 0), (5, 4, 3)]). ¿Cuántos tests cuentaxcrun swift testpor ella?