一切都连接了吗

Have you ever had a bug that the solution was just setting a delegate? Or an entire feature that broke because a single reference was not being set correctly? This is an everyday issue of everybody, but we could prevent thesm with tests covering our references. But… how?

您是否遇到过一个错误,即解决方案仅设置了一个委托? 还是由于未正确设置单个参考而导致整个功能中断? 这是每个人的日常问题,但是我们可以通过覆盖我们的参考文献的测试来防止这些问题。 但是……怎么了?

If you work with Swift in larger projects, we are probably talking about apps written with architectures a little more complex than the old MVC. And your architecture could be any fancy group of letters like: MVVM, VIP, VIPER, MVMC, etc.

如果您在大型项目中使用Swift,我们可能正在谈论的是使用比旧MVC稍微复杂一点的架构编写的应用程序。 您的架构可以是任何花哨的字母,例如:MVVM,VIP,VIPER,MVMC等。

More complex architectures are subjected to more complex bugs. Here, on iFood, we use a Clean Swift based architecture, the VIP, and we've had bugs being solved with simply adjusting a missing reference, or injecting a dependency correctly. But these issues can be really hard to find, because looking into your code, there are no warnings and the flow seems to be correct.

更复杂的体系结构会遇到更复杂的错误。 在这里,在iFood上,我们使用基于Clean Swift的架构VIP,并且已经通过简单地调整缺少的引用或正确注入依赖项解决了错误。 但是实际上很难找到这些问题,因为调查您的代码时,没有警告,并且流程似乎是正确的。

Image for post
Diagram of a VIP scene. The dashed lines represent weak references between the components
VIP场景图。 虚线表示组件之间的弱引用

Our View represents the layer of the UIViewController and the displayed UIView, it's the layer that handles the direct user interaction. Then, the Interactor contains all of our business logic, and the Presenter all of the presentation logic.

我们的视图表示UIViewController的层和显示的UIView ,它是处理直接用户交互的层。 然后, Interactor包含我们所有的业务逻辑, Presenter包含所有演示逻辑。

The reference from the Presenter to the ViewController is weak, to prevent a retain-cycle, and the same applies to the reference from the Router to the ViewController. And everything is bonded using protocols — to increase our testability.

PresenterViewController的引用很弱,以防止发生保留周期 ,这同样适用于从RouterViewController的引用 一切都使用协议进行绑定 -以提高我们的可测试性。

This represents a single scene, and every time we want to create a new one we have an object responsible for instancing and linking everything, and we call it: the Creator.

这代表一个场景,每次我们想要创建一个新场景时,我们都有一个对象负责实例化和链接所有对象,我们称其为Creator

一个简单的创造者 (A simple Creator)

The Creator's responsibility is to center the creation of our scene and to guarantee that everything is connected properly. Let's dive into an example:

创作者的责任是使场景的创建居中,并确保所有内容均正确连接。 让我们来看一个例子:

class SceneCreator {


  func createScene() -> UIViewController {
    let presenter = Presenter()
    let router = Router()


    let interactor = Interactor(presenter: presenter)
    let viewController = ViewController(interactor: interactor, router: router)


    router.viewController = viewController
    presenter.viewController = viewController


    return viewController
  }
}

We could totally write a test to check if our Creator returns the expected ViewController type. It would be a valid test, but… I would not ensure that all the components on the scene are attatched correctly.

我们完全可以编写一个测试来检查我们的创建者是否返回了预期的ViewController类型。 这将是一个有效的测试,但是……我不能确保正确安装现场的所有组件。

通过反射查找属性 (Finding properties with refelction)

Essentially, we want to make sure that our ViewController has a reference the Interactor, and this Interactor has the expected type (because since the reference is made using a protocol we could have multiple implementations, SceneAInteractorinstead ofSceneBInteractor), and the Interactor has a reference to the right Presenter… and so on until all of our references are covered.

本质上,我们要确保ViewController具有对Interactor的引用,并且该Interactor具有预期的类型(因为由于引用是使用协议进行的,所以我们可以有多个实现,即SceneAInteractor而不是SceneBInteractor ),并且Interactor具有引用正确的Presenter ...等等,直到涵盖所有引用。

The first issue is that we do not have a reference to the Interactor or the Presenter when we create the scene, we just have a ViewController. And even worse, most of these properties are private, so we can't really access them directly from the ViewController.

第一个问题是创建场景时我们没有对InteractorPresenter的引用,我们只有一个ViewController 。 更糟糕的是,这些属性大多数都是私有的,因此我们不能真正从ViewController直接访问它们

That's where the Mirror comes into play.

那就是镜子发挥作用的地方。

The Mirror, actually, represents a very common feature in programming languages called Reflection, which allows us to inspect and work over the content of an object in runtime. Its a struct that reflects an object and gives information about its composition: properties, tuples, enums, etc. Here's and example:

Mirror实际上代表了一种称为反射的编程语言中的一个非常常见的功能,它使我们能够在运行时检查和处理对象的内容。 属性,元组,枚举等下面是和例如:其反映的对象,并给出关于其组成信息的结构

struct Point {
    let x: Int, y: Int
}


let p = Point(x: 21, y: 30)
let mirror = Mirror(reflecting: p)


print(mirror) // Mirror for Point
print(mirror.children.first) // Optional((label: Optional("x"), value: 21))

The Mirror struct has a property called children which is a collection of tuples (label: Optional<String>, value: Any) where label is the name of the property and value well… its value!

Mirror结构体具有一个称为children的属性,该属性是一个元组的集合(label: Optional<String>, value: Any) ,其中label是属性的名称,并且很好地value ……其值!

If we take a better look into the code right above, when we access the first item of this collection we can see that the label is a "x", which is the same name we gave to it on the code, and its value isInt(21). We can use these informations to iterate over the children, using both the label and the value to find whathever we are looking for.

如果我们仔细看一下上面的代码,当我们访问该集合的第一项时,我们可以看到标签是"x" ,与我们在代码上为其赋予的名称相同,其值为Int(21) 。 我们可以使用这些信息遍历children ,同时使用标签来查找所需的内容。

What is nice about the Mirror is that it doesn't matter the visibility of the properties: internal, open, public, private… all of them will be reflected on the struct.

Mirror的优点在于,属性的可见性无关紧要:内部,开放,公共,私有……所有这些都会反映在结构上。

Reflection can have various aplications, but here we'll focus on using it for testing references. And its worth it to mention that the children will only contain stored properties — you will not find functions and computed properties in there 😉!

反射可以有多种用途,但在这里我们将重点放在将其用于测试参考上。 值得一提的是, children将仅包含存储的属性-您将不会在其中找到函数和计算的属性😉!

寻找参考 (Finding references)

Using Mirror, we can now inspect private properties and assert their values and/or references in our scene.

使用Mirror ,我们现在可以检查私有属性并在场景中声明它们的值和/或引用。

let viewController = SceneCreator().createScene()
let viewControllerMirror = Mirror(reflecting: viewController)


let interactor = viewControllerMirror.children
    .first { $0.label == "interactor" }?.value as? Interactor

Here, the constant interactor is a new reference the very own Interactor of the instancedviewController, and now we can access all of its properties, call its methods and even create a new Mirror reflecting it.

这里,常数interactor是一个新的参考自己的实例化的交互器 viewController ,现在我们可以访问其所有属性,调用它的方法,甚至创造新的镜子反射光线。

Since every child has a label, which is the name of its property on the code, and a value, that is a reference to the same value of the property, if finding the right value we can test if its reference is the one we expect it to be.

由于每个子代都有一个label (即代码中其属性的名称)和一个value (该值是对该属性相同值的引用),因此,如果找到正确的value我们可以测试其引用是否为我们期望的值就是这样。

Please note that, in order to do so, we had to know the name of the property we wanted to find, and that we need to cast it to the expected type because value is an Any.

请注意,为此,我们必须知道要查找的属性名称 ,并且由于valueAny因此需要将其强制转换为期望的类型。

We could have made the same thing looking for the first child with the value of an expected type, not using the label:

我们可以用相同的方法寻找第一个具有预期类型value的孩子,而不用使用label

let object = mirror.children
    .first { $0.value as? Interactor }

But that would be a problem if the reflected object had more than one property of the same type!

但是,如果反射的对象具有多个相同类型的属性,那将是一个问题!

测试我们的参考 (Testing our references)

Now that we know how to extract our properties, we can start testing if everything is connected as we expect.

现在,我们知道如何提取属性,我们可以开始测试是否按预期连接了所有内容。

For example, in our VIP architecture, the Presenter has a weak reference to the ViewController which is set by the Creator, and we want to make sure this reference is being set. If for some reason, someone deletes this line, there is no build or compiling warning, but our test will break and prevent this issue to go into production code.

例如,在我们的VIP架构中, Presenter创建者设置的ViewController的引用很弱,我们要确保已设置此引用。 如果由于某种原因有人删除了此行,则没有生成或编译警告,但是我们的测试将中断并阻止此问题进入生产代码。

In order to do that, we can reflect the Interactor and from it the Presenter and test the references.

为了做到这一点,我们可以反映交互器,并从中反映出演示者并测试引用。

class SceneCreatorTests: XCTestCase {
    
    private let sut = SceneCreator()
    
    func test_presentersViewShouldBeTheSameAsCreatedScene() {
        let scene = sut.createScene()
        
        guard let interactor = Mirror(reflecting: scene).children.first { $0.label == "interactor" }?.value as? Interactor,
            let presenter = Mirror(reflecting: interactor).children.first { $0.label == "presenter" }?.value as? Presenter,
            let presentersView = Mirror(reflecting: presenter).children.first { $0.label == "viewController" }?.value as? ViewController else {
          
            XCTFail("Unable to find expected properties for testing")
            return
        }
        
        XCTAssertTrue(scene === presentersView)
    }
}

Thats it! We've create a test that checks if our Creator is setting correclty the reference of our Presenter.

而已! 我们创建了一个测试,检查创建者是否设置了正确的Presenter引用。

The operator === compares whether the two operands point to the same object. The guard guarantees that the presentersView is not opcional, and the assert guarantees that the Presenter references the same ViewController returned by the Creator — as our architecture dictates.

运算符===比较两个操作数是否指向同一对象。 卫队保证presentersView不是可选的,而断言则保证Presenter引用创建者返回的相同ViewController-正如我们的体系结构所指示的那样。

We made the reflections inside a guard let because the result of this operation is optional, and making a Mirror of an optional object is actually reflecting an enum with cases .some(Value) and .none. Iterate over these children is not the same as iterating over a non-optional object.

由于此操作的结果是可选的,因此我们在警卫 .some(Value)进行了反射,并且使可选对象的Mirror实际上反映了带有case .some(Value)和的枚举none 。 对这些子项进行迭代与对非可选对象进行迭代不同。

If you want to find and test a property that is not stored by reference, like classes, but by value, like enums or structs, it does not make sense to compare them using ===, because they will not reference the same position in memory, but you can check if they have the expected values.

如果要查找和测试未按引用存储的属性(例如类) ,而不是按值存储的属性(例如枚举结构) ,则使用===进行比较是没有意义的,因为它们将不会引用相同的位置内存,但是您可以检查它们是否具有期望值。

使一切变得更漂亮 (Making everything a little bit prettier)

Mirror gives us a lot of superpowers, but the syntax is kinda ugly when we need to make a series of reflections to reach the property we want to test.

Mirror给我们带来了很多超能力,但是当我们需要进行一系列反射以达到我们要测试的属性时,语法有点丑陋。

And personally, I'm not a fan of the loose raw strings with the names of the properties I'm reflecting. So, a nice solution is to wrap all the reflection process and create constants for the property names:

就我个人而言,我不喜欢那些带有我所反映的属性名称的原始字符串。 因此,一个不错的解决方案是包装所有反射过程并为属性名称创建常量:

extension XCTestCase {
  enum MirrorConstants: String {
     case interactor
     case presenter
     case router
  }


  func mirror<T>(property: MirrorConstants, ofType: T.Type, from object: Any?) -> T? {
      guard let object = object else { return nil }
      let mirror = Mirror(reflecting: object)


      return mirror.children.first {
          $0.label == property.rawValue
      }?.value as? T
  }
}

This way, our Creatr's test gets much more readable:

这样,我们的创作者的测试变得更具可读性:

class SceneCreatorTest: XCTestCase {
  
  private let sut = SceneCreator()
  
  func test_presentersViewShouldBeTheSameAsCreatedScene() {
      // Given
      let scene = sut.createScene()


      // When
      let interactor = mirror(property: .interactor, ofType: Interactor.self, from: scene)
      let presenter = mirror(property: .presenter, ofType: Presenter.self, from: interactor)
      let presentersView = mirror(property: .viewController, ofType: ViewController.self, from: presenter)


      //Then
      XCTAssertNotNil(presentersView) // Check if there is a view on the presenter
      XCTAssertTrue(presentersView === scene) // and if its the same as `scene`
  }
}

And now, you can make pretty tests for all your references, test your complex architectures, and basically, make sure that everything you have instanced and injected are being referenced correctly preventing a bunch of bugs on your project.

现在,您可以对所有引用进行漂亮的测试,测试复杂的体系结构,并且基本上,确保已正确引用了实例化和注入的所有内容,从而避免了项目中的大量错误。

看看镜子-所有人! (Take a look into the Mirror — that's all folks!)

When we started testing our Creators with reflection here on iFood Brazil, we have prevented a lot of bugs — sometimes you are changing something on the code and you remove the reference by accident, or you have a really complex architecture, and those tests will allow you to identify these issues and fix them right away!

当我们开始在iFood Brazil上通过反射测试创作者时,我们已经避免了很多错误-有时您正在更改代码中的某些内容,并且偶然删除了引用,或者您的架构非常复杂,并且这些测试将允许您可以找出这些问题并立即解决!

Now that you have learned a little bit more about reflection on tests, you'll be able to prevent various bugs too :)

既然您已经了解了更多有关测试反思的知识,那么您也可以防止各种错误:)

Nice testing! | ¡ƃuıʇsǝʇ ǝɔıu

不错的测试! | ƃƃǝʇu

If you want, this same article is also available in Portuguese!

如果需要, 也可以使用葡萄牙语查看同一篇文章!

翻译自: https://medium.com/macoclock/is-everything-connected-6f339dd8a0fb

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值