Swift中网络单元测试的完整指南

面对现实吧,编写测试在iOS开发中并不那么流行(至少与为后端编写测试相比)。 我曾经是一个单独的开发人员,但最初并未受过本地“测试驱动”开发人员的培训。 因此,我花了很多时间研究如何编写测试以及如何编写可测试的代码。 这就是为什么我要写这篇文章。 我想分享我在Swift中进行测试时发现的东西。 希望我的见解可以节省您在丛林中奔波的时间。

在本文中,我们将讨论测试101的开始: 依赖注入

想象一下,您正在编写测试。
如果您的测试目标(SUT,被测系统)以某种方式与现实世界相关,例如网络和CoreData,则编写测试代码会更加复杂。 基本上,我们不希望我们的测试代码依赖于现实世界中的事物。 SUT不应依赖于其他复杂系统,因此我们能够更快,时间不变和环境不变地对其进行测试。 此外,重要的是我们的测试代码不要“污染”生产环境。 “污染”是什么意思? 这意味着我们的测试代码将一些测试内容写入数据库,将一些测试数据提交至生产服务器,等等。这就是存在依赖项注入的原因。

让我们从一个例子开始。
给定一个应该在生产环境中通过Internet执行的类。 Internet部分称为该类的依赖项。 如上所述,当我们运行测试时,该类的Internet部分必须能够用模拟或伪造环境代替。 换句话说,该类的依赖关系必须是“可注入的”。 依赖注入使我们的系统更加灵活。 我们可以在生产代码中“注入”真实的网络环境。 同时,我们还可以“注入”模拟网络环境来运行测试代码,而无需访问互联网。

TL; DR

在本文中,我们将讨论:

  1. 如何使用依赖注入技术设计对象
  2. 如何在Swift中使用Protocol设计模拟对象
  3. 如何测试对象使用的数据以及如何测试对象的行为

依赖注入(DI)

开始吧! 现在,我们将实现一个名为HttpClient的类。 HttpClient应该满足以下要求:

  1. HttpClient应该使用与分配的URL相同的URL提交请求。
  2. HttpClient应该提交请求。

因此,这是我们的HttpClient的第一个实现:

似乎HttpClient可以提交“ GET”请求,并通过闭包“ callback”传递返回的值。

HttpClient的用法

问题是:我们如何测试它? 我们如何确保代码满足上面列出的要求? 直观地,我们可以执行代码,为HttpClient分配一个URL,然后在控制台中观察结果。 但是,这样做意味着我们每次实现HttpClient时都必须连接到Internet。 如果测试URL在生产服务器上,那似乎更糟:您的测试运行在一定程度上确实影响了性能,并且您的测试数据已提交到真实世界。 如前所述,我们必须使HttpClient“可测试”。

让我们看一下URLSession。 URLSession是HttpClient的一种“环境”,它是Internet的网关。 还记得我们所说的“可测试”代码吗? 我们必须使Internet组件可更换。 因此,我们编辑HttpClient:

我们更换

然后我们添加一个新变量: session ,添加一个相应的init 。 从现在开始,当我们创建HttpClient时,我们必须分配session 。 也就是说,我们必须将会话“注入”到我们创建的任何HttpClient对象。 现在,我们可以在注入了“ URLSession.shared”的情况下运行生产代码,并在注入了模拟会话的情况下运行测试代码。 答对了!

HttpClient的用法变为:HttpClient(session:SomeURLSession()).get(url:url){(成功,响应)//返回数据}

向此HttpClient编写测试代码变得非常容易。 因此,我们设置了测试环境:

这是经典的XCTestCase设置。 变量httpClient是被测系统(SUT),变量session是我们要注入到httpClient的环境。 由于我们在测试环境中运行代码,因此我们将MockURLSession对象分配给session 。 然后,将模拟会话注入到httpClient。 它使httpClient运行在MockURLSession而不是URLSession.shared之上。

测试数据

现在,我们专注于我们的第一个要求:

  1. HttpClient应该使用与分配的URL相同的URL提交请求。

我们要确保请求的URL与开始时分配给“ get”方法的URL完全相同。

这是我们的测试用例草案:

该测试用例可以表示为:

  • 前提 :给定URL“ https:// mockurl”
  • 时间 :提交http GET请求
  • 声明 :提交的网址应等于“ https:// mockurl”

我们仍然需要编写断言部分。

那么,我们如何知道HttpClient的“ get”方法确实提交了正确的url? 让我们看一下依赖项:URLSession。 通常,“ get”方法使用给定的url创建一个请求,并将该请求分配给URLSession以提交请求:

现在,在测试环境中,请求已分配给MockURLSession。 因此,我们可以破解我们拥有的MockURLSession,以检查请求是否正确创建。

这是MockURLSession的草案:

MockURLSession的行为类似于URLSession。 URLSession和MockURLSession都具有相同的方法,dataTask()和相同的回调关闭类型。 尽管URLSession中的dataTask()比MockURLSession做更多的工作,但是它们的界面看起来很相似。 由于界面相同,我们可以用MockURLSession代替URLSession,而无需更改太多“ get”方法的代码。 然后,我们创建一个变量lastURL ,以跟踪在“ get”方法中提交的最终URL。 简而言之,在测试时,我们创建一个HttpClient,将MockURLSession注入其中,然后查看前后的网址是否相同。

测试用例草案为:

我们使用URL声明lastURL ,以查看“ get”方法是否正确创建了具有正确URL的请求。

在上面的代码中,还有一件事要实现: return // dataTask 。 在URLSession中,返回值必须是URLSessionDataTask。 但是,无法以编程方式创建URLSessionDataTask,因此,这是一个需要模拟的对象:

与URLSessionDataTask一样,此模拟具有相同的方法resume()。 因此,它可能能够将此模拟作为dataTask()的返回值。

然后,如果您与我一起编写代码,则会在代码中发现一些编译错误:

MockURLSession的接口与URLSession的接口不同。 因此,当我们尝试注入MockURLSession时,编译器将无法识别它。 我们必须使模拟对象的接口与真实对象相同。 因此,让我们介绍一下“协议”!

HttpClient的依赖项是:

我们希望会话为URLSession或MockURLSession。 因此,我们将类型从URLSession更改为协议URLSessionProtocol:

现在,我们可以注入URLSession或MockURLSession或符合此协议的任何对象。

这是该协议的实现:

在我们的测试代码中,我们仅需要一种方法: dataTask(NSURLRequest, DataTaskResult) ,因此我们在协议中仅定义了一种必需的方法。 当我们要模拟我们不拥有的东西时,通常会采用这种技术。

还记得MockURLDataTask吗? 那是我们不拥有的另一件事,所以是的,我们将创建另一个协议。

我们还必须使真实对象符合协议。

URLSessionDataTask具有完全相同的协议方法resume(),因此URLSessionDataTask没有任何反应。

问题是,URLSession没有返回URLSessionDataTaskProtocol的dataTask()。 因此,我们需要扩展一种方法以符合协议。

这是将返回类型从URLSessionDataTask转换为URLSessionDataTaskProtocol的简单方法。 它根本不会改变dataTask()的行为。

现在,我们可以完成MockURLSession中缺少的部分:

我们知道// dataTask…可能是MockURLSessionDataTask:

这是一个模拟,其行为类似于我们测试环境中的URLSession,并且可以保存url以进行断言。 摩天大楼已创建! 所有代码均已编译,测试已通过!

让我们继续前进。

测试行为

第二个要求是:

The HttpClient should submit the request

我们要确保HttpClient中的“ get”方法确实按预期提交了请求。

与先前测试中测试数据正确性的测试不同,该测试断言一个方法是否被调用。 换句话说,我们想知道是否调用了URLSessionDataTask.resume()。 让我们玩个老把戏:
我们创建一个新变量resumeWasCalled,以记录是否调用了简历。

因此,我们只需编写一个测试:

变量dataTask是一个模拟,由我们自己拥有,因此我们可以添加一个属性来测试resume()的行为:

如果resume()被调用,则resumeWasCalled将变为“ true”! 🙂很简单,对吧?

概括

在本文中,我们了解到:

  1. 如何适应DI以改变生产/测试环境。
  2. 如何利用协议创建模拟。
  3. 如何测试传递值的正确性。
  4. 如何断言某个功能的行为。

开始时,您必须花费大量时间编写简单的测试。 而且,测试代码也是代码,因此您仍然需要使其清晰且结构合理。 但是,编写测试的好处是无价的。 只能通过适当的测试来扩展代码,而测试可以帮助您避免琐碎的错误。 所以,让我们开始吧!

示例代码在GitHub上。 这是一个游乐场,我在那里进行了额外的测试。 随意下载/分叉,欢迎任何反馈!

感谢您阅读我的文章article。

参考

  1. 您不拥有的模拟课程
  2. 依赖注入
  3. 使用Swift进行测试驱动的iOS开发