闽公网安备 35020302035485号
class ModelTests {
let container: ModelContainer
init() throws {
let config = ModelConfiguration(isStoredInMemoryOnly: true)
container = tryModelContainer(for: User.self, configurations: config)
}
@Testfunc verifyBulkImport()throws {
User.bulkImport()
let modelContext = ModelContext(container)
letcount = try modelContext.fetchCount(FetchDescriptor<User>())
#expect(count == 100)
}
}
这段代码中,init 里创建了个内存中的 ModelContainer,然后测试方法里用它来检查导入功能。单看这个例子没啥问题,但实际项目中...咱们面临的情况可复杂多了。@Test(
.enabled(if: FeatureFlag.addition)
) func verifyAdd() {
let result = add(1, 2)
#expect(result == 3)
}
Scoping 更进一步 - 它允许我们在测试函数或测试套件执行前后插入自定义代码。有了它,就能创建可重用的测试环境设置,一次定义,到处使用,爽歪歪~struct Environment {
let search: (String) async throws -> [String]
}
extension Environment {
privatestaticvar production: Environment {
// 生产环境实现...(略过实现细节)
}
privatestaticvar mock: Environment {
// 模拟环境...(这里可以返回假数据)
}
}
extension Environment {
@TaskLocalstaticvar current = Environment.production
}
这段代码定义了个 Environment 结构体存放应用依赖,用 TaskLocal 存当前环境。默认是生产环境,但测试中想用模拟环境。下面是重点,通过 Scoping 把环境切换到测试模式:struct MockEnvironmentTrait: TestTrait, SuiteTrait, TestScoping {
func provideScope(
for test: Test,
testCase: Test.Case?,
performing function: @Sendable () async throws -> Void
) async throws {
try await Environment.$current.withValue(Environment.mock) {
try await function()
}
}
}
extension Trait where Self == MockEnvironmentTrait {
static var mockedEnvironment: Self { Self() }
}
这段代码乍一看挺吓人,但实际上就干了一件事:在测试运行期间,把环境临时切换到模拟版本。@Test(.mockedEnvironment) func verifySomething() async throws {
// 这里的 Environment.current 自动是模拟环境
let results = try await Environment.current.search("query")
#expect(!results.isEmpty)
}
@Suite(.mockedEnvironment) struct ExamplesTests {
// 整个测试套件都用模拟环境,不用一遍遍设置
}
只要加个 .mockedEnvironment 标记,测试就会在模拟环境中运行,完全不需要重复代码。感觉像是给测试加了个"魔法标签",标记一下就能改变整个测试的运行环境。struct NetworkService {
var fetch: (URL) async throws -> Data
}
extension NetworkService {
staticlet live = NetworkService(
fetch: { url in
let (data, _) = try await URLSession.shared.data(from: url)
return data
}
)
staticlet mock = NetworkService(
fetch: { url in
// 根据URL返回假数据
if url.absoluteString.contains("users") {
returnData("""
[{"id": 1, "name": "小明"}]
""".utf8)
}
returnData()
}
)
}
// 存储当前服务
extension NetworkService {
@TaskLocalstaticvar current = NetworkService.live
}
然后创建测试作用域:struct MockNetworkTrait: TestTrait, SuiteTrait, TestScoping {
func provideScope(
for test: Test,
testCase: Test.Case?,
performing function: @Sendable () async throws -> Void
) async throws {
try await NetworkService.$current.withValue(NetworkService.mock) {
try await function()
}
}
}
extension Trait where Self == MockNetworkTrait {
static var mockNetwork: Self { Self() }
}
使用起来超轻松:@Test(.mockNetwork) func testUserFetching() async throws {
let service = NetworkService.current // 自动是模拟服务!
let userData = try await service.fetch(URL(string: "https://api.example.com/users")!)
let users = try JSONDecoder().decode([User].self, from: userData)
#expect(users.count == 1)
#expect(users[0].name == "小明")
}
就一个 .mockNetwork 标记,测试中就能用模拟网络服务,连 NetworkService.mock 都不用写了,是不是很方便?class NetworkTests {
let mockService = NetworkService.mock
func testUserFetching() async throws {
let userData = try await mockService.fetch(URL(string: "https://api.example.com/users")!)
let users = tryJSONDecoder().decode([User].self, from: userData)
XCTAssertEqual(users.count, 1)
XCTAssertEqual(users[0].name, "小明")
}
func testProductFetching() async throws {
// 还得用同样的mock
let userData = try await mockService.fetch(URL(string: "https://api.example.com/products")!)
// 更多测试...
}
}
// 另一个测试类要用同样的mock...又得重复一遍
class OrderTests {
let mockService = NetworkService.mock
// 又是一堆重复设置...
}
新方法:@Suite(.mockNetwork) struct NetworkTests {
func testUserFetching() async throws {
let service = NetworkService.current // 自动有mock环境
let userData = try await service.fetch(URL(string: "https://api.example.com/users")!)
let users = tryJSONDecoder().decode([User].self, from: userData)
#expect(users.count == 1)
#expect(users[0].name == "小明")
}
func testProductFetching() async throws {
// 不用重复设置mock
// 直接用...
}
}
@Suite(.mockNetwork) struct OrderTests {
// 同样不用重复设置,一个标记搞定
}
新写法是不是干净多了?以后要改模拟服务的行为,只要改一个地方,不用翻遍整个测试目录去查找替换。我第一次用这种方式重构测试代码时,删掉了几百行重复的设置代码,那感觉...太爽了!// 离线模式测试
struct OfflineModeTrait: TestTrait, SuiteTrait, TestScoping {
func provideScope(for test: Test, testCase: Test.Case?, performing function: @Sendable () async throws -> Void) async throws {
try await Network.$isOnline.withValue(false) {
try await function()
}
}
}
// 错误响应测试
struct ErrorResponseTrait: TestTrait, SuiteTrait, TestScoping {
let statusCode: Int
func provideScope(for test: Test, testCase: Test.Case?, performing function: @Sendable () async throws -> Void) async throws {
try await NetworkService.$current.withValue(NetworkService.withErrorStatus(statusCode)) {
try await function()
}
}
}
// 组合使用,太灵活了
@Test(.mockNetwork, .offlineMode) func testOfflineBehavior() async throws {
// 测试离线状态下的行为...
}
// 还可以带参数
@Test(.errorResponse(statusCode: 404)) func testNotFoundHandling() async throws {
// 测试404错误处理...
}
这简直是测试各种边界情况的利器!我用这个方法模拟了网络超时、服务器错误、数据损坏等各种异常场景,测试覆盖率一下子提高了不少。4.让测试代码专注于验证逻辑而非环境设置