Johdanto: Testauksen uusi aikakausi on täällä
Apple pudotti WWDC 2024 -konferenssissa melkoisen pommin: Swift Testing -kehyksen, joka muuttaa tapaamme kirjoittaa testejä Swiftillä aivan perusteellisesti. Jos olet koskaan turhautunut XCTestin kanssa — ja rehellisesti sanottuna, kuka ei ole — niin tämä kehys on kuin raikas tuulahdus. Se on suunniteltu alusta alkaen hyödyntämään Swiftin moderneja ominaisuuksia: makroja, rinnakkaisuutta ja tyyppijärjestelmää. Parasta? Se tulee sisäänrakennettuna Xcode 16:een ja Swift 6 -työkaluketjuun, joten erillisiä asennuksia ei tarvita.
Tässä oppaassa käymme läpi Swift Testingin keskeiset ominaisuudet käytännön esimerkein. Opit käyttämään @Test- ja @Suite-makroja, hallitsemaan väittämät #expect- ja #require-makroilla, hyödyntämään parametrisoituja testejä ja paljon muuta.
Eli, sukellettaan suoraan asiaan.
Miksi Swift Testing korvaa XCTestin?
XCTest on palvellut iOS-kehittäjiä vuosien ajan, mutta sen juuret ovat syvällä Objective-C-maailmassa. Käytännössä tämä tarkoittaa, että joudut periyttämään testiluokat XCTestCase-luokasta, valitsemaan yli 40 erilaisesta XCTAssert-variantista oikean, ja rinnakkaistaminen onnistuu vain moniprosessimallin kautta. SwiftUI:n ja Swift Concurrencyn aikakaudella tämä tuntuu — no, vanhentuneelta.
Swift Testing ratkaisee nämä kipupisteet:
- Makropohjainen syntaksi — testit määritellään
@Test-attribuutilla missä tahansa tyypissä - Kaksi väittämämakroa —
#expectja#requirekorvaavat kaikki XCTAssert-variantit (kyllä, kaikki!) - Natiivi rinnakkaisuustuki — testit ajetaan rinnakkain oletusarvoisesti Swift Concurrencyn avulla
- Parametrisoidut testit — sama testi voidaan ajaa useilla syötteillä ilman toistoa
- Ominaisuusjärjestelmä (traits) — testien mukauttaminen deklaratiivisesti
- Rakenteet testityyppeinä — luokkien sijaan voidaan käyttää structeja ja actoreita
Swift Testing vs. XCTest — keskeiset erot
Tässä on tiivistetty vertailu, jotta näet erot yhdellä silmäyksellä:
- Kielituki: XCTest tukee sekä Objective-C:tä että Swiftiä — Swift Testing ainoastaan Swiftiä
- Rinnakkaisuus: XCTest käyttää moniprosessimallia — Swift Testing prosessin sisäistä rinnakkaisuutta
- Väittämät: XCTest tarjoaa 40+ varianttia — Swift Testing kaksi makroa
- Testien määrittely: XCTest vaatii
XCTestCase-aliluokan — Swift Testing käyttää@Test-makroa - Alustus/purkaminen: XCTest käyttää
setUp()/tearDown()— Swift Testing tavallistainit/deinit - UI-testaus: XCTest tukee — Swift Testing ei vielä
- Suorituskykytestit: XCTest tukee — Swift Testing ei vielä
Perusteet: @Test ja @Suite
Swift Testingin sydän on @Test-makro. Se merkitsee funktion testifunktioksi, ja toisin kuin XCTestissä, funktion nimen ei tarvitse alkaa sanalla "test" eikä sen tarvitse sijaita tietyssä luokassa. Tämä yksinkertainen muutos tekee testeistä huomattavasti luettavampia.
import Testing
@Test func yhteenlaskuToimiiOikein() {
let tulos = laske(2, plus: 3)
#expect(tulos == 5)
}
Testejä ryhmitellään @Suite-makrolla merkittyihin tyyppeihin. Apple suosittelee structien käyttöä luokkien sijaan, koska ne estävät tilan jakamisen testien välillä — mikä on itse asiassa yksi yleisimmistä testausbugeista:
import Testing
@Suite("Laskutoiminnot")
struct LaskuTestit {
let laskin = Laskin()
@Test("Yhteenlasku positiivisilla luvuilla")
func yhteenlasku() {
#expect(laskin.laske(2, plus: 3) == 5)
}
@Test("Vähennyslasku negatiivisella tuloksella")
func vahennyslasku() {
#expect(laskin.laske(2, miinus: 5) == -3)
}
}
Huomaa miten @Test- ja @Suite-makroille voidaan antaa kuvaava merkkijono. Tämä näkyy suoraan Xcoden testinavigaattorissa ja helpottaa testien tunnistamista ihan huomattavasti.
Alustaminen ja purkaminen
Muistatko XCTestin setUp()- ja tearDown()-metodit? Swift Testingissä ne korvataan Swiftin natiiveilla init- ja deinit-mekanismeilla:
@Suite("Tietokantayhteys")
struct TietokantaTestit {
let tietokanta: TestiTietokanta
init() async throws {
tietokanta = try await TestiTietokanta.luo()
}
@Test func kayttajanLisaaminen() async throws {
try await tietokanta.lisaaKayttaja(nimi: "Matti")
let kayttajat = try await tietokanta.haeKaikki()
#expect(kayttajat.count == 1)
}
}
Jokaiselle testille luodaan uusi instanssi testityypistä, joten tila ei koskaan vuoda testien välillä. Tämä on merkittävä parannus verrattuna XCTestin jaettuun tilaan, jossa on helppo vahingossa sotkea testejä keskenään.
Väittämät: #expect ja #require
Tässä mennään mielestäni asian ytimeen. Swift Testing tiivistää väittämät kahteen makroon — ja ne korvaavat kaikki XCTestin XCTAssert-variantit. Kaksi makroa neljänkymmenen sijaan. Ei hassumpaa.
#expect — pehmeä väittämä
#expect on niin sanottu pehmeä väittämä: testin suoritus jatkuu vaikka se epäonnistuisi. Se hyväksyy minkä tahansa Swift-lausekkeen, joka palauttaa Bool-arvon:
@Test func merkkijonoOperaatiot() {
let nimi = "Helsinki"
#expect(nimi.count == 8)
#expect(nimi.hasPrefix("Hel"))
#expect(nimi.lowercased() == "helsinki")
}
Kun #expect epäonnistuu, se näyttää automaattisesti sekä odotetun että todellisen arvon. Jos esimerkiksi nimi.count olisi 7, näkisit virheilmoituksen: Expectation failed: (nimi.count → 8) == 7. Tämä on iso parannus XCTestin epämääräisiin virheilmoituksiin verrattuna.
Virheitä heittävien funktioiden testaaminen
#expect-makrolla voidaan myös varmistaa, että funktio heittää odotetun virheen:
enum ValidointiVirhe: Error {
case tyhjaKentta
case liianLyhyt
}
@Test func validointiHeittaaVirheen() {
#expect(throws: ValidointiVirhe.tyhjaKentta) {
try validoiSyote("")
}
}
@Test func validointiHeittaaTietynVirheen() {
#expect(throws: ValidointiVirhe.self) {
try validoiSyote("ab")
}
}
#require — kova väittämä
#require on kovempi versio: testin suoritus pysähtyy välittömästi epäonnistumisen yhteydessä. Se on erityisen kätevä optionaalisten arvojen purkamisessa — ja korvaa XCTestin XCTUnwrap-funktion siististi:
@Test func kayttajanHakeminen() throws {
let kayttaja = try #require(haeKayttaja(id: 42))
// Jos kayttaja on nil, testi pysähtyy tähän
#expect(kayttaja.nimi == "Liisa")
#expect(kayttaja.ika > 0)
}
#require vaatii aina try-avainsanan, koska se heittää virheen pysäyttääkseen testin. Tämä pätee riippumatta siitä, heittääkö sisällä oleva lauseke itse virhettä vai ei.
Milloin käyttää kumpaakin?
Peukalosääntö on onneksi yksinkertainen:
- Käytä
#requirekun myöhemmät väittämät riippuvat tuloksesta (esim. optionaalin purkaminen) - Käytä
#expectkun haluat nähdä kaikki epäonnistumiset kerralla
@Test func tilaustenKasittely() throws {
let tilaus = try #require(haeTilaus(id: "ABC-123"))
// require — pysähtyy jos tilaus on nil
#expect(tilaus.tuotteet.count == 3)
// expect — jatkuu vaikka epäonnistuu
#expect(tilaus.kokonaissumma > 0)
// expect — näemme molemmat epäonnistumiset
#expect(tilaus.tila == .vahvistettu)
}
Mukautetut virheilmoitukset
Molemmat makrot tukevat valinnaista virheilmoitusta toisena parametrina, josta on hyötyä erityisesti monimutkaisemmissa testeissä:
@Test func ikarajoitus() {
let ika = 15
#expect(ika >= 18, "Käyttäjän iän tulisi olla vähintään 18, saatiin \(ika)")
}
Parametrisoidut testit
Tämä on henkilökohtaisesti yksi suosikeistani Swift Testingissä. Sen sijaan, että kirjoittaisit erillisen testin jokaiselle syötteelle (tai turvautuisit rumiin silmukoihin), voit määritellä syötteet suoraan @Test-makrossa:
@Test("Valuuttamuunnos", arguments: [
(euro: 1.0, dollari: 1.08),
(euro: 10.0, dollari: 10.80),
(euro: 100.0, dollari: 108.00),
(euro: 0.0, dollari: 0.0)
])
func valuuttamuunnos(euro: Double, dollari: Double) {
let muunnin = ValuuttaMuunnin(kurssi: 1.08)
let tulos = muunnin.eurostaaDollareihin(euro)
#expect(abs(tulos - dollari) < 0.01)
}
Xcode luo automaattisesti erillisen testirivin jokaiselle parametrille testinavigaattoriin. Käytännössä tämä tarkoittaa, että voit ajaa yksittäisen epäonnistuneen tapauksen uudelleen debugataksesi ongelmaa. XCTestin silmukoissa tämä ei ollut mahdollista, ja se aiheutti todella paljon turhaa päänvaivaa.
Enum-arvojen testaaminen
Parametrisoidut testit sopivat erinomaisesti enum-arvojen läpikäymiseen:
enum Viikonpaiva: String, CaseIterable {
case maanantai, tiistai, keskiviikko, torstai, perjantai, lauantai, sunnuntai
}
@Test("Viikonpäivien lokalisointi", arguments: Viikonpaiva.allCases)
func viikonpaivaLokalisointi(paiva: Viikonpaiva) {
let lokalisoitu = paiva.lokalisoituNimi(kieli: "fi")
#expect(!lokalisoitu.isEmpty)
#expect(lokalisoitu.first?.isUppercase == true)
}
Kahden kokoelman yhdistelmät
Swift Testing tukee myös kahden argumenttikokoelman yhdistelmiä zip-tyylisesti:
@Test("HTTP-vastauksen käsittely", arguments:
zip([200, 201, 204, 301, 404, 500],
[true, true, true, false, false, false])
)
func httpVastaus(statusKoodi: Int, onnistui: Bool) {
let vastaus = HTTPVastaus(statusKoodi: statusKoodi)
#expect(vastaus.onnistui == onnistui)
}
Ominaisuusjärjestelmä (Traits)
Ominaisuudet ovat Swift Testingin tapa mukauttaa ja annotoida testejä deklaratiivisesti. Ajattele niitä testien "metatietona", joka vaikuttaa suoritukseen. Ne välitetään @Test- tai @Suite-makron parametreina.
Sisäänrakennetut ominaisuudet
Swift Testing tarjoaa useita käteviä sisäänrakennettuja ominaisuuksia:
// Testin poistaminen käytöstä
@Test(.disabled("Ominaisuus on rikki, korjataan versiossa 2.1"))
func rikkinainenOminaisuus() { }
// Testin merkitseminen tunnettuun bugiin
@Test(.bug("IOS-1234", "Kaatuu käynnistyksessä"))
func bugRaportti() { }
// Ehdollinen suoritus
@Test(.enabled(if: Palvelin.onVerkossa))
func verkkotesti() async { }
// Aikaraja
@Test(.timeLimit(.minutes(2)))
func pitkakestoinenTesti() async { }
// Tagit testien kategorisointiin
@Test(.tags(.verkko, .integraatio))
func apiKutsu() async { }
Tagien käyttö
Tagit ovat mielestäni yksi aliarvostetuimmista ominaisuuksista. Ne auttavat testien organisoinnissa ja suodattamisessa aivan valtavasti. Omat tagit määritellään laajennuksella:
extension Tag {
@Tag static var verkko: Self
@Tag static var tietokanta: Self
@Tag static var integraatio: Self
@Tag static var suorituskyky: Self
}
@Suite(.tags(.tietokanta))
struct TietokantaTestit {
@Test(.tags(.integraatio))
func monimutkaineKysely() async throws {
// Tällä testillä on sekä .tietokanta että .integraatio tagit
}
}
Xcodessa voit suodattaa ja ajaa testejä tagien perusteella, mikä on todella kätevää kun haluat ajaa vaikka pelkästään verkko-testit erikseen.
.serialized — sarjallistettu suoritus
Koska Swift Testing ajaa testit rinnakkain oletusarvoisesti, joskus tarvitaan sarjallistettua suoritusta. Tämä on tyypillistä tilanteissa, joissa testit jakavat jonkin resurssin. .serialized-ominaisuus hoitaa asian:
@Suite("Jaettu tila -testit", .serialized)
struct JaettuTilaTestit {
@Test func ekaVaihe() async {
await JaettuPalvelu.shared.alusta()
}
@Test func tokaVaihe() async {
let tila = await JaettuPalvelu.shared.tila
#expect(tila == .valmis)
}
}
Parametrisoiduissa testeissä .serialized estää saman testin rinnakkaisen suorituksen eri parametreilla:
@Test("Pistemäärä on aina välillä 0...100", .serialized, arguments: [0, 50, 100, 200, -1])
func pisteidenLisays(maara: Int) async {
var pelaaja = Pelaaja()
await pelaaja.lisaaPisteita(maara)
#expect(pelaaja.pisteet >= 0 && pelaaja.pisteet <= 100)
}
Asynkroninen testaus ja confirmation
Swift Testing integroituu saumattomasti Swift Concurrencyn kanssa. Testifunktiot voivat olla async ja throws, ja ne ajetaan rinnakkain oletusarvoisesti. Tämä on iso etu verrattuna XCTestiin, jossa asynkroninen testaus vaati enemmän boilerplate-koodia.
Async-testien kirjoittaminen
@Test func verkkopalvelunKutsu() async throws {
let palvelu = APIClient()
let kayttajat = try await palvelu.haeKayttajat()
#expect(!kayttajat.isEmpty)
#expect(kayttajat.count <= 100)
}
MainActor-eristys
Jos testi tarvitsee suoritusta pääsäikeessä (esimerkiksi UI-komponenttien kanssa työskennellessä), merkitse se @MainActor-attribuutilla:
@Test @MainActor
func uiKomponentinPaivitys() {
let viewModel = ProfiiliViewModel()
viewModel.paivitaNimi("Eero")
#expect(viewModel.naytettavaNimi == "Eero")
}
confirmation — tapahtumien vahvistaminen
confirmation-funktio on tehokas työkalu callback-pohjaisten tapahtumien testaamiseen. Se varmistaa, että tietty tapahtuma tapahtuu odotetun määrän kertoja. Tämä korvaa XCTestin XCTestExpectation-mallin paljon elegantimmin:
@Test func ilmoituksenLahettaminen() async {
let ilmoitusKeskus = IlmoitusKeskus()
await confirmation(expectedCount: 1) { ilmoitusSaapui in
ilmoitusKeskus.kuuntele(.uusiViesti) { _ in
ilmoitusSaapui()
}
await ilmoitusKeskus.laheta(.uusiViesti)
}
}
Voit myös varmistaa, ettei tapahtumaa tapahdu lainkaan asettamalla expectedCount: 0:
@Test func eiTurhiaIlmoituksia() async {
let ilmoitusKeskus = IlmoitusKeskus()
await confirmation(expectedCount: 0) { ilmoitusSaapui in
ilmoitusKeskus.kuuntele(.uusiViesti) { _ in
ilmoitusSaapui()
}
// Ei lähetetä mitään — testin pitäisi onnistua
}
}
Swift 6.2: Ranged Confirmations
Swift 6.2 tuo mukanaan kiinnostavan uutuuden: ranged confirmations. Sen avulla voit määritellä tapahtumamäärän vaihteluvälillä, mikä on hyödyllistä tilanteissa joissa tarkka lukumäärä voi vaihdella:
@Test func tapahtumavirta() async {
await confirmation(expectedCount: 2...5) { tapahtuma in
let virta = TapahtumaVirta()
virta.kuuntele { _ in
tapahtuma()
}
await virta.kaynnista()
}
}
Mukautetut ominaisuudet ja TestScoping (Swift 6.1+)
Swift 6.1 toi mukanaan jotain todella hienoa: mahdollisuuden luoda mukautettuja ominaisuuksia, jotka suorittavat logiikkaa ennen ja jälkeen testien. Käytännössä tämä korvaa tarpeen globaalille tilalle setup- ja teardown-operaatioissa.
TestScoping-protokolla
TestScoping-protokolla mahdollistaa oman kontekstin tarjoamisen testille. Tämä on erityisen hyödyllistä kun useissa testeissä tarvitaan samaa alustuslogiikkaa:
struct MockiAPITunnisteet: TestTrait, TestScoping {
func provideScope(
for test: Test,
testCase: Test.Case?,
performing function: @Sendable () async throws -> Void
) async throws {
let mockTunnisteet = APITunnisteet(apiAvain: "testi-avain-123")
try await APITunnisteet.$nykyinen.withValue(mockTunnisteet) {
try await function()
}
}
}
extension TestTrait where Self == MockiAPITunnisteet {
static var mockiAPItunnisteet: Self { MockiAPITunnisteet() }
}
Nyt voit käyttää tätä ominaisuutta missä tahansa testissä tai kokonaisessa suitessa:
@Test(.mockiAPItunnisteet)
func apiKutsuMockiTunnisteilla() async throws {
let vastaus = try await apiClient.haeData()
#expect(vastaus.tila == .ok)
}
@Suite(.mockiAPItunnisteet)
struct KokoAPITestiSarja {
@Test func haeKayttajat() async throws { }
@Test func haeTuotteet() async throws { }
}
Käytännön esimerkki: Testitietokannan hallinta
TestScoping loistaa erityisesti testitietokannan hallinnassa. Tässä on esimerkki, jota olen itse käyttänyt vastaavaan tarkoitukseen:
struct TestiTietokantaTrait: TestTrait, TestScoping {
func provideScope(
for test: Test,
testCase: Test.Case?,
performing function: @Sendable () async throws -> Void
) async throws {
let db = try await TestiTietokanta.luoValiaikainen()
try await TestiTietokanta.$nykyinen.withValue(db) {
try await function()
}
// Tietokanta siivotaan automaattisesti scopesta poistuttaessa
}
}
extension TestTrait where Self == TestiTietokantaTrait {
static var testiTietokanta: Self { TestiTietokantaTrait() }
}
@Suite(.testiTietokanta)
struct KayttajaRepositorioTestit {
@Test func lisaaJaHaeKayttaja() async throws {
let repo = KayttajaRepositorio(db: TestiTietokanta.nykyinen)
try await repo.lisaa(Kayttaja(nimi: "Anna"))
let haettu = try await repo.hae(nimella: "Anna")
let kayttaja = try #require(haettu)
#expect(kayttaja.nimi == "Anna")
}
}
Testihierarkiat ja organisointi
Swift Testing tukee sisäkkäisiä suite-rakenteita, joiden avulla testit voidaan järjestää loogisiin hierarkioihin. Tämä on erityisen hyödyllistä isommissa projekteissa, joissa testien määrä kasvaa nopeasti:
@Suite("Verkkokauppa")
struct VerkkokauppaTestit {
@Suite("Ostoskori")
struct OstoskoriTestit {
@Test func tuotteenLisaaminen() {
var kori = Ostoskori()
kori.lisaa(Tuote(nimi: "Swift-kirja", hinta: 29.99))
#expect(kori.tuotteet.count == 1)
}
@Test func kokonaissummanLaskenta() {
var kori = Ostoskori()
kori.lisaa(Tuote(nimi: "Swift-kirja", hinta: 29.99))
kori.lisaa(Tuote(nimi: "Xcode-opas", hinta: 19.99))
#expect(kori.kokonaissumma == 49.98)
}
}
@Suite("Maksuprosessi")
struct MaksuprosessiTestit {
@Test func onnistunutMaksu() async throws {
let maksu = try await Maksupalvelu.suorita(summa: 49.98)
#expect(maksu.tila == .onnistunut)
}
}
}
Tämä luo Xcoden testinavigaattoriin selkeän puurakenteen. Kun testejä on satoja, tämä organisointitapa on kultaakin kalliimpi.
Siirtyminen XCTestistä Swift Testingiin
Hyvä uutinen: XCTest- ja Swift Testing -testit voivat elää rinnakkain samassa testikohteessa, jopa samassa tiedostossa. Tämä tarkoittaa, ettei sinun tarvitse tehdä massiivista migraatiota kerralla.
Vaiheittainen migraatiostrategia
Apple suosittelee seuraavaa lähestymistapaa (ja olen itse samaa mieltä):
- Kirjoita uudet testit Swift Testingillä — kaikki uudet ominaisuudet testataan uudella kehyksellä
- Migroi vanhat testit tilaisuuksien mukaan — kun korjaat bugia tai muutat vanhaa testiä, migroi se samalla
- Älä yritä massamigraatiota — se on riskialtista ja vie paljon aikaa ilman selvää hyötyä
Konkreettinen migraatioesimerkki
Katsotaan tyypillinen XCTest-testi ja sen Swift Testing -vastine vierekkäin:
XCTest (ennen):
import XCTest
@testable import MinunSovellus
final class LaskinTestit: XCTestCase {
var laskin: Laskin!
override func setUp() {
super.setUp()
laskin = Laskin()
}
override func tearDown() {
laskin = nil
super.tearDown()
}
func testYhteenlasku() {
XCTAssertEqual(laskin.laske(2, plus: 3), 5)
}
func testNollallaJako() {
XCTAssertThrowsError(try laskin.jaa(10, pisteella: 0)) { error in
XCTAssertTrue(error is LaskinVirhe)
}
}
func testOptionalTulos() throws {
let tulos = try XCTUnwrap(laskin.valinnainen(42))
XCTAssertGreaterThan(tulos, 0)
}
}
Swift Testing (jälkeen):
import Testing
@testable import MinunSovellus
@Suite("Laskin")
struct LaskinTestit {
let laskin = Laskin()
@Test("Yhteenlasku positiivisilla luvuilla")
func yhteenlasku() {
#expect(laskin.laske(2, plus: 3) == 5)
}
@Test("Nollalla jako heittää virheen")
func nollallaJako() {
#expect(throws: LaskinVirhe.self) {
try laskin.jaa(10, pisteella: 0)
}
}
@Test("Valinnainen tulos puretaan oikein")
func optionalTulos() throws {
let tulos = try #require(laskin.valinnainen(42))
#expect(tulos > 0)
}
}
Huomaatko kuinka paljon siistimpi jälkimmäinen versio on? Ei perintää, ei setUp/tearDown-tanssia, ei force-unwrappeja.
Migraation keskeiset säännöt
- Älä sekoita väittämiä — älä kutsu
XCTAssert-funktioita Swift Testing -testistä tai#expect-makroa XCTest-testistä - Korvaa
setUp/tearDownSwiftininit/deinit-mekanismilla - Korvaa
XCTUnwrapkäyttämällätry #require-makroa - Huomioi rinnakkaisuus — testit ajetaan nyt rinnakkain, joten jaettu tila voi aiheuttaa yllätyksiä. Käytä
.serialized-ominaisuutta väliaikaisratkaisuna - Pidä UI- ja suorituskykytestit XCTestissä — Swift Testing ei tue näitä vielä
Xcode 26:n uudet testausominaisuudet
Xcode 26 tuo mukanaan muutaman kivan lisäyksen testaustyönkulkuun.
Runtime Issue Detection
Uusi Runtime Issue Detection -ominaisuus tunnistaa ajonaikaisia ongelmia testien aikana automaattisesti. Oletusarvoisesti ongelmat raportoidaan varoituksina — eivät virheinä — joten olemassa olevat testit eivät rikkoudu pelkästä Xcode-päivityksestä:
// Runtime-ongelmat näkyvät varoituksina testiraportissa
// Esimerkiksi: pääsäikeen ulkopuolelta tehty UI-päivitys
@Test func dataLataus() async throws {
let data = try await lataaData()
// Jos lataaData() päivittää UI:ta taustasäikeestä,
// Xcode 26 varoittaa tästä automaattisesti
#expect(!data.isEmpty)
}
Tekoälypohjainen testiavustaja
Xcode 26:n integroitu tekoälyavustaja osaa auttaa testien kirjoittamisessa, testikattavuuden tunnistamisessa ja virheiden analysoinnissa. Yhdistettynä Swift Testingin selkeään syntaksiin tämä tekee testauksesta entistä nopeampaa.
Parhaat käytännöt
Tässä on kokoelma käytäntöjä, jotka olen havainnut toimiviksi Swift Testingin kanssa.
1. Suosi structeja testisuiteina
Structit ovat oletusvalinta. Ne estävät tilan jakamisen testien välillä, mikä eliminoi kokonaisen kategorian bugeja. Käytä luokkaa vain jos tarvitset deinit-mekanismia resurssien vapauttamiseen.
2. Nimeä testit kuvaavasti
Hyödynnä @Test-makron merkkijonoparametria:
// Huono — ei kerro mitä testataan
@Test func testi1() { }
// Hyvä — kertoo tarkalleen mitä testataan
@Test("Ostoskoriin lisääminen päivittää kokonaissumman")
func ostoskorinKokonaissumma() { }
3. Hyödynnä parametrisoituja testejä ahkerasti
Aina kun huomaat kirjoittavasi useita samankaltaisia testejä eri arvoilla, se on merkki siitä, että parametrisoitu testi olisi parempi ratkaisu.
4. Käytä tageja johdonmukaisesti
Määrittele projektin laajuiset tagit ja käytä niitä järjestelmällisesti. Tämä mahdollistaa tiettyjen testikategorioiden ajamisen CI/CD-putkissa:
extension Tag {
@Tag static var nopea: Self // < 1 sekunti
@Tag static var hidas: Self // > 1 sekunti
@Tag static var verkko: Self // vaatii verkkoyhteyden
@Tag static var integraatio: Self // integraatiotestit
}
5. Vältä testien välistä riippuvuutta
Jokaisen testin tulee olla itsenäinen. Jos testit riippuvat toisistaan, käytä TestScoping-ominaisuuksia yhteisen kontekstin tarjoamiseen — älä luota suoritusjärjestykseen.
6. Aseta aikarajat pitkäkestoisille testeille
.timeLimit-ominaisuus estää testejä jumiutumasta ja hidastamasta koko testisarjaa:
@Test(.timeLimit(.seconds(30)))
func verkkopalvelunVastaus() async throws {
let vastaus = try await apiClient.haeData()
#expect(vastaus.tila == .ok)
}
7. Dokumentoi poistossa olevat testit
Kun poistat testin käytöstä, kirjoita aina syy. Tulevaisuuden sinä kiittää:
@Test(.disabled("Odotetaan backend-tiimiä korjaamaan API-endpointti #IOS-456"))
func maksunKasittely() async throws { }
Kattava käytännön esimerkki
Kootaan kaikki yhteen kattavalla esimerkillä. Tässä testataan yksinkertaista tehtävänhallintajärjestelmää hyödyntäen lähes kaikkia aiemmin käsiteltyjä ominaisuuksia:
import Testing
@testable import TehtavaApp
// Tagien määrittely
extension Tag {
@Tag static var malli: Self
@Tag static var palvelu: Self
}
// Testitietokanta-ominaisuus
struct TestiYmparistoTrait: TestTrait, TestScoping {
func provideScope(
for test: Test,
testCase: Test.Case?,
performing function: @Sendable () async throws -> Void
) async throws {
let ymparisto = try await TestiYmparisto.luo()
try await TestiYmparisto.$nykyinen.withValue(ymparisto) {
try await function()
}
}
}
extension TestTrait where Self == TestiYmparistoTrait {
static var testiYmparisto: Self { TestiYmparistoTrait() }
}
// Testisuite
@Suite("Tehtävänhallinta", .testiYmparisto)
struct TehtavanhallintaTestit {
@Suite("Tehtävämalli", .tags(.malli))
struct MalliTestit {
@Test("Tehtävän luominen oletusarvoilla")
func luominen() {
let tehtava = Tehtava(otsikko: "Opettele Swift Testing")
#expect(tehtava.otsikko == "Opettele Swift Testing")
#expect(tehtava.tila == .avoin)
#expect(tehtava.prioriteetti == .normaali)
}
@Test("Tehtävän prioriteetit", arguments: Prioriteetti.allCases)
func prioriteetit(prioriteetti: Prioriteetti) {
let tehtava = Tehtava(otsikko: "Testi", prioriteetti: prioriteetti)
#expect(tehtava.prioriteetti == prioriteetti)
#expect(tehtava.prioriteetti.rawValue > 0)
}
@Test("Tyhjä otsikko ei ole sallittu")
func tyhjaOtsikko() {
#expect(throws: TehtavaVirhe.tyhjaOtsikko) {
try Tehtava.validoitu(otsikko: "")
}
}
}
@Suite("Tehtäväpalvelu", .tags(.palvelu))
struct PalveluTestit {
@Test("Tehtävän tallentaminen ja hakeminen")
func tallennusJaHaku() async throws {
let palvelu = TehtavaPalvelu(ymparisto: .nykyinen)
let luotu = try await palvelu.luo(otsikko: "Uusi tehtävä")
let haettu = try #require(await palvelu.hae(id: luotu.id))
#expect(haettu.otsikko == "Uusi tehtävä")
}
@Test("Tehtävän tilan päivitys", .serialized, arguments: [
Tila.kesken, Tila.valmis, Tila.peruutettu
])
func tilanPaivitys(uusiTila: Tila) async throws {
let palvelu = TehtavaPalvelu(ymparisto: .nykyinen)
let tehtava = try await palvelu.luo(otsikko: "Tilantesti")
try await palvelu.paivitaTila(tehtava.id, tila: uusiTila)
let paivitetty = try #require(await palvelu.hae(id: tehtava.id))
#expect(paivitetty.tila == uusiTila)
}
@Test("Ilmoitus lähetetään tehtävän valmistuessa")
func valmistumisilmoitus() async throws {
let palvelu = TehtavaPalvelu(ymparisto: .nykyinen)
let tehtava = try await palvelu.luo(otsikko: "Ilmoitustesti")
await confirmation(expectedCount: 1) { ilmoitus in
palvelu.kuunteleIlmoituksia { tyyppi in
if tyyppi == .tehtavaValmis {
ilmoitus()
}
}
try? await palvelu.paivitaTila(tehtava.id, tila: .valmis)
}
}
}
}
Yhteenveto
Swift Testing on iso askel eteenpäin iOS-testauksessa. Se tuo mukanaan modernin syntaksin, joka hyödyntää Swiftin parhaita puolia — makroja, rinnakkaisuutta ja tyyppijärjestelmää. Ja mikä parasta, siirtymä ei tarvitse tapahtua kerralla.
Keskeiset muistettavat asiat:
@Testja@SuitekorvaavatXCTestCase-aliluokat#expectja#requirekorvaavat kaikkiXCTAssert-variantit- Parametrisoidut testit vähentävät toistuvan koodin määrää merkittävästi
- Ominaisuudet (traits) mahdollistavat testien mukauttamisen deklaratiivisesti
- TestScoping (Swift 6.1+) tarjoaa modernin tavan hallita setup/teardown-logiikkaa
- confirmation mahdollistaa callback-pohjaisten tapahtumien testaamisen elegantisti
- Vaiheittainen migraatio XCTestistä on mahdollista ja suositeltavaa
Suosittelen aloittamaan uusien testien kirjoittamisen Swift Testingillä heti ja migroimaan vanhoja testejä pikkuhiljaa tilaisuuksien tullen. Kehys kehittyy jatkuvasti — Swift 6.2 tuo jo mukanaan ranged confirmations -ominaisuuden ja muita parannuksia. Testauksen tulevaisuus Swiftissä näyttää todella lupaavalta.