Made the live tests resilient to live service data changes in the library tests target..

This commit is contained in:
2026-07-26 01:54:04 +02:00
parent 03f66cdeb3
commit c1c6f147f6
@@ -16,7 +16,14 @@ import AmiiboService
import Foundation import Foundation
import Testing import Testing
@Suite("Amiibo Service", .tags(.live)) @Suite(
"Amiibo Service",
.tags(.live),
.enabled(
if: ProcessInfo.processInfo.environment["AMIIBO_LIVE_TESTS"] == "1",
"Set the 'AMIIBO_LIVE_TESTS' environment variable to '1' to run these tests against the live service."
)
)
struct AmiiboServiceLiveTests { struct AmiiboServiceLiveTests {
// MARK: Properties // MARK: Properties
@@ -37,11 +44,11 @@ struct AmiiboServiceLiveTests {
)) ))
func `get amiibos`( func `get amiibos`(
filter: AmiiboFilter, filter: AmiiboFilter,
expects numberOfItems: Int expects count: ExpectedCount
) async throws { ) async throws {
try await assertAmiibos( try await assertAmiibos(
with: filter, with: filter,
expects: numberOfItems expects: count
) )
} }
@@ -65,11 +72,11 @@ struct AmiiboServiceLiveTests {
)) ))
func `get amiibo series`( func `get amiibo series`(
filter: AmiiboSeriesFilter, filter: AmiiboSeriesFilter,
expects numberOfItems: Int expects count: ExpectedCount
) async throws { ) async throws {
try await assertAmiiboSeries( try await assertAmiiboSeries(
with: filter, with: filter,
expects: numberOfItems expects: count
) )
} }
@@ -93,11 +100,11 @@ struct AmiiboServiceLiveTests {
)) ))
func `get amiibo types`( func `get amiibo types`(
filter: AmiiboTypeFilter, filter: AmiiboTypeFilter,
expects numberOfItems: Int expects count: ExpectedCount
) async throws { ) async throws {
try await assertAmiiboTypes( try await assertAmiiboTypes(
with: filter, with: filter,
expects: numberOfItems expects: count
) )
} }
@@ -121,11 +128,11 @@ struct AmiiboServiceLiveTests {
)) ))
func `get game characters`( func `get game characters`(
filter: GameCharacterFilter, filter: GameCharacterFilter,
expects numberOfItems: Int expects count: ExpectedCount
) async throws { ) async throws {
try await assertGameCharacters( try await assertGameCharacters(
with: filter, with: filter,
expects: numberOfItems expects: count
) )
} }
@@ -149,11 +156,11 @@ struct AmiiboServiceLiveTests {
)) ))
func `get game series`( func `get game series`(
filter: GameSeriesFilter, filter: GameSeriesFilter,
expects numberOfItems: Int expects count: ExpectedCount
) async throws { ) async throws {
try await assertGameSeries( try await assertGameSeries(
with: filter, with: filter,
expects: numberOfItems expects: count
) )
} }
@@ -174,7 +181,7 @@ struct AmiiboServiceLiveTests {
@Test @Test
func `get the last updated timestamp`() async throws { func `get the last updated timestamp`() async throws {
try await assertLastUpdated( try await assertLastUpdated(
day: 24, onOrAfterDay: 24,
month: 7, month: 7,
year: 2026 year: 2026
) )
@@ -191,17 +198,17 @@ private extension AmiiboServiceLiveTests {
/// Asserts the number of items returned by the `amiibos` endpoint that matched a given filter. /// Asserts the number of items returned by the `amiibos` endpoint that matched a given filter.
/// - Parameters: /// - Parameters:
/// - filter: An amiibo filter type. /// - filter: An amiibo filter type.
/// - numberOfItems: An expected number of items returned. /// - count: An expected count of items to be returned.
func assertAmiibos( func assertAmiibos(
with filter: AmiiboFilter, with filter: AmiiboFilter,
expects numberOfItems: Int expects count: ExpectedCount
) async throws { ) async throws {
// GIVEN // GIVEN
// WHEN // WHEN
let amiibos = try await service.getAmiibos(filter) let amiibos = try await service.getAmiibos(filter)
// THEN // THEN
#expect(amiibos.count == numberOfItems) assert(amiibos.count, matches: count)
guard guard
!amiibos.isEmpty, !amiibos.isEmpty,
@@ -240,17 +247,17 @@ private extension AmiiboServiceLiveTests {
/// Asserts the number of items returned by the `amiiboSeries` endpoint that matched a given filter. /// Asserts the number of items returned by the `amiiboSeries` endpoint that matched a given filter.
/// - Parameters: /// - Parameters:
/// - filter: An amiibo series filter type. /// - filter: An amiibo series filter type.
/// - numberOfItems: An expected number of items returned. /// - count: An expected count of items to be returned.
func assertAmiiboSeries( func assertAmiiboSeries(
with filter: AmiiboSeriesFilter, with filter: AmiiboSeriesFilter,
expects numberOfItems: Int expects count: ExpectedCount
) async throws { ) async throws {
// GIVEN // GIVEN
// WHEN // WHEN
let amiiboSeries = try await service.getAmiiboSeries(filter) let amiiboSeries = try await service.getAmiiboSeries(filter)
// THEN // THEN
#expect(amiiboSeries.count == numberOfItems) assert(amiiboSeries.count, matches: count)
} }
/// Asserts the error thrown by the `amiiboSeries` endpoint. /// Asserts the error thrown by the `amiiboSeries` endpoint.
@@ -272,17 +279,17 @@ private extension AmiiboServiceLiveTests {
/// Asserts the number of items returned by the `amiiboTypes` endpoint that matched a given filter. /// Asserts the number of items returned by the `amiiboTypes` endpoint that matched a given filter.
/// - Parameters: /// - Parameters:
/// - filter: An amiibo type filter type. /// - filter: An amiibo type filter type.
/// - numberOfItems: An expected number of items returned. /// - count: An expected count of items to be returned.
func assertAmiiboTypes( func assertAmiiboTypes(
with filter: AmiiboTypeFilter, with filter: AmiiboTypeFilter,
expects numberOfItems: Int expects count: ExpectedCount
) async throws { ) async throws {
// GIVEN // GIVEN
// WHEN // WHEN
let amiiboTypes = try await service.getAmiiboTypes(filter) let amiiboTypes = try await service.getAmiiboTypes(filter)
// THEN // THEN
#expect(amiiboTypes.count == numberOfItems) assert(amiiboTypes.count, matches: count)
} }
/// Asserts the error thrown by the `amiiboTypes` endpoint. /// Asserts the error thrown by the `amiiboTypes` endpoint.
@@ -304,17 +311,17 @@ private extension AmiiboServiceLiveTests {
/// Asserts the number of items returned by the `gameCharacters` endpoint that matched a given filter. /// Asserts the number of items returned by the `gameCharacters` endpoint that matched a given filter.
/// - Parameters: /// - Parameters:
/// - filter: A game character filter type. /// - filter: A game character filter type.
/// - numberOfItems: An expected number of items returned. /// - count: An expected count of items to be returned.
func assertGameCharacters( func assertGameCharacters(
with filter: GameCharacterFilter, with filter: GameCharacterFilter,
expects numberOfItems: Int expects count: ExpectedCount
) async throws { ) async throws {
// GIVEN // GIVEN
// WHEN // WHEN
let gameCharacters = try await service.getGameCharacters(filter) let gameCharacters = try await service.getGameCharacters(filter)
// THEN // THEN
#expect(gameCharacters.count == numberOfItems) assert(gameCharacters.count, matches: count)
} }
/// Asserts the error thrown by the `gameCharacters` endpoint. /// Asserts the error thrown by the `gameCharacters` endpoint.
@@ -336,17 +343,17 @@ private extension AmiiboServiceLiveTests {
/// Asserts the number of items returned by the `gameSeries` endpoint that matched a given filter. /// Asserts the number of items returned by the `gameSeries` endpoint that matched a given filter.
/// - Parameters: /// - Parameters:
/// - filter: A game series filter type. /// - filter: A game series filter type.
/// - numberOfItems: An expected number of items returned. /// - count: An expected count of items to be returned.
func assertGameSeries( func assertGameSeries(
with filter: GameSeriesFilter, with filter: GameSeriesFilter,
expects numberOfItems: Int expects count: ExpectedCount
) async throws { ) async throws {
// GIVEN // GIVEN
// WHEN // WHEN
let gameSeries = try await service.getGameSeries(filter) let gameSeries = try await service.getGameSeries(filter)
// THEN // THEN
#expect(gameSeries.count == numberOfItems) assert(gameSeries.count, matches: count)
} }
/// Asserts the error thrown by the `gameSeries` endpoint. /// Asserts the error thrown by the `gameSeries` endpoint.
@@ -365,31 +372,61 @@ private extension AmiiboServiceLiveTests {
} }
} }
/// Asserts the date returned by the `lastUpdated` endpoint. /// Asserts the date returned by the `lastUpdated` endpoint is within a plausible range.
///
/// The live service updates its data over time, so this assertion checks the returned date is not before the last known update and not in the future, instead of matching an exact date that would break on every update.
///
/// - Parameters: /// - Parameters:
/// - day: A number of day of the last updated date. /// - day: A number of day of the earliest expected last updated date.
/// - month: A number of month of the last updated date. /// - month: A number of month of the earliest expected last updated date.
/// - year: A number of year of the last updated date. /// - year: A number of year of the earliest expected last updated date.
func assertLastUpdated( func assertLastUpdated(
day: Int, onOrAfterDay day: Int,
month: Int, month: Int,
year: Int year: Int
) async throws { ) async throws {
// GIVEN // GIVEN
var calendar = Calendar(identifier: .gregorian)
calendar.timeZone = try #require(TimeZone(secondsFromGMT: 0))
let earliest = try #require(calendar.date(from: .init(
year: year,
month: month,
day: day
)))
// WHEN // WHEN
let dateLastUpdated = try await service.getLastUpdated() let dateLastUpdated = try await service.getLastUpdated()
// THEN // THEN
let dateComponents = Calendar.current.dateComponents( #expect(dateLastUpdated >= earliest)
[.year, .month, .day], #expect(dateLastUpdated <= .now)
from: dateLastUpdated
)
#expect(dateComponents.year == year)
#expect(dateComponents.month == month)
#expect(dateComponents.day == day)
} }
/// Asserts an actual number of items matches an expected count.
/// - Parameters:
/// - actualCount: A number of items returned by an endpoint.
/// - expectation: An expected count to match the number of items against.
func assert(_ actualCount: Int, matches expectation: ExpectedCount) {
switch expectation {
case let .exactly(expected):
#expect(actualCount == expected)
case let .atLeast(minimum):
#expect(actualCount >= minimum)
}
}
}
// MARK: - Expectations
/// An expectation about the number of items returned by an endpoint of the live service.
enum ExpectedCount {
/// The endpoint is expected to return exactly the associated number of items.
case exactly(Int)
/// The endpoint is expected to return at least the associated number of items, as the data at the live service grows over time.
case atLeast(Int)
} }
// MARK: - Arguments // MARK: - Arguments
@@ -509,24 +546,37 @@ enum Input {
} }
enum Output { enum Output {
/// A list of number of items that are expected from the `assertAmiibos` assertion. /// A list of expected counts from the `assertAmiibos` assertion.
static let amiibos: [Int] = [.totalAmiibos, 7, 7, 1, 1, 1, .zero, .zero, 5, .zero, 7, .totalAmiibos, 254, 254, .zero, .zero, .zero, .zero, 96, 26, .zero, .zero, 63, .totalAmiibos, 14, 6, .zero, .zero, .zero, .totalAmiibos, 53, 32, .zero, .zero, 152, .totalAmiibos, .totalAmiibos, .totalAmiibos] ///
/// Counts for filters that match a growing set of items are expressed as `atLeast` minimums, so the tests do not break whenever new amiibo items are added to the live service.
static let amiibos: [ExpectedCount] = [
.atLeast(.minimumAmiibos),
.atLeast(7), .atLeast(7),
.exactly(1), .atLeast(1),
.exactly(1), .exactly(.zero), .exactly(.zero),
.atLeast(5), .exactly(.zero), .atLeast(7), .atLeast(.minimumAmiibos),
.atLeast(254), .atLeast(254), .exactly(.zero), .exactly(.zero), .exactly(.zero), .exactly(.zero),
.atLeast(96), .atLeast(26), .exactly(.zero), .exactly(.zero), .atLeast(63), .atLeast(.minimumAmiibos),
.atLeast(14), .atLeast(6), .exactly(.zero), .exactly(.zero), .exactly(.zero), .atLeast(.minimumAmiibos),
.atLeast(53), .atLeast(32), .exactly(.zero), .exactly(.zero), .atLeast(152), .atLeast(.minimumAmiibos),
.atLeast(.minimumAmiibos), .atLeast(.minimumAmiibos)
]
/// A list of errors are expected to be thrown from the `assertAmiibosThrows` assertion. /// A list of errors are expected to be thrown from the `assertAmiibosThrows` assertion.
static let amiibosThrows: [AmiiboServiceError] = [.badRequest, .badRequest, .badRequest, .badRequest, .badRequest, .badRequest, .badRequest] static let amiibosThrows: [AmiiboServiceError] = [.badRequest, .badRequest, .badRequest, .badRequest, .badRequest, .badRequest, .badRequest]
/// A list of number of items that are expected from the `assertAmiiboSeries` assertion. /// A list of expected counts from the `assertAmiiboSeries` assertion.
static let amiiboSeries: [Int] = [.totalAmiiboSeries, 1, 1, 1, .totalAmiiboSeries] static let amiiboSeries: [ExpectedCount] = [.atLeast(.minimumAmiiboSeries), .exactly(1), .exactly(1), .exactly(1), .atLeast(.minimumAmiiboSeries)]
/// A list of errors are expected to be thrown from the `assertAmiiboSeriesThrows` assertion. /// A list of errors are expected to be thrown from the `assertAmiiboSeriesThrows` assertion.
static let amiiboSeriesThrows: [AmiiboServiceError] = [.notFound, .badRequest, .badRequest, .notFound] static let amiiboSeriesThrows: [AmiiboServiceError] = [.notFound, .badRequest, .badRequest, .notFound]
/// A list of number of items that are expected from the `assertAmiiboTypes` assertion. /// A list of expected counts from the `assertAmiiboTypes` assertion.
static let amiiboTypes: [Int] = [.totalAmiiboTypes, 1, 1, 1, .totalAmiiboTypes] static let amiiboTypes: [ExpectedCount] = [.atLeast(.minimumAmiiboTypes), .exactly(1), .exactly(1), .exactly(1), .atLeast(.minimumAmiiboTypes)]
/// A list of errors are expected to be thrown from the `assertAmiiboTypesThrows` assertion. /// A list of errors are expected to be thrown from the `assertAmiiboTypesThrows` assertion.
static let amiiboTypesThrows: [AmiiboServiceError] = [.notFound, .badRequest, .badRequest, .notFound] static let amiiboTypesThrows: [AmiiboServiceError] = [.notFound, .badRequest, .badRequest, .notFound]
/// A list of number of items that are expected from the `assertGameCharacters` assertion. /// A list of expected counts from the `assertGameCharacters` assertion.
static let gameCharacters: [Int] = [.totalGameCharacters, 1, 1, 1, .totalGameCharacters] static let gameCharacters: [ExpectedCount] = [.atLeast(.minimumGameCharacters), .exactly(1), .exactly(1), .exactly(1), .atLeast(.minimumGameCharacters)]
/// A list of errors are expected to be thrown from the `assertGameCharactersThrows` assertion. /// A list of errors are expected to be thrown from the `assertGameCharactersThrows` assertion.
static let gameCharactersThrows: [AmiiboServiceError] = [.notFound, .badRequest, .badRequest, .notFound] static let gameCharactersThrows: [AmiiboServiceError] = [.notFound, .badRequest, .badRequest, .notFound]
/// A list of number of items that are expected from the `assertGameSeries` assertion. /// A list of expected counts from the `assertGameSeries` assertion.
static let gameSeries: [Int] = [.totalGameSeries, 1, 1, 1, .totalGameSeries] static let gameSeries: [ExpectedCount] = [.atLeast(.minimumGameSeries), .exactly(1), .exactly(1), .exactly(1), .atLeast(.minimumGameSeries)]
/// A list of errors are expected to be thrown from the `assertGameSeriesThrows` assertion. /// A list of errors are expected to be thrown from the `assertGameSeriesThrows` assertion.
static let gameSeriesThrows: [AmiiboServiceError] = [.notFound, .badRequest, .badRequest, .notFound] static let gameSeriesThrows: [AmiiboServiceError] = [.notFound, .badRequest, .badRequest, .notFound]
} }
@@ -534,16 +584,16 @@ enum Output {
// MARK: - Constants // MARK: - Constants
private extension Int { private extension Int {
/// A number that represents the total number of amiibo items currently available at the live service. /// A number that represents the minimum number of amiibo items expected from the live service, as of July 2026.
static let totalAmiibos = 946 static let minimumAmiibos = 946
/// A number that represents the total number of amiibo series currently available at the live service. /// A number that represents the minimum number of amiibo series expected from the live service, as of July 2026.
static let totalAmiiboSeries = 31 static let minimumAmiiboSeries = 31
/// A number that represents the total number of amiibo types currently available at the live service. /// A number that represents the minimum number of amiibo types expected from the live service, as of July 2026.
static let totalAmiiboTypes = 5 static let minimumAmiiboTypes = 5
/// A number that represents the total number of game characters currently available at the live service. /// A number that represents the minimum number of game characters expected from the live service, as of July 2026.
static let totalGameCharacters = 681 static let minimumGameCharacters = 681
/// A number that represents the total number of game series currently available at the live service. /// A number that represents the minimum number of game series expected from the live service, as of July 2026.
static let totalGameSeries = 118 static let minimumGameSeries = 118
} }
private extension String { private extension String {