mock 单元测试 mvc

    科技2023-12-04  101

    mock 单元测试 mvc

    Well, the good old Model-View-Controller pattern. So popular, so widespread and yet SO misunderstood. First introduced in Smalltalk-79, is definitely a go to choice for many developers and companies as a “standard” to follow — including Apple. But, have you ever read the definition of this pattern? Do you really know what every component in it stands for or you just have inferred it from context? Let’s jump right into it. In this article, we’re going to apply it to later, in the next one, effectively test it!

    好吧,好的旧的Model-View-Controller模式。 如此受欢迎,如此广泛而又被误解了。 在Smalltalk-79中首次引入,绝对是许多开发人员和公司(包括Apple)遵循的“标准”选择。 但是,您是否曾经阅读过这种模式的定义? 您是否真的知道其中的每个组件代表什么,或者只是从上下文中推断出它? 让我们直接跳进去。 在本文中,我们将在以后的文章中应用它,对它进行有效的测试!

    From Wikipedia, the three components are defined as:

    在Wikipedia中,这三个组件定义为:

    Model — The central component of the pattern. It is the application’s dynamic data structure, independent of the user interface. It directly manages the data, logic and rules of the application.

    模型-模式 的核心组成部分。 它是应用程序的动态数据结构,独立于用户界面。 它直接管理应用程序的数据,逻辑和规则。

    View — Any representation of information such as a chart, diagram or table. Multiple views of the same information are possible, such as a bar chart for management and a tabular view for accountants.

    视图- 信息的任何表示形式,例如图表,图表或表格。 可以使用同一信息的多种视图,例如用于管理的条形图和用于会计的表格视图。

    Controller — Accepts input and converts it to commands for the model or view.

    控制器- 接受输入并将其转换为模型或视图的命令。

    source: https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller

    来源: https : //zh.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller

    From here, I would like to point out a big misunderstanding that happens in the iOS community as a whole. Read the model definition once again. It is the central component of the pattern. It is not a class that merely holds data. It should in fact, contain the business logic that is pertinent to its data. It is not just a single class, also. It can be composed by several service classes that apply the business logic to the application’s data.

    从这里,我想指出一个很大的误会 整个iOS社区都会发生这种情况。 再次阅读模型定义。 它是模式的中心组成部分。 这不是仅保存数据的类。 实际上,它应该包含与其数据相关的业务逻辑。 它不仅是一个单一的类。 它可以由几个将业务逻辑应用于应用程序数据的服务类组成。

    Also, let’s set down this once and for all: The controller is not the star of the show. And as mentioned above, the model class (or set of classes) is not a data container. Welp, I sincerely don’t know when everything lost track and we all end up with a MassiveViewController and POJO models approach of the MVC pattern. But, yeah, here I’ll do my best effort to dismantle this notion for you, reader.

    此外,让我们一劳永逸地设置此设置:控制器不是演出的明星。 并且如上所述,模型类(或类集)不是数据容器。 抱歉,我真的不知道什么时候一切都消失了,我们最终都采用了MVC模式的MassiveViewController和POJO模型方法。 但是,是的,读者,我将尽最大努力为您消除这个概念。

    Now that you effectively know the true definition of the MVC pattern, let’s try to apply it (and later on, test it!) in Swift using the knowledge previously brought to the table in the previous article. Hence, we’ll be relying upon abstractions and using protocols for such. And due to the fact that we’re using protocols, let’s follow a Protocol Oriented Programming (POP) approach.

    现在您已经有效地了解了MVC模式的真正定义,让我们尝试使用上一篇文章中先前介绍的知识在Swift中应用它(并稍后对其进行测试!)。 因此,我们将依靠抽象并为此使用协议。 由于我们正在使用协议,因此让我们遵循面向协议的编程(POP)方法。

    So, let’s get our hands dirty! Here’s the deal: we’ll try to model a simple application that represents a gumball machine — and not any machine, it will be the Gumball3000!

    所以,让我们动手吧! 这是交易:我们将尝试为代表口香糖机的简单应用程序建模-而不是任何机器,它将是Gumball3000!

    note: The full project developed for this article is available on this github link: https://github.com/JPedroAmorim/GumballMachine3000

    注意: 可以在以下github链接上找到为本文开发的完整项目: https : //github.com/JPedroAmorim/GumballMachine3000

    credits: https://br.freepik.com/ for the icons 积分: https : //br.freepik.com/用于图标

    note: In this tutorial, I’ll not be covering the basics of Swift, for instance: how to set up constraints in the Interface Builder, how to start a project in Xcode and so forth.

    注意: 在本教程中,我不会介绍Swift的基础知识,例如:如何在Interface Builder中设置约束,如何在Xcode中启动项目等等。

    Since we’re following a POP approach, let’s start defining our protocols. We have three of them:

    由于我们遵循的是POP方法,因此让我们开始定义协议。 我们有三个:

    protocol GumballModelProtocol { func gumballWithdraw(_ quantity: Int) -> Int? } protocol GumballViewProtocol { func presentError(_ errorMessage: String) func presentSucess(_ withdrawnQuantity: Int) func setViewController(_ gumballViewController: GumballViewControllerProtocol) } protocol GumballViewControllerProtocol { func moneyWasSubmitted(_ amount: String) func setView(_ view: GumballViewProtocol) func setModel(_ model: GumballModelProtocol) }

    So, let’s talk a bit about what’s going on

    所以,让我们谈谈正在发生的事情

    GumballModelProtocol stands for the abstraction of our model. In our model, we’ll encapsulate the core logic of our application. In our case, the model will be responsible for dealing with the withdraw of gumballs and all the logic that revolves around it. The protocol is solely defined by the gumballWithdraw method, where an optional Int is the return, because we’re dealing with an operation that may fail (e.g. image trying to withdraw 10 gumballs when you have only 5 left in stock).

    GumballModelProtocol代表模型的抽象。 在我们的模型中,我们将封装应用程序的核心逻辑。 在我们的案例中,该模型将负责处理口香糖的撤回以及围绕它的所有逻辑。 该协议仅由gumballWithdraw方法定义,其中可选的Int是返回值,因为我们正在处理可能失败的操作(例如,当您只剩5个库存时,图像试图提取10个gumballs)。

    GumballViewProtocol will be, guess what, the abstraction of our cute view (which you already had a glimpse of it). The only information that is available about it for the exterior world is that the view should present a successful operation or a failing operation through the presentSuccess and presentError methods (we’ll talk about the setViewController method in no time, hang in there). Notice that the concrete implementation of how the message is displayed doesn’t come up — for classes interacting with the view, it shouldn’t matter on how the view displays this message (if it is through a label, through an image, through an animation…) the only thing that really matters is the fact that a message was effectively displayed.

    猜猜GumballViewProtocol将是我们可爱视图的抽象(您已经瞥见了它)。 关于外部世界的唯一可用信息是,该视图应通过presentSuccess和presentError方法呈现成功的操作或失败的操作(我们将立即讨论setViewController方法,请牢牢抓住那里)。 请注意,并未显示如何显示消息的具体实现-对于与视图进行交互的类,与视图如何显示此消息无关紧要(如果通过标签,通过图像,通过动画……)真正重要的是有效地显示一条消息。

    GumballViewControllerProtocol will finally, be the abstraction of our controller. Perceive that its main purpose is to be a “man in the middle” between the view and the model — it takes input from the view and transforms it to a format that the model can handle. For instance, let’s take a look on the moneyWasSubmitted method — through it, our view will inform the controller about the user’s input (as a String), the controller will handle this input and transform it to an Int, and then, it will use it as an parameter for the model’s gumballWithdraw method.

    GumballViewControllerProtocol最终将成为我们控制器的抽象。 意识到它的主要目的是成为视图和模型之间的“中间人”,它从视图中获取输入并将其转换为模型可以处理的格式。 例如,让我们看一下moneyWasSubmitted方法-通过它,我们的视图将通知控制器用户的输入(作为String),控制器将处理此输入并将其转换为Int,然后它将使用它作为模型的gumballWithdraw方法的参数。

    I know that this amount of information right now might be overwhelming, but bare with me, it’ll become very clear once you see the code behind it!

    我知道现在的信息量可能不胜枚举,但是对我而言,一旦看到背后的代码,它就会变得非常清晰!

    note: Even though the model is rather simple in this approach, it definitely could be a set of classes, as mentioned earlier. In order to not keep this tutorial long, I’m just presenting a model with only one class. Though, in my github project, I will also present an alternative version (and more complex in design patterns terms) where the model protocol act as a facade for a whole set of classes.

    注意: 尽管此方法中的模型非常简单,但它肯定可以是一组类,如前所述。 为了不使本教程冗长,我只介绍一个只有一个类的模型。 但是,在我的github项目中,我还将展示一个替代版本(在设计模式方面更为复杂),其中模型协议充当整个类集的基础。

    But, before jumping into the code itself, let’s talk about the setViewController method in the GumballViewProtocol and the setModel and setView methods in the GumballViewControllerProtocol — let’s talk about dependency injection.

    但是,在进入代码本身之前,让我们谈谈GumballViewProtocol中的setViewController方法以及GumballViewControllerProtocol中的setModel和setView方法。 关于依赖注入。

    note: If you’re familiar with this concept, I know this “set methods” sounds too Java-ish. But nonetheless, even though it’s a Swift-based tutorial, I wanted to maintain core OOP patterns with a rather agnostic approach to the technological environment you are (be it Swift, Java, Python, whatever). But a more idiomatic Swift approach would definitely declare this dependencies as variables in the protocol.

    注意: 如果您熟悉此概念,我知道这种“设置方法”听起来像Java一样。 但是尽管如此,尽管它是基于Swift的教程,但我还是想通过一种不可知的方法来维护您所处的技术环境(无论是Swift,Java,Python等)来维护核心OOP模式。 但是,更惯用的Swift方法肯定会将此依赖项声明为协议中的变量。

    Well, the term “dependency injection” make scare a few through as an eerie, complex terminology used in software architecture. But, to be quite sincere with you, as James Shore once said, it is a 25-dollar term for a 5-cent concept. So, what does dependency injection stands for?

    好吧,“依赖注入”一词在软件体系结构中使用了一种令人毛骨悚然的复杂术语。 但是,正如詹姆斯·肖尔 ( James Shore)曾经说过的那样,要对您真诚一点,它是5美分概念的25美元术语。 那么,依赖注入代表什么呢?

    Dependency injection is giving an object its instance variables (usually classes — we call them dependencies). That’s it.

    依赖注入是给对象一个实例变量(通常是类,我们称它们为依赖)。 而已。

    What do I mean by that for our context? Simple.

    对于我们的上下文,这意味着什么? 简单。

    Instead of this:

    代替这个:

    credits: Me, a majestic drawer 学分:我,雄伟的抽屉

    We’ll go with this:

    我们将这样做:

    credits: Me, a majestic drawer 学分:我,雄伟的抽屉

    Through dependency injection and protocols, we are transforming that big continuous lego block (that usually is your ViewController class) into small, discrete, interchangeable lego blocks.

    通过依赖注入和协议,我们正在将大的连续的lego块(通常是ViewController类)转换为小的,离散的,可互换的lego块。

    Nice, now that we’ve defined our protocols, we’re good to start developing our first concrete implementation. Let’s start with our Model.

    好的,既然我们已经定义了协议,那么很高兴开始开发我们的第一个具体实现。 让我们从模型开始。

    import Foundation class GumballModel: GumballModelProtocol { // MARK: - Variables (1) private var gumballs = 100 // MARK: - GumballModelProtocol methods (2) func gumballWithdraw(_ quantity: Int) -> Int? { guard quantity > 0 else { return nil } let gumballsLeft = gumballs - quantity guard gumballsLeft >= 0 else { return nil } gumballs = gumballsLeft return quantity } }

    Let’s see whats going on:

    让我们看看发生了什么:

    Here, we have our main variable. Notice that its access modifier is set to private, because the only class that should be able to alter its value should be the GumballModel. Notice that its existence is not disclosed in the GumballModelProtocol — because it’s an implementation detail and it shouldn’t matter for the outside world.

    在这里,我们有我们的主要变量。 请注意,其访问修饰符设置为private,因为唯一能够更改其值的类应该是GumballModel。 注意,它的存在没有在GumballModelProtocol中公开-因为它是实现细节,对于外部世界来说无关紧要。 Here, we have the concrete implementation of our gumballWithdraw method. Its logic is pretty straight forward: it should fail (produce a nil output) if the given quantity is either negative or if the quantity that is about to be withdrawn is larger than the amount of gumballs that are in stock. If it succeeds, this method should alter the value of the gumballs variable and return the withdrawn quantity.

    在这里,我们有了gumballWithdraw方法的具体实现。 它的逻辑很简单:如果给定数量为负或要提取的数量大于库存的口香糖数量,它将失败(产生nil输出)。 如果成功,则此方法应更改gumballs变量的值并返回提取的数量。

    note: if you are implementing the classes in this article by yourself, don’t forget to add them to the tests module by ticking its box on the “Target Membership” field in the class identity inspector tab.

    注意: 如果您要自己实现本文中的类,请不要忘记在类标识检查器选项卡的“目标成员身份”字段中的方框中打勾,将其添加到测试模块中。

    Now, let’s head in for the implementation of our view. The key aspect of the view’s implementation is that we’ll be doing it through a .xib file instead of throwing outlets in a storyboard. Also, the owner of this .xib file will be a class that subclasses UIView instead of UIViewController.

    现在,让我们开始实现我们的观点。 该视图实现的关键方面是,我们将通过.xib文件进行此操作,而不是在情节提要中添加插座。 同样,此.xib文件的所有者将是继承UIView而不是UIViewController的类。

    Here’s our .xib and its outlets. Also, notice that we’ve set the File’s owner attribute to the GumballView class, whose implementation can be seen down below:

    这是我们的.xib及其分店。 另外,请注意,我们已经将File的owner属性设置为GumballView类,其实现如下所示:

    import UIKit class GumballView: UIView, GumballViewProtocol { // MARK: - IBOutlets (1) @IBOutlet var inputTextField: UITextField! @IBOutlet var depositButton: UIButton! @IBOutlet var resultLabel: UILabel! @IBOutlet var resultImage: UIImageView! // MARK: - Dependencies (2) private(set) var gumballViewController: GumballViewControllerProtocol? // MARK: - Init methods (3) required init?(coder: NSCoder) { super.init(coder: coder) } override init(frame: CGRect) { super.init(frame: frame) initFromNib() } private func initFromNib() { if let nib = Bundle.main.loadNibNamed("GumballView", owner: self, options: nil), let nibView = nib.first as? UIView { nibView.frame = bounds nibView.autoresizingMask = [.flexibleWidth, .flexibleHeight] addSubview(nibView) } } // MARK: - Dependency Injection methods (4) func setViewController(_ gumballViewController: GumballViewControllerProtocol) { self.gumballViewController = gumballViewController } // MARK: - GumballViewProtocol methods (5) func presentError(_ errorMessage: String) { resultLabel.text = "U-oh! An error occurred: \(errorMessage)" resultImage.image = UIImage(named: "cross") } func presentSucess(_ withdrawnQuantity: Int) { resultLabel.text = "Yay! Here are your \(withdrawnQuantity) gumballs!" resultImage.image = UIImage(named: "tick") } // MARK: - Request method (6) @IBAction func depositButtonPressed(_ sender: Any) { guard let moneyAmount = inputTextField.text else { presentError("Please enter a non-empty amount") return } gumballViewController?.moneyWasSubmitted(moneyAmount) } }

    There’s a LOT to talk about here, so here we go:

    这里有很多要讨论的,所以我们开始:

    First, we have the views IBOutlets. Notice that outlets are an implementation detail! Hence, the controller should not have knowledge about them. The ONLY thing that the controller knows is that the view can present a successful operation or an error (the two methods disclosed in the view’s protocol).

    首先,我们有IBOutlets视图。 注意,插座是实现细节! 因此,控制器不应对此有所了解。 控制器唯一知道的是视图可以呈现成功的操作或错误(视图协议中公开的两种方法)。 Here, we have the view’s only dependency: the view controller. It has a private(set) access modifier, and it basically means that this variable can be seen from an outer scope (like from another class) but only the GumballView is able to actually set it.

    在这里,我们只有视图依赖:视图控制器。 它具有一个private(set)访问修饰符,它的基本含义是可以从外部范围(例如从另一个类)中看到此变量,但只有GumballView可以实际设置它。 Here, we have some initialization methods. First, we have the required init?(coder: NSCoder) method, which is mandatory to implement. Another init method that we’ll implement (and the reason for it will be clear once we reach the code for the view controller) is override init(frame: CGRect). In it, we call a setup method that properly initializes the view from the .xib file.

    在这里,我们有一些初始化方法。 首先,我们有必需的init?(编码器:NSCoder)方法,该方法必须强制实施。 我们将要实现的另一种init方法(一旦到达视图控制器的代码,原因便很清楚)是重写init(frame:CGRect)。 在其中,我们调用了一个setup方法,该方法可以正确地初始化.xib文件中的视图。 Here, we have the dependency injection of this class dependency — the view controller.

    在这里,我们有了此类依赖项的依赖项注入-视图控制器。 Also, here we have the implementation for the methods stated in the view’s protocol. Notice that the implementations details for a successful operation or an error presentation (if it is through an image, through an animation, through a label…) is encapsulated within the class and should not be accessible from an outer scope.

    同样,在这里,我们可以实现视图协议中所述方法的实现。 请注意,成功操作或错误表示的实现细节(如果是通过图像,动画,通过标签……)封装在类中,并且不应从外部范围访问。 In the end, we have a method that forwards the user input to the view controller, whom will do the proper handling of it.

    最后,我们提供了一种将用户输入转发到视图控制器的方法,该方法将对其进行适当的处​​理。

    One cool thing that I’d like to point out: What would happen if we moved from an Interface Builder approach and remodeled our view using Apple’s SwiftUI framework? If this new approach also come to implement the protocol we defined for the view, the rest of the code (view controller, model) would suffer no collateral damage. The only thing they know about the view is that it implements a protocol — nothing else. Using the Interface Builder or SwiftUI is a detail of the implementation, and it should only matter to the view itself.

    我想指出的一件很酷的事情:如果我们退出了Interface Builder方法,并使用Apple的SwiftUI框架重塑了视图,将会发生什么? 如果这种新方法也可以实现我们为视图定义的协议, 则其余代码(视图控制器,模型)将不会遭受附带损害。 他们对视图的唯一了解是它实现了协议,仅此而已。 使用Interface Builder或SwiftUI是实现的细节,它只应与视图本身有关。

    And, finally, let’s hop in for our view controller:

    最后,让我们加入我们的视图控制器:

    import UIKit class GumballViewController: UIViewController, GumballViewControllerProtocol { // MARK: - Dependencies (1) private(set) var gumballView: GumballViewProtocol? private(set) var gumballModel: GumballModelProtocol? // MARK: - Lifecycle methods (2) override func loadView() { super.loadView() setupGumballView() } override func viewDidLoad() { super.viewDidLoad() setupGumballModel() } // MARK: - Setup methods (3) private func setupGumballView() { let gumballView = GumballView() self.view = gumballView setView(gumballView) } private func setupGumballModel() { let gumballModel = GumballModel() setModel(gumballModel) } // MARK: - Dependency Injection methods (4) func setView(_ view: GumballViewProtocol) { self.gumballView = view self.gumballView?.setViewController(self) } func setModel(_ model: GumballModelProtocol) { self.gumballModel = model } // MARK: - GumballViewControllerProtocol methods (5) func moneyWasSubmitted(_ amount: String) { guard let integerAmount = Int(amount) else { gumballView?.presentError("Please enter an integer amount.") return } guard integerAmount > 0 else { gumballView?.presentError("Please enter a positive integer amount.") return } guard integerAmount % 2 == 0 else { gumballView?.presentError("We don't have any change! Please enter an amount divisible by 2.") return } guard let withdrawnQuantity = gumballModel?.gumballWithdraw(integerAmount) else { gumballView?.presentError("Not enough gumballs left in stock.") return } gumballView?.presentSucess(withdrawnQuantity) } }

    Once again, we have a lot to talk about:

    再一次,我们有很多要讨论的内容:

    Here, we define our dependencies, just as we did in our view class. But, this time, we have two of them: both the view and the model.

    在这里,我们定义依赖项,就像在视图类中一样。 但是,这一次,我们有两个:视图和模型。 Here, we have lifecycle related methods — and we call setup methods while on them.

    在这里,我们有与生命周期相关的方法-并在它们上调用设置方法。

    Here, we have a very important concept — this setup methods are used to set a DEFAULT value for both of our dependencies. In our case, the default values for them would be the concrete implementations of our view and model class. Since the view controller acts as a middle man between the view and the model (and have them as dependencies), it is an ideal place to set default values for them. Also, here’s the reason why we implemented override init(frame: CGRect) in the view — so that the view controller can instantiate it as a default value for the its view dependency.

    在这里,我们有一个非常重要的概念-此设置方法用于为我们的两个依赖项设置DEFAULT值。 在我们的情况下,它们的默认值将是我们的视图和模型类的具体实现。 由于视图控制器充当视图和模型之间的中间人(并将它们作为依赖项),因此是为它们设置默认值的理想场所。 同样,这也是我们在视图中实现override init(frame:CGRect)的原因,以便视图控制器可以将其实例化为其视图依赖项的默认值。

    As before, the dependency injection methods. Pay attention to them, they’ll be of great use while testing our view controller.

    和以前一样,使用依赖项注入方法。 请注意它们,它们在测试我们的视图控制器时将非常有用。

    Here, we have the concrete implementation of the moneyWasSubmitted method. As mentioned in the start of this article, this method is responsible for handling the input provided by the view and format it in a way that the model can handle it. It also does some error checking and properly alert the view if something goes wrong.

    在这里,我们有moneyWasSubmitted方法的具体实现。 如本文开头所述,此方法负责处理视图提供的输入,并以模型可以处理的方式对其进行格式化。 它还会进行一些错误检查,并在出现问题时适当地警告视图。

    Keep reading, in the next article we’ll develop our tests for what we’ve implemented so far!

    继续阅读,在下一篇文章中,我们将针对到目前为止已实现的内容开发测试!

    翻译自: https://medium.com/academy-eldoradocps/the-foundations-of-unit-testing-2-3-effectively-applying-the-mvc-pattern-in-swift-3ad6838873a9

    mock 单元测试 mvc

    相关资源:详解Spring MVC如何测试Controller(使用springmvc mock测试)
    Processed: 0.012, SQL: 9