使用 Swift 结果类型设置 XCTest 时出错

Errors setting up XCTest using Swift Result type

我正在学习如何为我的 API 请求编写测试,但在设置测试的完成代码和响应模型时遇到问题。

我尝试使用 UserResponse 的实例 (let userResponse = UserResponse() ),但是它需要一个值作为它的初始值设定项(来自:Decoder),我不知道里面有什么。我得到的错误是:

"Argument type 'Decoder.Protocol' does not conform to expected type 'Decoder'"

此外,我在创建测试的完成处理程序时遇到错误,我正在使用新的 Swift 结果类型 (Result)。我得到的错误是:

"Type of expression is ambiguous without more context"

这是一个@escaping 函数,但我在测试中收到一条错误消息,要求删除@escaping。

有什么问题吗?我已经在下面用注释标记了故障代码。

谢谢!

// APP CODE

class SignupViewModel: ObservableObject {

  func createAccount(user: UserSignup, completion: @escaping( Result<UserResponse, Error>) -> Void) {
    AuthService.createAccount(user: user, completion: completion)
  }  

}

struct UserSignup: Encodable {
    var username: String
    var email: String
    var password: String
}


struct UserResponse: Decodable {
    var user: User
    var token: String
}


struct User: Decodable {
   var username: String
   var email: String
   // etc
   { private enum UserKeys }
   init(from decoder: Decoder) throws { container / decode code }
}

// TEST CODE

class SignupViewModelTests: XCTestCase {
    var sut: SignupViewModel!

    override func setUpWithError() throws {
        sut = SignupViewModel() 
    }

    override func tearDownWithError() throws {
        sut = nil 
    }

    func testCreateAccount_WhenGivenSuccessfulResponse_ReturnsSuccess() {

        let userSignup = UserSignup(username: "johnsmith", email: "john@test.com", password: "abc123abc")

// WHAT GOES IN from:??

        let userResponse = UserResponse(from: Decoder) 
        
// ERROR: "Type of expression is ambiguous without more context"??

        func testCreateAccount_WhenGivenSuccessfulResponse_ReturnsSuccess() {
        //arrange
        let userSignup = UserSignup(username: "johnsmith", email: "john@test.com", password: "abc123abc")
        

        sut.createAccount(user: UserSignup, completion: ( Result <UserResponse, Error> ) -> Void ) {
            
            XCTAssertEqual(UserResponse.user.username, "johnsmith")
        }
    }
      
    }
}

好的,您对代码进行单元测试的方式存在多个问题,我不会详细说明所有问题,但要使它正常工作,您需要的要点是

func testCreateAccount() {
        //given
        let userSignup = UserSignup(username: "johnsmith", email: "john@test.com", password: "abc123abc")
        let signupExpectation = expectation(description: "Sign up should succeed")

        //when
        sut.createAccount(user: userSignup) { (result) in
            //then
            switch result {
            case .success(let response):
                XCTAssertTrue(response.user.email == "john@test.com", "User email should be john@test.com")
                XCTAssertTrue(response.user.username == "johnsmith", "Username should be johnsmith")
                XCTAssertTrue(response.token != "", "Token should not be empty")

            case .failure(let error):
                XCTFail("Authorization should not fail, failed with \(error.localizedDescription)")
            }
        }

        wait(for: [signupExpectation], timeout: 10.0)
    }

您正在尝试测试异步 API 调用,因此无法使用您需要 expectation 的同步 XCAssert 语句对其进行测试。如果您使用像 RxTest 这样的第三方库,有一些方法可以使异步 API 调用同步并使用直接的 XCAssert 语句对其进行测试。考虑到您仍然是使用 Swift.

进行单元测试的新手,我认为它超出了您的范围

您可以在这里阅读所有关于期望的信息:Apple doc

我遵循了 Given 的简单代码结构,如答案中的评论所示,如果您不使用任何类型的第三方库来编写描述性单元测试,这是一种非常简洁的代码安排方式喜欢 Quick and Nimble

这里的 raywnderlich 教程中有一篇关于使用普通旧 XCTest 框架进行单元测试的漂亮文章。

最后,作为结束语,我们不会进行实际的 API 调用来在单元测试中测试我们的代码,我们编写伪造和存根来测试我们的代码。

假设您最终为整个项目编写了单元测试,并且您的 CI/CD 系统开始 运行 为所有 PR 和构建启动整个测试套件,并且您的单元测试最终使实际API 调用,进行实际 API 调用所浪费的时间会迅速增加您的测试套件 运行 时间,使发布过程成为一场噩梦。此外,测试后端 API 并不是您单元测试的目的,因此请避免使用 mocks、fakes 和 stubs 进行实际的 API 调用,阅读相关内容 :)

上面提供的单元测试只是告诉你如何使用期望和异步测试API,绝对不会涵盖所有可能的情况:)

要在测试代码中创建 UserResponse,请调用其合成初始化程序,而不是解码器初始化程序。类似于:

let userResponse = UserResponse(user: User("Chris"), token: "TOKEN")

要在测试代码中创建闭包,您需要为其提供代码。测试中的完成闭包有一项工作,即捕获它们是如何被调用的。类似于:

var capturedResponses: [Result<UserResponse, Error>] = []
sut.createAccount(user: UserSignup, completion: { capturedResponses.append([=11=]) })

这会捕获 createAccount(user:completion:) 发送给完成处理程序的 Result

...也就是说,看起来您的视图模型正在直接调用进行服务调用的静态函数。如果测试运行,它会在某处创建一个实际帐户吗?或者你有一些我们看不到的边界吗?

而不是直接测试 createAccount(user:completion:),您可能想要测试的是:

  • 某项操作(注册)将尝试为给定用户创建帐户——但实际上并没有这样做。
  • 成功后,视图模型会做一件事。
  • 失败时,视图模型会做另一件事。

如果我的假设是正确的,我可以告诉你怎么做。