Swift 101中的单元测试

什么是单元测试? (What is unit testing?)

In computer programming, unit testing is a software testing method by which individual units of source code, sets of one or more computer program modules together with associated control data, usage procedures, and operating procedures, are tested to determine whether they are fit for use (Wikipedia).

计算机编程中 单元测试 是一种 软件测试 方法,通过该方法可以 测试 源代码的 各个单元, 一个或多个计算机程序模块的集合以及相关的控制数据,使用过程和操作过程,以确定它们是否适合使用 ( 维基百科 )

In short, this tells us that unit testing is:

简而言之,这告诉我们单元测试是:

  1. Validating the individual unit of our source code (e.g. all the public or internal functions of a class)

    验证源代码的各个单元( 例如,类的 所有公共或内部函数 )

  2. Checking for the correct behavior of our units when used with other modules (e.g. are inputs on a view properly persisted in storage module?)

    与其他模块一起使用时,检查本机的行为是否正确( 例如,视图中的输入是否正确保存在存储模块中 ?)

Image for post

入门 (Getting started)

The most challenging part of writing unit tests is getting started. Most of the time, we start developing features using some architectural pattern. But when writing test cases, we need to use a suitable architecture or modify the app to work with Test Driven Development(TDD).

编写单元测试中最具挑战性的部分是入门。 大多数时候,我们开始使用某些架构模式来开发功能。 但是在编写测​​试用例时,我们需要使用合适的体系结构或修改应用程序以与测试驱动开发(TDD)一起使用。

Testing the app makes our code more isolated and decoupled (which turns into a benefit when changes are required later on).

测试该应用程序可使我们的代码更隔离和分离(这在以后需要更改时会有所帮助)。

There are many architectural patterns we can use to start our unit testing project. What we choose depends upon the requirements, future scope, scalability and maintainability. One of the testable architectures we use is MVVM (Model — View — ViewModel).

我们可以使用许多架构模式来启动我们的单元测试项目。 我们选择什么取决于需求,未来范围,可伸缩性和可维护性。 我们使用的可测试体系结构之一是MVVM(模型-视图-视图模型)。

Image for post
MVVM Architecture
MVVM架构

Model: Also known as entities, which consist of the domain data

模型 :也称为实体,由领域数据组成

View: User Interface, which displays the data and interaction with the end-user (e.g. button tap, swipe)

视图 :用户界面,显示数据和与最终用户的交互(例如,点击按钮,滑动 )

ViewModel: The business logic and mediator between View and Model. This is UIKit independent, and both manages the presentation state for Views and updates the Models. In general, it is responsible for altering the Model by reacting to the user’s actions performed on the View, while updating the View with changes from the Model.

ViewModel :视图和模型之间的业务逻辑和中介。 这是独立于UIKit的,并且既管理View的表示状态,又更新模型。 通常,它负责通过对用户在视图上执行的操作做出React来更改模型,同时使用来自模型的更改来更新视图。

Check out more architectural pattern options here.

在此处查看更多的建筑模式选项。

处理依赖关系 (Dealing with dependencies)

Unit tests involve testing of isolated code. This isolation can be achieved by removing the dependencies in the module.

单元测试涉及隔离代码的测试。 这种隔离可以通过删除模块中的依赖项来实现。

Usually, we need an API Service to fetch data from the server and perform operations or display it to the View. This ‘APIService’ is like a dependency on our ViewModels. We generally need it in the VMs, as that’s where most of the business logic is handled.

通常,我们需要一个API服务来从服务器获取数据并执行操作或将其显示在View中。 此“ APIService”就像我们ViewModel的依赖项。 我们通常在VM中需要它,因为这是处理大多数业务逻辑的地方。

Talking about isolation, rather abstraction which is used in Object-Oriented Programming can be obtained using the Protocols in Swift. As an example, the property apiService in the ViewModel will always capture the instance that conforms to APIServiceProtocol. This not only gives us separation of the Network Layer but also helps us mock these dependencies, which is helpful for testing.

谈论隔离,可以使用Swift中的Protocols来获得面向对象编程中使用的抽象。 例如,ViewModel中的apiService属性将始终捕获符合APIServiceProtocol的实例。 这不仅使我们分离了网络层,而且还帮助我们模拟了这些依赖关系,这对测试很有帮助。

Types of dependencies:

依赖项类型:

  1. Constructor Injection: we always pass the dependency while initializing the module

    构造函数注入:我们总是在初始化模块时传递依赖项
  2. Property Injection: this happens when the class is already initialized and we inject the dependent module via a property

    属性注入:当类已经初始化并且我们通过属性注入依赖模块时,会发生这种情况
let loginViewController = UIStoryboard.instantiate(.login)
loginViewController.viewModel = LoginViewModel()

3. Functional Injection: a separate function is used to inject the dependency to the module

3.功能注入:使用单独的函数将依赖项注入模块

let loginViewController = UIStoryboard.instantiate(.login)
loginViewController.setup(viewModel: LoginViewModel())

测试双打 (Test doubles)

Image for post
Test doubles and its types
测试双打及其类型

A test double is any component we eventually use to satisfy object dependencies.

测试倍数是我们最终用来满足对象依赖性的任何组件。

A few examples of test doubles are Dummy, Stub, Spy, Mock and Fake.

测试双打的一些示例包括Dummy,Stub,Spy,Mock和Fake。

Dummy: This is a dependency object which is used just to shut the compiler warning. Think of it as an object that does nothing, but is needed to satisfy the condition.

虚拟:这是一个依赖对象,仅用于关闭编译器警告。 将其视为不执行任何操作但需要满足条件的对象。

/// A dependent object for loading images in your app.
protocol ImageLoader {
func downloadImage(url: String)
}// Real Implementation
class ImageDownloader: ImageLoader {
func downloadImage(url: String) {
AF.downloadImage(url)
}
}// Dummy implementation which will do nothing.
class DummyImageLoader: ImageLoader {
func downloadImage(url: String) {
/// Nothing to do here
}
}

As a general rule, a dummy should do nothing and return as close to nothing as possible.

通常,假人应该什么也不做,并且尽可能返回几乎什么都不做。

Stub: This provides a predefined output for the indirect inputs our SUT will need for tests. Stubs are frequently used in a lot of test cases.

存根:这为SUT测试所需的间接输入提供了预定义的输出。 存根经常在许多测试案例中使用。

We know we need a Stub when we need to control the indirect inputs of our SUT, and if we don’t want to have any verification logic on it.

我们知道,当我们需要控制SUT的间接输入时,以及我们不想在其上使用任何验证逻辑时,都需要一个Stub。

// The account manager dependency protocol.
protocol AccountManager {
var isUserAuthenticated: Bool { get } func login(email: String, password: String,
completionHandler: @escaping (Bool, String?) -> Void)
}class AccountManagerStub: AccountManager { // We implement the dependency property.
var isUserAuthenticated = true func login(email: String, password: String,
completionHandler: @escaping (Bool, String?) -> Void) {
completionHandler(true, "Success")
}
}

Spy: This works exactly like a Stub, but also records the information from the function that was invoked.

间谍程序:它的工作原理与Stub完全相同,但它还会记录所调用函数的信息。

protocol WishlistService {
func add(productID: String)
}class CartController { let wishlistService: WishlistService

init(wishlistService: WishlistService) {
self.wishlistService = wishlistService
}

// Order later product method will call a add method which
// returns nothing and, hence it will be dificult to test that add was called.
func orderLater(product: String) {
wishlistService.add(productID: product)
}
}class WishlistServiceSpy: WishlistService {

// In addition to the interface implementation.
// The spy has some extra properties that
// will remember how the dependency was called.
var wishlistedProductCount: Int = 0

func add(productID: String) {
wishlistedProductCount += 1
}
}

Here, add() method returns nothing and has been called from the CartController’s instance method orderLater().

在这里,add()方法不返回任何内容,并且已从CartController的实例方法orderLater()中调用。

How could we determine that the add() method is actually being executed while testing? This can be resolved by making a WishlistServiceSpy while actually testing the CartController. Doing so will spy a number of times the add method was called, and give us a test double for testing the CartController.

我们如何确定在测试过程中add()方法实际上正在执行? 这可以通过在实际测试CartController时制作一个WishlistServiceSpy来解决。 这样做将监视多次调用add方法的过程,并为我们提供一个测试CartController的测试倍数。

Mock: This is similar to a Spy in terms of remembering information, but with an added advantage: it knows the exact behavior that should take place. While a Spy just gets the indirect values in the test, a Mock verifies whether the output is as expected.

模拟:就记忆信息而言,这类似于间谍,但有一个额外的优势:它知道应该发生的确切行为。 间谍只是在测试中获取间接值,而模拟程序将验证输出是否符合预期。

protocol WishlistService {
func add(productID: String)
}class CartController { let wishlistService: WishlistService

init(wishlistService: WishlistService) {
self.wishlistService = wishlistService
}

// Order later product method will call a add method which
// returns nothing and, hence it will be dificult to test that add was called.
func orderLater(product: String) {
wishlistService.add(productID: product)
}
}class WishlistServiceMock: WishlistService {

// In addition to the interface implementation.
// The spy has some extra properties that
// will remember how the dependency was called.
var wishlistedProductCount: Int = 0

// This will verify the output for the test.
func verifyAddWasCalled() -> Bool {
wishlistedProductCount > 0
} func add(productID: String) {
wishlistedProductCount += 1
}
}

Ideally, we should use Mocks over Spies in most cases. We only use a Spy when using a Mock is not possible or overly complicated.

理想情况下,在大多数情况下,我们应该使用对间谍的模仿。 我们仅在无法使用模拟或过于复杂的情况下才使用间谍。

Fake: This is usually used when SUT is dependent on some other complex framework or components. It will be a simplified version of that framework or any component.

伪造:通常在SUT依赖于其他复杂框架或组件时使用。 这将是该框架或任何组件的简化版本。

When some of the dependencies cannot be replaced with any static values or other test doubles, a fake should be used. As such, fakes will always comprise with some logic. They can also grow depending upon the external components’ logic and test cases.

当某些依赖项无法用任何静态值或其他测试值替代时,应使用伪造品。 因此,假货将始终具有一定的逻辑性。 它们也可以根据外部组件的逻辑和测试用例而增长。

class FakePersistenceClass: PersistenceClass {    // A dictionary we will use to store during unit tests
private var fakeStorage: [String: Any] = [:]

override func save(user: String) {
fakeStorage[defaultName] = user
}

// Override the real get method to fake user retrieval behavior
override func getUser() -> String? {
return fakeStorage[defaultName]
}
}

Generally, most of the database or persistence layer abstraction can be used as a fake database while testing, as we don’t actually require SUT to store our data to a real database.

通常,大多数数据库或持久层抽象都可以在测试时用作伪数据库,因为我们实际上不需要SUT将数据存储到真实数据库中。

In the above example, the FakePersistenceClass overrides the actual PersistenceClass to fake save and getUser methods by using a dictionary to store and retrieve the values.

在上面的示例中,FakePersistenceClass通过使用字典存储和检索值来覆盖实际的PersistenceClass以伪造save和getUser方法。

最后……让我们开始编写测试用例! (Finally… Let's start writing our test cases!)

We usually start by testing the business logic of the application, which is the crux of any feature. While not every developer does this, test cases should be written first and then implemented in a strict TDD environment.

我们通常从测试应用程序的业务逻辑开始,这是任何功能的关键。 尽管不是每个开发人员都这样做,但是应该首先编写测试用例,然后在严格的TDD环境中实施测试用例。

In this example, we will use Apple’s default test suite XCTest for writing our test cases. There are also other frameworks, like Quick and Nimble.

在此示例中,我们将使用Apple的默认测试套件XCTest 用于编写我们的测试用例。 还有其他框架,例如Quick和Nimble。

We will be testing the business logic in the ViewModel of the application. We have a sample HomeViewModel class, and we will be writing test cases for it.

我们将在应用程序的ViewModel中测试业务逻辑。 我们有一个示例HomeViewModel类,我们将为此编写测试用例。

Pro Tip: Unit tests are run within a unit testing target. If your app does not yet have one, first add a “Unit Testing Bundle” target using Xcode’s File > New > Target... menu before getting started.

专家提示:单元测试在单元测试目标内运行。 如果您的应用程序还没有,请先使用Xcode的“ File > New > Target... 菜单 添加“单元测试包”目标, 然后再开始使用。

First, we need to add a test class in the Unit Tests target. Select the target folder and add a New File( File > New > File…) and select Unit Test Case Class.

首先,我们需要在“单元测试”目标中添加一个测试类。 选择目标文件夹并添加一个新文件(File> New> File…),然后选择Unit Test Case Class

Image for post
Image for post

Once we hit the Next button, we will have some boilerplate code for the XCTest class that we will have to modify with our requirements. We will be testing the behavior of two things:

按下“下一步”按钮后,我们将为XCTest类提供一些样板代码,必须根据需求对其进行修改。 我们将测试两件事的行为:

  1. Verify that the view model executes the service call and fetches the data.

    验证视图模型是否执行了服务调用并获取了数据。
  2. Confirm that once we get the data from our backend, it tells our View to update itself.

    确认一旦我们从后端获取数据,它就会告诉我们的视图进行更新。

For every test case, there should be a test function starting with the word test which indicates for XCTest that it is a testing function (e.g. func test_viewDidLoad()). However trivial the test case, it is imperative to identify minute mistakes or code flow in our features.

对于每个测试用例,应该有一个以单词test开头的测试函数,该单词为XCTest表示它是一个测试函数(例如func test_viewDidLoad() )。 无论测试用例多么琐碎,都必须在我们的功能中识别微小的错误或代码流。

  1. We always use Subject Under Test (SUT) to determine which component we are testing.

    我们始终使用被测对象(SUT)来确定要测试的组件。
  2. setUp() is used to allocate our SUT. In our case, HomeViewModel and tearDown() is used to clean up all the allocations. These functions always run before every test execution, which helps in distinguishing behavior testing.

    setUp()用于分配我们的SUT。 在我们的例子中,使用HomeViewModel和tearDown()清理所有分配。 这些功能始终在每次测试执行之前运行,这有助于区分行为测试。

  3. In our first test to verify if the VM is fetching the data from the server, we have used MockAPIService which mocks the API layer. This mockApiService has a property, didfetchPopularMovies, which determines if it was fetched or not.

    在验证VM是否正在从服务器获取数据的第一个测试中,我们使用了模拟API层的MockAPIService。 此mockApiService具有属性didfetchPopularMovies ,该属性确定是否获取了它。

  4. In the second test case, we have also used a spy viz homeViewSpy to verify if the View gets updated when the data is fetched from the backend. It also checks if the data source has loaded with data and notifies its View, which can be validated with updateViewCalled in homeViewSpy.

    在第二个测试用例中,我们还使用了间谍工具homeViewSpy来验证从后端获取数据时View是否得到更新。 它还检查数据源是否已加载数据并通知其View,可以使用updateViewCalled中的homeViewSpy进行验证。

Note: We have used two test doubles. One of them is Mock to spoof the Service layer’s implementation as a local one which isn't calling actual web service but behaving like one. Another is a Spy to remember if the View was updated.

注意:我们使用了两次测试双打。 其中之一是Mock,将服务层的实现伪装成本地的,而不是调用实际的Web服务,而是表现得像一个。 另一个是要记住视图是否已更新的间谍。

结论… (In conclusion…)

Using TDD is in itself may seem tedious, but helps produce robust, scalable and maintainable software.

使用TDD本身看起来似乎很乏味,但是可以帮助开发健壮,可伸缩和可维护的软件。

It may be tricky at first to understand the importance of writing trivial test cases. But in the long run, it actually helps with major code changes when test cases start to fail, because we can identify the fixes for code flow beforehand.

首先,要理解编写琐碎的测试用例的重要性可能会有些棘手。 但是从长远来看,当测试用例开始失败时,它实际上有助于重大代码更改,因为我们可以事先确定代码流的修复程序。

Thanks for reading!

谢谢阅读!

Take a peek at the project with all the test cases here.

看看这里的所有测试用例的项目。

翻译自: https://medium.com/make-it-heady/unit-testing-in-swift-101-aabd158a4efa

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值