使用Xcode XCTest的TDD iOS网络API调用
测试驱动开发(TDD)是软件开发人员可以在软件开发中使用的方法之一。 在TDD中,开发人员计划要创建的软件功能,然后在编写功能实现之前,为软件的每个功能编写测试用例。
在开始时,测试用例显然会失败,因为代码尚未实现。 此阶段通常称为红色阶段。 然后,开发人员编写代码以确保测试用例成功通过,并且不会破坏任何组件或当前的测试用例,因此不必完全优化和高效地完成此阶段中的代码。 此阶段称为绿色阶段。 此后,开发人员应通过清理,维护代码库和优化代码效率来重构代码的实现。 然后,应在添加新的测试用例时重复此循环。 每个测试单元应做得尽可能小且隔离,以使其易于阅读和维护。
在本文中,我们将使用带有Xcode XCTest Framework的TDD构建一个简单的网络API单元测试。 Network API将对服务器的网络调用封装为以JSON格式获取电影列表,然后将其编码为Movie Swift类的数组。 网络测试需要在不发出实际网络请求的情况下快速执行,为此,我们将创建模拟对象和存根来模拟服务器调用和响应。
- APIRepository类:此类封装了我们对服务器的网络请求调用,以获取电影列表
- APIRepositoryTests类:XCTest子类,我们将使用该子类为APIRepository类编写测试用例
- MockURLSession类:URLSession子类,充当模拟对象以测试传递的URL以及使用存根数据,URLResponse和Error对象创建的MockURLSessionDataTask
- MockURLSessionDataTask类:URLSessionDataTask子类,充当模拟对象,用于存储网络调用中的存根数据,URLResponse,Error,completionHandler对象,它覆盖恢复调用并调用传递存根对象的completionHandler存根。
测试用例1 —从API获取电影按预期设置URL主机和路径
我们将创建的第一个测试用例是测试get films方法是否在正确的期望范围内设置了URL Host和Path。 首先在“测试”模块内创建APIRepositoryTests单元测试类。 不要忘记添加“ @testable import project-name”以将项目模块包含在测试模块中。 要在Xcode中运行测试,可以使用快捷键Command + U。
在函数中,我们设置实例化APIRepository类对象,此方法中创建了一个名为testGetMoviesWithExpectedURLHostAndPath()的方法,因为我们尚未输入APIRepository类,所以我们键入了有关未解析标识符APIRepository的编译器投诉。
导入 XCTest
@testable 导入 APITest 类 APIRepositoryTests:XCTestCase { func testGetMoviesWithExpectedURLHostAndPath(){
让 apiRespository = APIRepository()
}
}
要编译测试,请创建一个包含APIRepository类的名为APIRepository.swift的新文件。
导入基础类 APIRepository {}
接下来,在testGetMoviesWithExpectedURLHostAndPath内部,我们将调用APIRepository方法,以从通过完成处理程序的网络中获取电影。
func testGetMoviesWithExpectedURLHostAndPath(){
让 apiRespository = APIRepository()
apiRepository.getMovies {电影,错误}
}
编译器会抱怨APIRepository没有成员getMovies,在APIRepository类内部将getMovies函数和Create Movie类添加到新File中以进行测试编译。
导入基础班级电影:可编码{
让标题:字符串
详细说明:字符串
} 类 APIRepository {
func getMovies(completion: @escaping ([Movie] ?, Error?)-> Void){
}
}
为了测试URL主机和路径名,我们需要一种方法,当URLSession调用dataTask(带有:,completionHandler :)并传递包含我们测试中Server API的主机名和路径的URL时,缓存URL的方法。 为此,我们将创建一个MockURLSession类,该类将URLSession类作为子类,并添加包含URL的存储属性。 然后,我们重写dataTask(with:,completementHandler :)方法,并将url分配给instance属性。
MockURLSession 类 :URLSession {
var cachedUrl:URL? 覆盖 func dataTask(使用url:URL,completionHandler: @转义 (数据?,URLResponse ?、错误?)->无效)-> URLSessionDataTask {
自我 .cachedUrl =网址
返回 URLSessionDataTask()
}
}
在我们的测试案例中,我们将MockURLSession分配给APIRepository会话属性,然后在调用getMovies之后,使用XCTAssertEqual检查APIRepository会话的URL主机和期望的路径名。
func testGetMoviesWithExpectedURLHostAndPath(){
让 apiRespository = APIRepository()
让 mockURLSession = MockURLSession()
apiRespository.session = mockURLSession
apiRespository.getMovies(){电影,错误}
XCTAssertEqual(mockURLSession.cachedUrl?.host,“ mymovieslist.com”)
XCTAssertEqual(mockURLSession.cachedUrl?.path,“ / topmovies”)
}
编译器将失败,使测试编译仅在APIRepository类内添加session属性。
类 APIRepository {
var会话:URLSession!
...
}
运行测试,由于XCTAssertEqual,测试将失败。 我们需要实现将正确的URL传递给sessionDataTask的getMovies方法,以通过测试。 之后,测试将最终成功通过。
func getMovies(完成: @转义 ([电影] ?,错误?)->无效)
警卫 让 url = URL(字符串:“ https://mymovieslist.com/topmovies”)
否则 {fatalError()}
session.dataTask(with:url){( _ , _ , _ ) in }
}
测试案例2-从API获取电影成功返回剧集列表
下一个测试是测试获取电影的API何时成功响应,应该调用完成处理程序并传递电影列表。 为了测试Xcode中的异步代码,我们可以使用Expectation和waitForExpectation函数在我们传递的指定超时内实现。 满足期望后,将调用完成处理程序,我们可以使用该处理程序从异步代码中声明实现的结果。 在APIRepositoryTestClass和以下代码中创建testGetMoviesSuccessReturnsMovies函数:
func testGetMoviesSuccessReturnsMovies(){
让 apiRespository = APIRepository()
让 mockURLSession = MockURLSession()
apiRespository.session = mockURLSession
让 moviesExpectation =期望(描述:“电影”)
var filmsResponse:[Movie]?
apiRespository.getMovies {(电影,错误) 在
电影响应=电影
filmsExpectation.fulfill()
}
waitForExpectations(超时:1){(错误) 在
XCTAssertNotNil(moviesResponse)
}
}
该测试将失败,因为我们尚未实现getMovies完成处理程序以将JSON从响应序列化为Movie类。 但是,如何在没有实际的服务器返回数据的情况下进行测试呢? 我们可以使用MockDataTask并为数据传递存根。 MockDataTask是一个URLSessionDataTask子类,可以使用存根数据,URLResponse和Error对象进行初始化,然后将其缓存在实例属性中。 它还将completionHandler存储为实例属性,因此可以在调用override resume方法时调用它。
类 MockTask:URLSessionDataTask {
私人 出租数据:数据?
私人 让 urlResponse:URLResponse吗?
私人 让错:错误?
var completeHandler:(((Data ?, URLResponse ?, Error?)->无效)
初始化 (数据:数据?,urlResponse:URLResponse ?,错误:错误?){
自我 .data =数据
自我 .urlResponse = urlResponse
自我。错误=错误
}
覆盖 func resume(){
DispatchQueue.main.async {
self .completionHandler?( self .data, self .urlResponse, self .error)
}
}
}
我们将MockTask作为MockURLSession类的实例属性,并且我们还将创建接受Data,URLResponse和Error对象的初始化程序,然后使用这些参数实例化模拟任务对象。 在dataTask覆盖方法内,我们还将完成处理程序分配给mockTask完成处理程序属性,以便在调用履历表时将其调用。
MockURLSession 类 :URLSession {
...
私人 让 mockTask:MockTask
初始化 (数据:数据?,urlResponse:URLResponse ?,错误:错误?){
mockTask = MockTask(数据:数据,urlResponse:urlResponse,错误:
错误)
覆盖func dataTask(使用url:URL,completionHandler: @转义 (数据?,URLResponse ?、错误?)->无效)-> URLSessionDataTask {
自我 .cachedUrl =网址
mockTask.completionHandler =完成处理器
返回 dataTask
}
}
现在在测试中,我们创建存根json数据,然后初始化传递存根数据的MockSession。 运行测试,该测试失败,因为我们尚未实现getMovies完成处理程序来处理响应并将JSON数据序列化为Movie对象。
func testGetMoviesSuccessReturnsMovies(){
let jsonData =“ [{\” title \“:\”不可能发生的任务\“,\” detail \“:\”汤姆·克鲁斯电影“”]“。data(使用:.utf8)
让 apiRespository = APIRepository()
let mockURLSession = MockURLSession(data:jsonData,urlResponse: nil ,错误: nil )
apiRespository.session = mockURLSession
让 moviesExpectation =期望(描述:“电影”)
var filmsResponse:[Movie]?
apiRespository.getMovies {(电影,错误) 在
电影响应=电影
filmsExpectation.fulfill()
}
waitForExpectations(超时:1){(错误) 在
XCTAssertNotNil(moviesResponse)
}
}
为了使测试成功通过,请在APIRepository类的获取电影中实现completionHandler:
func getMovies(completion: @escaping ([Movie] ?, Error?)-> Void){
警卫 让 url = URL(字符串:“ https://mymovieslist.com/topmovies”)
else {fatalError()} session.dataTask(with:url){(数据,响应,错误) 在
守护 让数据=数据其他 { 返回 }
让电影= 尝试 ! JSONDecoder()。decode([Movie]。self,来自:数据)
完成(电影, 零 )
}。恢复()}
测试用例3 —从具有URL响应错误的API获取影片返回ErrorResponse
第三个测试用例是测试dataTask完成处理程序是否有错误,并返回ResponseError。 在APIRepositoryTests类中创建testGetMoviesWhenResponseErrorReturnsError方法:
func testGetMoviesWhenResponseErrorReturnsError(){
让 apiRespository = APIRepository()
let error = NSError(domain:“ error”,代码:1234,userInfo: nil )
let mockURLSession = MockURLSession(数据: nil ,urlResponse: nil ,错误:error)
apiRespository.session = mockURLSession
让 errorExpectation =期望(描述:“错误”)
var errorResponse:错误?
apiRespository.getMovies {(电影,错误) 在
errorResponse =错误
errorExpectation.fulfill()
}
waitForExpectations(超时:1){(错误) 在
XCTAssertNotNil(errorResponse)
}
}
测试失败,因为我们尚未在完成处理程序中处理实现或处理响应错误。 添加实现以使用guard检查错误是否为nil,并将错误传递给完成处理程序,如果存在则返回错误。 运行测试以确保它成功通过。
func getMovies(completion: @escaping ([Movie] ?, Error?)-> Void){
警卫 让 url = URL(字符串:“ https://mymovieslist.com/topmovies”)
else {fatalError()} session.dataTask(with:url){(数据,响应,错误) 在
警卫错误== 无 其他 {
完成( 无 ,错误)
返回
} 保护 let data = data else { return }
让电影= 尝试 ! JSONDecoder()。decode([Movie]。self,来自:数据)
完成(电影, 零 )
}。恢复()}
测试案例4-使用空数据从API获取电影会返回错误
该测试将检查响应何时返回空数据,然后将通过Error对象调用完成处理程序。 在APIRepositoryTests类中创建testGetMoviesWhenEmptyDataReturnsError函数。
func testGetMoviesWhenEmptyDataReturnsError(){
让 apiRespository = APIRepository()
let mockURLSession = MockURLSession(data: nil ,urlResponse: nil ,错误: nil )
apiRespository.session = mockURLSession
让 errorExpectation =期望(描述:“错误”)
var errorResponse:错误?
apiRespository.getMovies {(电影,错误) 在
errorResponse =错误
errorExpectation.fulfill()
}
waitForExpectations(超时:1){(错误) 在
XCTAssertNotNil(errorResponse)
}
}
测试失败,我们需要使用警卫来检查数据,如果数据不存在,请使用Error对象调用完成处理程序。 运行测试以确保它通过。
func getMovies(completion: @escaping ([Movie] ?, Error?)-> Void){
警卫 让 url = URL(字符串:“ https://mymovieslist.com/topmovies”)
else {fatalError()} session.dataTask(with:url){(数据,响应,错误) 在
警卫错误== 无 其他 {
完成( 无 ,错误)
返回
} 保护数据=数据其他 {
完成( nil ,NSError(domain:“ no data”,代码:10,userInfo: nil ))
返回
让电影= 尝试 ! JSONDecoder()。decode([Movie]。self,来自:数据)
完成(电影, 零 )
}。恢复()}
测试案例5 —使用无效的JSON从API获取电影会返回错误
获取电影API的最终测试是处理将无效JSON数据传递到对象序列化时返回Error的情况。 在APIRepositoryTests类中创建一个testGetMoviesInvalidJSONReturnsError。
func testGetMoviesInvalidJSONReturnsError(){
让 jsonData =“ [{\” t \“}]”。data(使用:.utf8)
让 apiRespository = APIRepository()
let mockURLSession = MockURLSession(data:jsonData,urlResponse: nil ,错误: nil )
apiRespository.session = mockURLSession
让 errorExpectation =期望(描述:“错误”)
var errorResponse:错误?
apiRespository.getMovies {(电影,错误) 在
errorResponse =错误
errorExpectation.fulfill()
}
waitForExpectations(超时:1){(错误) 在
XCTAssertNotNil(errorResponse)
}
}
运行测试,由于使用try,测试将崩溃! 处理JSONDecoder解码功能,以将JSON序列化为Movie Class。 我们需要使用do try catch块来重构代码,并在JSON解析中发生错误时返回错误。
func getMovies(completion: @escaping ([Movie] ?, Error?)-> Void){
警卫 让 url = URL(字符串:“ https://mymovieslist.com/topmovies”)
else {fatalError()} session.dataTask(with:url){(数据,响应,错误) 在
警卫错误== 无 其他 {
完成( 无 ,错误)
返回
} 保护数据=数据其他 {
完成( nil ,NSError(domain:“ no data”,代码:10,userInfo: nil ))
返回
} 做 {
让电影= 尝试 JSONDecoder()。decode([Movie]。self,来自:数据)
完成(电影, 零 )
} {
完成( 无 ,错误)
}
}。恢复()}
运行测试以确保我们所有的测试通过都没有回归!
从长远来看,软件开发中的TDD会导致一个更健壮和可维护的软件,因为作为开发人员,我们始终可以在开发过程中再次运行测试,并通过检测代码的回归来添加新功能。 只要我们对整个模块都有良好的测试覆盖范围,TDD在我们的应用程序中产生的错误也应该更少。