神刀安全网

Real World Mocking in Swift

Mocks allow us to write fast tests that do not mess with production data. Without OCMock, we will need to write our own mocks, but it does not have to be much work. In this talk from try! Swift , Veronica Ray looks at the techniques for practical mocking in Swift that allow us to create simple, easy-to-maintain mocks for the most important parts of our codebase.

See the discussion on Hacker News .

Transcription below provided by Realm: a replacement for SQLite & Core Data with first-class support for Swift! Check out the Swift docs!

Get new videos & tutorials — we won’t email you for any other reason, ever.

About the Speaker: Veronica Ray

Veronica Ray is a software engineer at LinkedIn on the Video team. Once, she rode her bike between two moose. She blogs on Medium and is on Twitter as @nerdonica

@nerdonica

You Want To Write Tests…

…but you do not want them to mess up your real stuff or be slow. Here is a simple example of an NSURLsession you might have in your codebase. If you want to make a real request, your test will be slow. SequenceTest is not reliable. The more tests you have, the higher the likelihood one or more fail randomly.

let session = NSURLSession() let url = NSURL(string: "http://www.tryswiftconf.com")! let task = session.dataTaskWithURL(url) { (data, _, _) -> Void in     if let data = data {         let string = String(data: data, encoding: NSUTF8StringEncoding)         print(string)     } } task.resume()

But how do you test this without making a real request?

OCMock Cannot Save You Now

Technically, you can use OCMock with Swift, but only with severe restrictions. Functionality is still limited; there is no point in trying to use it. Mocking frameworks are all built on top of reflection, taking advantage of being able to change class types and objects at run time.

Swift is currently read-only; there is no way to modify your program at run time. That doesn’t seem like it will change, which is part of what makes Swift such a safe programming language. Swift does have some mocking frameworks currently, but they are different than what you would find in languages with more access to the language run-time.

As a result, we will need to write our own mocks.

class HTTPClientTests: XCTestCase {     var subject: HTTPClient!     let session = MockURLSession()      override func setUp() {         super.setUp()         subject = HTTPClient(session: session)     }

class HTTPClientTests: XCTestCase {     var subject: HTTPClient!     let session = MockURLSession()      override func setUp() {         super.setUp()         subject = HTTPClient(session: session)     }      func test_GET_RequestsTheURL() {         let url = NSURL(string: "http://www.tryswiftconf.com")!          subject.get(url) { (_, _) -> Void in }

class HTTPClientTests: XCTestCase {     var subject: HTTPClient!     let session = MockURLSession()      override func setUp() {         super.setUp()         subject = HTTPClient(session: session)     }      func test_GET_RequestsTheURL() {         let url = NSURL(string: "http://www.tryswiftconf.com")!          subject.get(url) { (_, _) -> Void in }          XCTAssert(session.lastURL === url)     } }

We first create a mock NSURLSession() and inject it. Then, we call get with a referenced URL. Finally, we assert that the URL the session received was the same one we passed in.

Writing your own mocks will take more time than having OCMock create them for you, but I will show you ways to make this take less time. Is it worth it? For every mock you write, you have to think about whether using the real type is worth the time you will spend writing, integrating, and maintaining a mock. I will show you how to think about this tradeoff, and to only write mocks that are worth the investment.

  • Make tests (thousands of times) faster :bullettrain_side:

Web servers, databases and services over the network run thousands of times slower than computer instructions, slowing down the test. Tests do not get run as often, and are therefore less useful.

  • Increase the coverage of test suite :earth_americas:

Error conditions and exceptions are nearly impossible to test without mocks that can simulate them. Functions that perform dangerous tasks, such as deleting files or deleting database tables, are difficult to safely test without mocks.

  • Make tests more robust :muscle:

Without mocks, tests are sensitive to parts of the system that are not related to what is being tested. Network timings can be thrown off by an unexpected computer load. Databases may contain extra or missing rows. Configuration files may have been modified.

Testing is not as common in iOS development, but there are great reasons to do it, and to do it now. The book “The Effective Engineer” by Edmond Lau had a huge effect on me. It convinced me that adding tests was one of the highest leverage improvements I could make to our codebase. It allows engineers to make changes, especially large refractorings with significantly higher confidence. If I want to rethink how we do networking, try out Cocoa, or improve our utilities for concurrency, I want to have tests in place to not break anything. If you want to improve your application, testing is one of the first places you should look. It decreases bugs and repetitive work of manual testing. When code does break, automated tests help to efficiently identify who is accountable. Tests also offer executable documentation of what cases the original author considered, and how to invoke the code.

The time to write tests is now.

Writing tests is done more easily by the original authors when their code is fresh in their minds, rather than by those who try to modify it months or even years later. Fostering a culture of testing on a team will take time and effort, so it is best to start the process now.

Dependency Injection

Here, we use multiple constructor injection with a default constructor. The default constructor is a convenient way to initialize a type with all real dependencies, without needing to pass in any arguments. This is considered a bad practice by Java programmers (and has the title “BastardInjection”). However, that is mostly because it causes issues when you use a dependency injection container, either rolling your own or through a framework (e.g. Typhoon or Swinject). If you are not doing that, then it is not bad practice.

class VideoCaptureManager: NSObject {     var userDefaults: UserDefaultsProtocol      //MARK: - Initializers     override init() {         self.userDefaults = UserDefaults()     }      convenience init(userDefaults: UserDefaultsProtocol) {         self.init()         self.userDefaults = userDefaults     } }

“Dependency injection means giving an object its instance variables. Really. That’s it.” – James Shore

This is a one-line singleton for a VideoUploadManager . It is used in the TimeMachineAPI to upload a video.

class VideoUploadManager {     static let sharedInstance = VideoUploadManager() }  class TimeMachineAPI {     func uploadVideo(videoRecording: VideoRecording) {         VideoUploadManager.sharedInstance.upload(videoRecording: self.videoRecording, completion: { (error) -> Void in             if let error = error {                 Log.error(error.localizedDescription)             }         })     } }

Why use dependency injection?

  • Easy customization :art:

When creating an object, it is easy to customize parts of it for specific scenarios, instead of using the same singleton everywhere.

  • Clear ownership :closed_lock_with_key:

Particularly when using constructor injection, the object ownership rules are strictly enforced, helping to avoid circular dependencies.

  • Testability :heart_eyes:

No hidden dependencies need to be managed. It becomes easy to mock up the dependencies to focus our task on the objects being tested.

Constructor injection

There are several main forms of dependency injection, but constructor injection is generally preferred because it makes dependencies explicit. I will be using constructor injection in all my examples.

Using dependency injection, we can pass and test doubles inside of real objects. OCMock made it easy to create stubs, mocks, and partial mocks. In looking at many resources on testing, I found that the definitions used for these terms are consistent within iOS and Java. This is important because Java is where we find some of the most authoritative resources on the topic, and where mocks were originally developed.

Types of Test Doubles:

  • Stubs
  • Mocks
  • Partial mocks

“Fakes a response to method calls of an object” – Unit Testing Tutorial: Mocking Objects

Some of the most common stubs are for APIs. They give you fake responses instead of hitting the real API. This is also useful when you are relying on an API that has not been built yet, as this API below. The API call to the TimeMachineAPI would make the time machine go to the provided year, take a short video of what it sees, and upload it to YouTube when it gets back to the present. The stub API call will return a hardcoded video URL, instead of calling the API.

class StubTimeMachineAPI: TimeMachineAPI {     var videoUrl = "https://www.youtube.com/watch?v=SQ8aRKG9660"      func getVideoFor(year: Int) -> String {         return videoUrl     } }

“Let you check if a method call is performed or if a property is set” – Unit Testing Tutorial: Mocking Objects.

These are more complex. This TimeMachine allows you to travel to any year you want. A MockTimeMachine has a variable timeTravelWasCalled . When we call time travel, TimeTravelWasCalled is set to true . This allows us to test that the function we want to get called actually gets called.

class MockTimeMachine: TimeMachine {     var timeTravelWasCalled = false      mutating func timeTravelTo(year: Int) {         timeTravelWasCalled = true     } }

“Any actual object which has been wrapped or changed, to provide artificial responses to some methods, but not others” – Justin Searls

Partial mocks are an anti-pattern. I would not encourage you to use them, nor will I go into detail on how you can create and use them in your Swift apps.

I do not take the word anti-pattern lightly. First, they are challenging to set up. They require instantiating a real object, altering or wrapping it, then providing it. Second, they decrease the comprehensibility of the test. :no_mouth:

Their use often raises questions, e.g. “ What is the value of this test? What is real? What is fake? Can I trust that a passing test means it is working under real conditions? ”. None of these comments from a teammate would make me feel the test I wrote were very effective. Whenever this happens, I rethink my test and create new ones that are effective.

We are going to get into some nitty-gritty details about how to create these test doubles. I use the term mocking to describe any test double, not just mocks.

Mocking in Swift Via Protocols

A best practice emerges: mocking in swift via protocols. Not everyone considers this mocking. In his WWDC 2015 session Protocol-Oriented Programming in Swift , Dave Abrahams said, This testing is similar to what you get with mocks, but better ”. He went on, “ Mocks are inherently fragile. You have to couple your testing code […] to the implementation details of the code under test ”.

From that session, we saw that protocols have many advantages over subclassing. These advantages will carry over into the mocks you write, but until a better name appears, I will still use “mock” and “sub” to describe these test doubles, and almost all resources you read on the topic will also follow this approach.

Here are more advantages of mocking with protocols instead of subclassing. First, it plays well with structs and classes . Now you can have one consistent approach to creating mocks throughout your codebase.

Protocols help when there is internal dependencies . An NSURLSession could interact with classes, most likely private ones. Maybe to ensure our test works the way we want, we have to sub one or two dependencies. Since these are internal to the class, what happens when iOS 9 is released, and these two classes are renamed? Or two new dependencies are needed to be stubbed? When a superclass has stored properties, you must accept them, but this is not the case when your type conforms to a protocol. More best practices can be found, often coming from Java.

“Don’t Mock Types You Don’t Own”

We will see how they work in iOS with Swift. They said “ do not mock types you do not own ”. This includes the people working at ThoughtWorks in London, who developed mock objects in 1999.

It is bad to mock types you do not own for two main reasons:

  • You have to be sure that the behavior you implement in a mock matches the external library. This depends on how well you know the external library and whether it is an open or closed source.

  • The external library could change, breaking your mock. Based on past experience, you could guess how stable or volatile a library is, but that guess would be less accurate than one you could make about your own code. But we are iOS developers, and we work with many Apple framework classes.

Mocking Apple Framework Classes

class UserDefaultsMock: UserDefaultsProtocol {      private var objectData = [String : AnyObject]()      func objectForKey(key: UserDefaultsKey) -> AnyObject? {         return objectData[key.name]     }           func setObject(value: AnyObject?, forKey key: UserDefaultsKey) {         objectData[key.name] = value     }      func removeObjectForKey(key: UserDefaultsKey) {         objectData[key.name] = nil     } }

Sometimes we should mock them. We should generally do this when setup is difficult; if not, mocking might interfere with other tests.

The two main classes often given as examples are NSUserDefaults and NSNotificationCenter . This is a user default’s mock. We will see later that in practice, mocking NSNotificationCenter did not turn out to be as useful. I have heard the advice that when you replace Apple’s classes with mocks, it is very important to only test the interaction with that class, not the behavior, as implementation details could change at any point. NSUserDefaults has not changed in 15 years, and it is safe to say it will not change much in the future. This is a risk I am willing to take in order to not accidentally mess up the real data in someone’s NSUserDefaults .

Say you want an app for people who want to keep up with the latest developments in time machines. We will call it the Time Traveler Times. Here, we were testing that if the user default show that the user wants to receive breaking news or daily digest push notifications, the data provider’s notification settings will reflect that.

func testNotificationSettingsLoad() {     let userDefaultsMock = UserDefaultsMock()     mockUserDefaults.setObject("NEVER", forKey: "timeMachineBreakingNewsPushNotification") mockUserDefaults.setObject("NEVER", forKey: "timeMachineDailyDigestPushNotification")      let dataProvider = PushNotificationsDataProvider(userDefaults: mockUserDefaults)      let expectedFrequencies: [PushNotificationFrequency] = [.Never, .All, .All, .Never]      XCTAssertEqual(expectedFrequencies, dataProvider.notificationSettings.values) }

Mocking NSNotificationCenter

From my project, mocking NSNotificationCenter was not worth it:

  • The mock was very complex. It took several days for me to write.
  • As it was used widely throughout the codebase, injecting it took work.
  • Difficult to make the mock accurately reflect the real object. I resorted to ugly hacks that made the compiler happy, but limited the utility of the mock.
  • NSNotificationCenter , mocking it avoids an issue sending real notifications during tests that I have not found a compelling reason to be concerned about. If it ends up messing our test, I will look into it. But a mock NSNotificationCenter is not how I would solve that problem.

Do Not Mock Value Types

This idea has already been embraced by the Swift community. When you are doing simple data in and out, you do not need mocking or stubbing. You can pass a value into a function then look at the resulting value. One way to write fewer mocks is to use more value types. That removes the work and future maintenance costs of creating a mock.

If you have classes lying around your codebase, it can be hard to decide where to start. I was worried at first I would have to make huge changes to our codebase in order to add more value types. Fortunately, you do not have to rewrite your entire codebase.

Immutable Reference Types

You do not need to be an expert on value vs. reference semantics. Joe Groff works on the Swift programming language, and he gave me some great advice for how to approach incrementally refracting your reference types to be value types: “Try making your reference types immutable”.

An immutable class and a struct behave almost the same, and that gives you a common transition point. If you can successfully make the class immutable, then turning it into a struct afterward should be easy. I started looking at all the classes in our codebase and asking, “Why is this a class?” Sometimes it will be because we needed the reference semantics. Other times our class inherited from NSObject to follow an implementation we saw elsewhere, but it did not really need to. You will learn more about the decisions that went into the code your teammates wrote, and hopefully find small ways to improve your codebase.

What Makes a Good Mock

  • Quick and easy to write

It takes time to decide which types to mock, and what your approach should be, but if you are spending three straight days creating a mock, you should think about whether it is a worthwhile investment of your time.

  • Relatively short and does not contain much information you don’t need

If you are mocking a type you do not own, it should not include any methods in the original type that you are not using in your codebase. Also, if you are setting the properties of a mock to useful default values, set the required ones, not the optional ones.

  • Legitimate reason to not use real object

Weigh the costs of writing and maintaining this mock versus the benefits of the fast, thorough and reliable test it should enable. I am thoughtful about what gets mocked and why.

Swift Mocking Frameworks

Let’s look towards the future. What is the future of mocking frameworks in Swift? Three main frameworks have emerged that take a similar approach:

  • Dobby, MockFive and SwiftMock

They take the approach that you will still need to write your own mocks, but they provide helpers for setting expectations and stubbing out return values.

  • Cuckoo

This is different from the others, since it uses a compiled time generator to generate supporting structs and classes for you. It gets us closer to what we had in OCMock. It will create a mock for you from a protocol or a class, but not a struct. You can then stub out its method, make calls, and verify behavior.

You might be thinking, “ are not we going towards this new world described by Gary Bernhardt’s “Boundaries” talk, and Andy Matuschak’s “Controlling Complexity in Swift” talk? ” The idea that your codebase will be made up of this functional core and imperative shell, you will not need mocks for the functional core because we are working with value types, and the imperative shell is thin, you can just use integration tests for it. From what I have explored in Swift, this type of architecture is mostly in toy apps and in its early stages. This is why I was excited by Ayaka’s talk yesterday. This is the first time I have heard about this architecture being used in production iOS apps.

For almost all of us, we are not there yet. Unless you went to do a rewrite of your app’s architecture, you will need mocks to write good tests.

If you want to improve your codebase, write tests. Mocks allow you to write great tests. I want you to create simple mocks that will last. Write mocks, and use them for great good.

:muscle:

See the discussion on Hacker News .

Get new videos & tutorials — we won’t email you for any other reason, ever.

Realm: a replacement for SQLite & Core Data with first-class support for Swift! Check out the Swift docs!

转载本站任何文章请注明:转载至神刀安全网,谢谢神刀安全网 » Real World Mocking in Swift

分享到:更多 ()

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址
分享按钮