Saltar al contenido
K
Swift: el lenguajePaso a paso38 min de lectura · 20 min de máquina Básico

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 { … }: func declara 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: public hace 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: Torneo es uno, y el programa de consola que crearás luego es otro); private, solo dentro de su tipo o fichero. Sin public, el programa de consola de más abajo no vería puntosPor.
  • if que devuelve valor: if … { 3 } else if … { 1 } else { 0 } como cuerpo de la función: el valor de la rama elegida es el resultado, sin return. En Kotlin escribiste esto con un when; el equivalente en Swift es switch, que verás en Funciones y switch.

Las cuatro piezas juntas, con otra función:

ejemplo mínimo: una función pública con etiquetas y un if que devuelve valor (con comentarios)
// 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
raspado

El 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.

ejemplo mínimo: un for con rango y otro for dentro (con comentarios)
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-3

Kotlin 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:

KotlinSwift
for (jornada in 1..totalDeJornadas)for jornada in 1...totalDeJornadas
fun puntosPor(…): Int = when { … }func puntosPor(…) -> Int { if … }
./gradlew :torneo:testxcrun 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).

ejemplo mínimo del test (con comentarios)
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 un struct, un tipo que guarda datos y funciones juntos (lo verás a fondo en struct frente a class). Cada @Test func dentro es una prueba.
  • @testable import Torneo: importa Torneo en el test y, además, deja ver lo que no es public. Aquí bastaría un import Torneo normal, porque puntosPor es public, 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:

ejemplo mínimo: pruebas agrupadas en un struct (con comentarios)
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:

ejemplo mínimo: una prueba con varios casos (con comentarios)
    // 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
solución del ejercicio
// 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.

Torneo/
Sources/Torneo/
Torneo.swiftse borra: plantilla
Puntos.swiftse crea
Tests/TorneoTests/
TorneoTests.swiftse borra: plantilla
PuntosTests.swiftse crea
Los ficheros de este paso. Los dos de plantilla se borran.

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.swift

2 · 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
Puntos.swift
// `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
PuntosTests.swift
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
PuntosTests.swift: la prueba con cuatro casos
    // 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 issues

El 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:

Package.swift: products, antes
        .library(
            name: "Torneo",
            targets: ["Torneo"]
        ),
    ],
Package.swift: products, después
        .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:

Package.swift: targets, antes
        .target(
            name: "Torneo"
        ),
        .testTarget(
Package.swift: targets, después
        .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 local
Intenta primero. Cuando termines, compara con la solución.Mostrar el código
main.swift
// 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-cli

Debes 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:

  1. 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'.
  2. Falta public en puntosPor: main.swift no la ve y dice cannot find 'puntosPor' in scope.
  3. Estás en otra carpeta: error: Could not find Package.swift in this directory or any of its parent directories.. Haz cd ~/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, con Package.swift, Sources y Tests; Sources tiene las carpetas Torneo (con Puntos.swift) y torneo-cli (con main.swift), y Tests/TorneoTests solo tiene PuntosTests.swift.
  • Desde ~/Marcador-Swift/Torneo, xcrun swift build termina con Build complete!.
  • xcrun swift run torneo-cli empieza con Torneo: Copa Barrio, sigue con Jornada 1 de 3, Jornada 2 de 3 y Jornada 3 de 3, y acaba con los nueve marcadores, de 0-0 -> 1 puntos para el local a 2-2 -> 1 puntos para el local.
  • xcrun swift test termina con Test run with 4 tests in 1 suite passed (ignora la línea Executed 0 tests de XCTest). El tiempo varía según la máquina.
  • Provocaste el fallo del paso 5: cambiaste el 3 de la primera rama de puntosPor por un 4, viste fallar pruebas con xcrun swift test y lo devolviste a su sitio.
  • Sabes decir para qué sirve @Test y 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. 1.¿Qué comprueba #expect(puntosPor(golesAFavor: 2, golesEnContra: 1) == 3) dentro de una función marcada con @Test?

  2. 2.Una función de Swift tiene este cuerpo: if x > 0 { "positivo" } else { "no positivo" }, sin return. ¿Qué pasa?

  3. 3.¿Qué recorre for numero in 1...3 { … }?

  4. 4.Una prueba lleva @Test(arguments: [(3, 0, 3), (2, 2, 1), (0, 1, 0), (5, 4, 3)]). ¿Cuántos tests cuenta xcrun swift test por ella?

Faltan 4 de 4