Pragmatic Ios Testing
Pragmatic Ios Testing
of Contents
Summary
Introduction
1.1
1.2
1.3
1.4
Chapters/En-Uk/Xctest/Types Of Testing
1.5
Chapters/En-Uk/Xctest/Unit Testing
1.6
1.7
Chapters/En-Uk/Xctest/Behavior Testing
1.8
1.9
Chapters/En-Uk/Xctest/Integration Testing
1.10
Chapters/En-Uk/Foundations/Dependency Injection
1.11
1.12
1.13
1.14
1.15
1.16
Chapters/En-Uk/Setup/Getting Setup
1.17
1.18
1.19
1.20
1.21
1.22
1.23
1.24
1.25
1.26
1.27
Chapters/En-Uk/Async/Animations
1.28
1.29
1.31 1.30
1.32
1.33
1.34
1.35
1.36
1.37
1.39 1.38
1.40
Chapters/En-Uk/Wrap Up/Books
1.41
1.42
1.43
Introduction
Who is it for?
Introduction
Anyone interested in applying tests to iOS applications. Which hopefully should be a large
amount of people. I'm trying to aim this book at myself back in 2012, new to the ideas of
testing and seeing a whole world of possibilities, but not knowing exactly where to start or
how to continue once I've made one or two tests.
Swift or Objective-C?
It's easy to get caught up in what's new and shiny, but in reality there's a lot of existing
Objective-C code-bases out there. I will aim to try and cover both Swift and Objective-C. As
we have test-suites in both languages, some concepts work better in one language vs the
other. If you can only read one language, I'm not apologising for that. It's not pragmatic to
only focus on one language, a language is a language, in time you should know as many as
possible.
Who is it for?
Anyone interested in applying tests to iOS applications. Which hopefully should be a large
amount of people. I'm trying to aim this book at myself back in 2012, new to the ideas of
testing and seeing a whole world of possibilities, but not knowing exactly where to start or
how to continue once I've made one or two tests.
Swift or Objective-C?
It's easy to get caught up in what's new and shiny, but in reality there's a lot of existing
Objective-C code-bases out there. I will aim to try and cover both Swift and Objective-C. As
we have test-suites in both languages, some concepts work better in one language vs the
other. If you can only read one language, I'm not apologising for that. It's not pragmatic to
only focus on one language, a language is a language, in time you should know as many as
possible.
hand you have unit tests, which are a much finer brush for covering the edge cases which
snapshots won't or can't cover.
By using multiple testing techniques, you can cover the shapes of your application, but still
offers the chance to have fewer places to change as you evolve your application.
The actual implementation of XCTest works by creating a bundle, which can optionally be
injected into an application ( Apple calls this hosting the tests. ) The bundle contains test
resources like dummy images or JSON, and your test code. There is a version of XCTest
open source'd by Apple.
XCTest provides a series of macros or functions based on OCUnit's for calling an exception
when an expectation isn't met. For example XCTAssertEqual ,
XCTFail and XCTAssertLessThanOrEqual`. You can explore how the XCT* functions work in
the OSS XCTest.
Here's a real example of an XCTest case subclass taken from the Swift Package Manager:
import Utility
import XCTest
class ShellTests: XCTestCase {
func testPopen() {
XCTAssertEqual(try! popen(["echo", "foo"]), "foo\n")
}
func testPopenWithBufferLargerThanThatAllocated() {
let path = Path.join(#file, "../../Get/DependencyGraphTests.swift").normpath
XCTAssertGreaterThan(try! popen(["cat", path]).characters.count, 4096)
}
func testPopenWithBinaryOutput() {
if (try? popen(["cat", "/bin/cat"])) != nil {
XCTFail("popen succeeded but should have failed")
}
}
}
running application, and your tests run with that happening around it. This gives you access
to a graphics context, the application's bundle and other useful bits and pieces.
Un-hosted tests are useful if you're testing something very ephemeral/logical and relying
only on Foundation, but anything related to UIKit/Cocoa subclasses will eventually require
you to host the test bundle in an application. You'll see this come up every now and again
when setting up test-suites.
Further reading:
History of SenTestingKit on objc.io by Daniel Eggert
How XCTest works on objc.io by Daniel Eggert and Arne Schroppe
10
Chapters/En-Uk/Xctest/Types Of Testing
Types of Testing
A test is a way of verifying code, and making your assumptions explicit. Tests exist to show
connections between objects. When you make a change somewhere, tests reveal implicit
connections.
In the Ruby world the tests are basically considered the application's documentation. This is
one of the first port of calls for understanding how systems work after reading the
overview/README. It's quite similar with node. Both languages are interpreted languages
where even variable name typos only show themselves in runtime errors. Without a
compiler, you can catch this stuff only with rigorous testing.
In Cocoa, this is less of a problem as we can rely on the tooling more. Given a compiler,
static type systems, and a well integrated static analyser -- you can know if your code is
going to break someone else's very quickly. However, this is only a lint, the code compiling
doesn't mean that it will still correctly show a view controller when you tap a button. In other
words, the behaviour of your app can still be wrong.
That is what testing is for. I will cover three major topics in this book. There are many, many
other types of testing patterns -- for example, last week I was introduced to Model Testing.
However, these three topics are the dominent patterns that exist within the Cocoa world, and
I have experience with them. Otherwise, I'd just be copy & pasting from Wikipedia.
Unit Testing
Unit testing is the idea of testing a specific unit of code. This can range from a function, to a
single Model to a full UIViewController subclass. The idea being that you are testing and
verifying a unit of application code. Where you draw the boundary is really up to you, but for
pragmatic reasons we'll mostly stick to functions and objects because they easily match our
mental model of a "thing," that is, a Unit.
Integration Testing
Integration testing is a way of testing that your application code integrates well with external
systems. The most common example of integration testing in iOS apps are user interface
runners that emulate users tapping though you application.
Behavioural Testing
11
Chapters/En-Uk/Xctest/Types Of Testing
Behavioural testing is essentially a school of thought. The principles state that the way you
write and describe your tests should reflect the behaviours of your objects. There is a
controlled vocabulary using words like "it", "describe", "spec", "before" and "after" which
mean that most BDD-testing frameworks all read very similar.
This school of thought has also brought about a different style of test-driving the design of
your app: instead of strictly focusing on a single unit and developing your app from the
inside-out, behaviour-driven development sometimes favors working outside-in. This style
could start with a failing integration test that describes a feature of the app and lead you to
discover new Unit Tests on the way to make the feature tes t "green." Of course it's up to
you how you utilize a BDD framework: the difference in typing test cases doesn't force you to
change your habits.
12
Chapters/En-Uk/Xctest/Unit Testing
Unit Testing
So, what is a unit of code? Well, that's subjective. I'd argue that it's anything you can easily,
reliably measure. A unit test can generally be considered something that is set up (arrange),
then you perform some action (act) and verify the end result (assert.) It's like science
experiments, kinda.
Arrange, Act, Assert.
We had some real-world examples from Swift Package Manager in the last chapter that
were too small for doing Arrange, Act, Assert, so let's use another example:
class WalkTests: XCTestCase {
[...]
func testRecursive() {
let root = Path.join(#file, "../../../Sources").normpath
var expected = [
Path.join(root, "Build"),
Path.join(root, "Utility")
]
for x in walk(root) {
if let i = expected.indexOf(x) {
expected.removeAtIndex(i)
}
}
XCTAssertEqual(expected.count, 0)
}
[...]
}
The first section, the author arranges the data-models as they expect them. Then in the
for they act on that data. This is the unit of code being tested. Finally, they assert that the
code has had the expected result. In this case, that the expected array has become empty.
Using the Arrange, Act, Assert methodology, it becomes very easy to structure your test
cases. It makes tests obvious to other practitioners, and you can determine code smells
quicker by seeing large amounts of code in your arrange or assert sections.
13
Chapters/En-Uk/Xctest/Unit Testing
func testParentDirectory() {
XCTAssertEqual("foo/bar/baz".parentDirectory, "foo/bar")
XCTAssertEqual("foo/bar/baz".parentDirectory.parentDirectory, "foo")
XCTAssertEqual("/bar".parentDirectory, "/")
XCTAssertEqual("/".parentDirectory.parentDirectory, "/")
XCTAssertEqual("/bar/../foo/..//".parentDirectory.parentDirectory, "/")
}
It shows a wide array of inputs, and their expected outputs for the same function. Someone
looking over these tests would have a better idea of what parentDirectory does, and it
covers a bunch of use cases. Great.
Note, the Quick documentation really shines on ArrangeActAssert - I would strongly
recommend giving it a quick browse.
Further reading:
Quick/Quick documentation: ArrangeActAssert.md
14
Return Value
it(@"sets up its properties upon initialization", ^{
// Arrange + Act
ARShowNetworkModel *model = [[ARShowNetworkModel alloc] initWithFair:fair show:show]
;
// Assert
expect(model.show).to.equal(show);
});
ARShowNetworkModelTests.m
You can setup your subject of the test, make a change to it, and check the return value of a
function is what you expect. This is what you think of when you start writing tests, and
inevitably Model objects are really easy to cover this way due to their ability to hold data and
convert that to information.
State
it(@"changes selected to deselected", ^{
// Arrange
ARAnimatedTickView *tickView = [[ARAnimatedTickView alloc] initWithSelection:YES];
// Act
[tickView setSelected:NO animated:NO];
/// Assert
expect(tickView).to.haveValidSnapshotNamed(@"deselected");
});
ARAnimatedTickViewTest.m
State tests work by querying the subject. In this case were using snapshots to investigate
that the visual end result is as we expect it to be.
15
These tests can be a little bit more tricky than straight return value tests, as they may require
some kind of mis-direction depending on the public API for an object.
Interaction Tests
An interaction test is more tricky because it usually involves more than just one subject. The
idea is that you want to test how a cluster of objects interact in order
it(@"adds Twitter handle for Twitter", ^{
// Arrange
provider = [[ARMessageItemProvider alloc] initWithMessage:placeHolderMessage path:pa
th];
// Act
providerMock = [OCMockObject partialMockForObject:provider];
[[[providerMock stub] andReturn:UIActivityTypePostToTwitter] activityType];
// Assert
expect([provider item]).to.equal(@"So And So on @Artsy");
});
ARMessageItemProviderTests.m
In this case to test the interaction between the ARMessageItemProvider and the
activityType we need to mock out a section of the code that does not belong to the
Full Details
There is a talk by Jon Reid of qualitycoding.org on this topic that is really the definitive guide
to understanding how you can test a unit of code.
TODO: Get Jon Reid's MCE talk video TODO: Re-watch it and flesh this out a bit more
16
Chapters/En-Uk/Xctest/Behavior Testing
17
Chapters/En-Uk/Xctest/Behavior Testing
18
Chapters/En-Uk/Xctest/Behavior Testing
Note: They are split up in Act, Arrange, Assert, but they do an awful lot of repeating
themselves. As test bases get bigger, maybe it makes sense to start trying to split out some
of the logic in your tests.
So what about if we moved some of the logic inside each test out, into a section before, this
simplifies out tests, and allows each test to have more focus.
class ModuleTests: XCTestCase {
let t1 = Module(name: "t1")
let t2 = Module(name: "t2")
let t3 = Module(name: "t3")
let t4 = Module(name: "t4")
func test1() {
t3.dependsOn(t2)
t2.dependsOn(t1)
XCTAssertEqual(t3.recursiveDeps, [t2, t1])
XCTAssertEqual(t2.recursiveDeps, [t1])
}
func test2() {
t4.dependsOn(t2)
t4.dependsOn(t3)
t4.dependsOn(t1)
t3.dependsOn(t2)
t3.dependsOn(t1)
t2.dependsOn(t1)
XCTAssertEqual(t4.recursiveDeps, [t3, t2, t1])
XCTAssertEqual(t3.recursiveDeps, [t2, t1])
XCTAssertEqual(t2.recursiveDeps, [t1])
}
func test3() {
t4.dependsOn(t1)
t4.dependsOn(t2)
t4.dependsOn(t3)
t3.dependsOn(t2)
t3.dependsOn(t1)
t2.dependsOn(t1)
[...]
}
This is great, we're not quite doing so much arranging, but we're definitely doing some
obvious Acting and Asserting. The tests are shorter, more concise, and nothing is lost in the
refactor. This is easy when you have a few immutable let variables, but gets complicated
once you want to have your Arrange steps perform actions.
19
Chapters/En-Uk/Xctest/Behavior Testing
Behaviour Driven Development is about being able to have a consistent vocabulary in your
test-suites. BDD defines the terminology, so they're the same between BDD libraries. This
means, if you use Rspec, Specta, Quick, Ginkgo and many others you will be able to employ
similar testing structures.
So what are these words?
describe - used to collate a collection of tests under a descriptive name.
it - used to set up a unit to be tested
before/after - callbacks to code that happens before, or after each it or describe
20
Chapters/En-Uk/Xctest/Behavior Testing
describe("buy button") {
beforeAll {
// sets up a mock for a singleton object
}
afterAll {
// stops mocking
}
before {
// ensure we are in a logged out state
}
after {
// clear all user credentials
}
it("posts order if artwork has no edition sets") {
// sets up a view controller
// taps a button
// verifies what routes have been called
}
it("posts order if artwork has 1 edition set") {
// [...]
}
it("displays inquiry form if artwork has multiple sets") {
// [...]
}
it("displays inquiry form if request fails") {
// [...]
}
}
By using BDD, we can effectively tell a story about what expectations there are within a
codebase, specifically around this buy button. It tells you:
In the context of Buy button, it posts order if artwork has no edition sets
In the context of Buy button, it posts order if artwork has 1 edition set
In the context of Buy button, it displays inquiry form if artwork has multiple sets
In the context of Buy button, it displays inquiry form if request fails
Yeah, the English gets a bit janky, but you can easily read these as though they were english
sentences. That's pretty cool. These describe blocks can be nested, this makes
contextualising different aspects of your testing suite easily. So you might end up with this in
the future:
21
Chapters/En-Uk/Xctest/Behavior Testing
In the context of Buy button, when logged in, it posts order if artwork has no edition sets
In the context of Buy button, when logged in, it posts order if artwork has 1 edition set
In the context of Buy button, when logged in, it displays inquiry form if artwork has
multiple sets
In the context of Buy button, when logged in, it displays inquiry form if request fails
In the context of Buy button, when logged out, it asks for a email if we don't have one
In the context of Buy button, when logged out, it posts order if no edition sets and we
have email
Where you can split out the it blocks into different describes called logged in and
logged out .
having a collection of setup code in each test. This makes it easier for each it block to be
focused on just the arrange and assert .
I've never felt comfortable writing plain old XCTest formatted code, and so from this point on,
expect to not see any more examples in that format.
Matchers
BDD only provides a lexicon for structuring your code, in all of the examples further on you'll
see things like:
// Objective-C
expect([item.attributeSet title]).to.equal(artist.gridTitle);
// Swift
expect(range.min) == 500
These types of expectations are not provided as a part of XCTest. XCTest provides a
collection of ugly macros/functions like XCTFail , XCTAssertGreaterThan or XCTAssertEqual
which does some simple logic and raises an error denoting the on the line it was called from.
22
Chapters/En-Uk/Xctest/Behavior Testing
As these are pretty limited in what they can do, and are un-aesthetically pleasing, I don't use
them. Instead I use a matcher library. For example Expecta, Nimble or OCHamcrest. These
provide a variety of tools for creating test assertions.
It's common for these libraries to be separate from the libraries doing BDD, in the Cocoa
world, only Kiwi aims to do both BDD structures and matchers.
From my perspective, there's only one major advantage to bundling the two, and that is that
you can fail a test if there were no matchers ran ( e.g. an async test never called back in
time. ) To my knowledge, only Rspec for ruby provides that feature.
23
This gives you a green (passing) test, from there you would refactor your code, now that you
can verify the end result.
You would then move on to the next test, which may verify the type of value returned or
whatever you're really meant to be working on. The idea is that you keep repeating this
pattern and at the end you've got a list of all your expectations in the tests.
24
25
Chapters/En-Uk/Xctest/Integration Testing
Integration Testing
Integration Testing is a different concept to Unit Testing. It is the idea of testing changes in
aggregate, as opposed to individual units. A good testing goal is to have a lot of the finergrained ( thinner brush ) tests covered by unit testing, then Integration Testing will help you
deal with larger ideas ( a paint roller. )
Within the context of Cocoa, integration tests generally means writing tests against things
you have no control over. Which you could argue is all of UIKit, but hey, gotta do that to build
an app. Seriously though, UIKit is the most common thing against which people have done
integration testing.
UI Testing
UI Testing involves running your app as though there was a human on the other side tappig
buttons, waiting for animations and filling in all of bits of data. The APIs make it easy to
make tests like "If I've not added an email, is the submit button disabled?" and "After hitting
submit with credentials, do it go to the home screen?" These let you write tests pretty quickly
( it's now built into Xcode ) and it can be used to provide a lot of coverage fast.
The tooling for in the OSS world is pretty mature now. The dominant player is Square's KIF.
KIF's tests generally look like this:
class ReaderViewUITests: KIFTestCase, UITextFieldDelegate {
[...]
func markAsReadFromReaderView() {
tester().tapViewWithAccessibilityLabel("Mark as Read")
tester().tapViewWithAccessibilityIdentifier("url")
tester().tapViewWithAccessibilityLabel("Reading list")
tester().swipeViewWithAccessibilityLabel("Reader View Test", inDirection: KIFSw
ipeDirection.Right)
tester().waitForViewWithAccessibilityLabel("Mark as Unread")
tester().tapViewWithAccessibilityLabel("Cancel")
}
[...]
}
Where KIF will look or wait for specific views in the view hierarchy, then perform some
actions. Apple's version of KIF, UITesting is similar, but different.
It works by having a completely different test target just for UI Integration Tests, separate
from your Unit Tests. It can build out your test-suite much faster, as it can record the things
you click on in the simulator, and save the actions to your source files in Xcode.
26
Chapters/En-Uk/Xctest/Integration Testing
These tests look like vanilla XCTest, here's some examples from Deck-Tracker
class About: XCTestCase {
let backButton = XCUIApplication().navigationBars["About"].buttons["Settings"]
let aboutTitleScreen = XCUIApplication().navigationBars["About"].staticTexts["Abo
ut"]
let hearthstoneImage = XCUIApplication().images["Hearthstone About"]
[...]
override func setUp() {
super.setUp()
continueAfterFailure = false
XCUIApplication().launch()
let app = XCUIApplication()
app.navigationBars["Games List"].buttons["More Info"].tap()
app.tables.staticTexts["About"].tap()
}
func testElementsOnScreen() {
XCTAssert(backButton.exists)
XCTAssert(aboutTitleScreen.exists)
XCTAssert(hearthstoneImage.exists)
XCTAssert(versionNumberLabel.exists)
XCTAssert(createdByLabel.exists)
XCTAssert(emailButton.exists)
XCTAssert(nounIconsLabel.exists)
}
[...]
}
There are some good up-sides to this approach, it's really fast to set up and to re-create
when something changes. It's a really wide-brushed approach to covering your tested.
The biggest down-side is that it's slow. It requires running all the usual animations and
networking would be performed as usual in the app. These can be worked around with some
networking stubbing libraries, mainly VCR or HTTP Stubs but that adds a lot more
complexity to what is generally a simple approach.
API Testing
If you have a staging environment for your API, it can be worth having your application run
through a series of real-world networking tasks to verify the APIs which you rely on ( but
don't necessarily maintain) continue to act in the way you expect.
This can normally be built with KIF/UITesting, and can be tested
27
Chapters/En-Uk/Xctest/Integration Testing
28
Chapters/En-Uk/Foundations/Dependency Injection
Dependency Injection
Dependency Injection (DI, from here on in) is a way of dealing with how you keep your code
concerns separated. On a more pragmatic level it is expressed elegantly in Jame Shore's
blog post
Dependency injection means giving an object its instance variables. Really. That's it.
This alone isn't really enough to show the problems that DI solves. So, let's look at some
code and investigate what DI really means in practice.
Testing this code can be tricky, as it relies on functions inside the NSUserDefaults and User
class. These are the dependencies inside this function. Ideally when we test this code we
want to be able to replace the dependencies with something specific to the test. There are
many ways to start applying DI, but I think the easiest way here is to try and make it so that
the function takes in it's dependencies as arguments. In this case we are giving the function
both the NSUserDefaults object and a User model.
- (void)saveUser:(User *)user inDefaults:(NSUserDefaults *)defaults
{
[defaults setObject:[user dictionaryRepresentation] forKey:@"user"];
[defaults setBool:YES forKey:@"injected"];
}
In Swift we can use default arguments to acknowledge that we'll most often be using the
sharedUserDefaults as the defaults var:
29
Chapters/En-Uk/Foundations/Dependency Injection
This little change in abstraction means that we can now insert our own custom objects inside
this function. Thus, we can inject a new instance of both arguments and test the end results
of them. Something like:
it(@"saves user defaults", ^{
NSUserDefaults *defaults = [[NSUserDefaults alloc] init];
User *user = [User stubbedUser];
UserArchiver *archiver = [[UserArchiver alloc] init];
[archiver saveUser:user inDefaults:defaults];
expect([user dictionaryRepresentation]).to.equal([defaults objectForKey:@"user"]);
expect([defaults boolForKey:@"injected"]).to.equal(YES);
});
We can now easily test the changes via inspecting our custom dependencies.
This example grabs some names via an API call, then sets the instance variable names to
be the new value from the network. In this example the object that is outside of the scope of
the UserNameTableVC is the MyNetworkingClient .
30
Chapters/En-Uk/Foundations/Dependency Injection
This means that in order to easily test the view controller, we would need to stub or mock the
sharedClient() function to return a different version based on each test.
The easiest way to simplify this, would be to move the networking client into an instance
variable. We can use Swift's default initialisers to set it as the app's default which means
less glue code ( in Objective-C you would override a property's getter function with a default
unless the instance variable has been set. )
class UserNameTableVC: UITableViewController {
var names: [String] = [] {
[...]
}
var network: MyNetworkingClient = .sharedClient()
override func viewDidLoad() {
super.viewDidLoad()
network.getUserNames { newNames in
self.names = newNames
}
}
}
This can result in simpler app code, and significantly easier tests. Now you can init a
UITableViewController and set the .network with any version of the MyNetworkingClient
31
Chapters/En-Uk/Foundations/Dependency Injection
[...]
@interface ARSyncConfig : NSObject
- (instancetype)initWithManagedObjectContext:(NSManagedObjectContext *)context
defaults:(NSUserDefaults *)defaults
deleter:(ARSyncDeleter *)deleter;
@property (nonatomic, readonly, strong) NSManagedObjectContext *managedObjectContext
;
@property (nonatomic, readonly, strong) NSUserDefaults *defaults;
@property (nonatomic, readonly, strong) ARSyncDeleter *deleter;
@end
The ARSyncAnalytics doesn't have any instance variables at all, the sync object is DI'd in as
a function argument. From there the analytics are set according to the defaults provided
inside the ARSync 's context object. I believe the official name for this pattern is ambient
context.
Read more:
http://www.bignerdranch.com/blog/dependency-injection-ios/ http://www.objc.io/issue15/dependency-injection.html
32
Mocks
A mock object is an object created by a library to emulate an existing object's API. In general
there are two main types of mocks
1. Strict Mocks - or probably just Mocks. These objects will only respond to what you
define upfront, and will assert if they receive anything else.
2. Nice (or Partial) Mocks which wrap existing objects. These mocks objects can define
the methods that they should respond too, but will pass any function / messages they
haven't been told about to the original.
In Objective-C you can define mocks that act as specific instance of a class, conform to
specific protocols or be a class itself. In Swift this is still all up in the air, given the language's
strict type system.
Stubs
A stub is a method that is replaced at runtime with another implementation. It is common for
a stub to not call the original method. It's useful in setting up context for when you want to
use known a return value with a method.
You can think of it as being method swizzling, really.
running on an iPhone simulator. This saves us time from running our test-suite twice,
sequentially, on multiple simulators.
When you own the code that you're working with, it can often be easier to use a fake.
Fakes
33
A Fake is an API compatible version of an object. That is it. Fakes are extremely easy to
make in both Swift and Objective-C.
In Objective-C fakes can be created easily using the loose typing at runtime. If you create an
object that responds to the same selectors as the one you want to fake you can pass it
instead by typecasting it to the original.
In Swift the use of AnyObject is discouraged by the compiler, so the use of fudging types
doesn't work. Instead, you are better off using a protocol. This means that you can rely on a
different object conforming to the same protocol to make test code run differently. It provides
a different level of coupling.
For my favourite use case of Fakes, look at the chapter on Network Models, or Testing
Delegates.
34
Custom Matchers
35
36
37
38
Chapters/En-Uk/Setup/Getting Setup
Getting setup
We're pragmatic, so we use CocoaPods. It is a dependency manager for Cocoa projects,
we're going to use it to pull in the required dependencies. If you're new to CocoaPods then
read the extensive guides website to help you get started.
This links the testing Pods to only the test target. This inherits the app's CocoaPods (in this
case ORStackView. ) CocoaPods will generate a second library for the testing pods Specta,
Expecta and FBSnapshotTestCase. That is only linked to your Tests target.
You can test that everything is working well together by either going to Product > Test in
the menu or by pressing + u . This will compile your application, then run it and inject
your testing bundle into the application.
39
Chapters/En-Uk/Setup/Getting Setup
If that doesn't happen, it's likely that your Scheme is not set up correctly. Go to Product >
Scheme > Edit Scheme.. or press + + , . Then make sure that you have a valid test
40
Covered
- (void)method
{
...
}
This style change was agreed on throughout our apps as a reminder that there was work to
be done.
I would still get started in an existing project without tests this way, make it obvious what is
and what isn't under tests. Then start with some small bugs, think of a way to prove the bug
wrong in code then add the test.
Once we were confident with our testing flow from the smaller bugs. We discussed internally
what parts of the app were we scared to touch. This was easy for us, and that was
authentication for registering and logging in.
41
It was a lot of hastily written code, as it had a large amount of callbacks and a lot of hundred
line+ methods. That was the first thing to hit 100% test coverage. I'm not going to say it was
easy, but I would have no issues letting people make changes there presuming the tests
pass.
This code became well tested, and eventually made its way out of the application and into a
new CocoaPod on its own. A strategy for generating great Open Source.
42
43
I think they have their place, but I'd like to see how long we can go without being forced
into using one of these tools.
I would try and ensure that our tests can run in a non-deterministic order.
Specta, the testing library, doesn't have a way of doing this, nor to my knowledge does
Quick. However, I know for sure that the test cases I have will only work in the same
order that Xcode runs that as an implementation detail. Un-doing that would require
some effort, and an improvement on tooling though.
I would have a log parsers for Auto Layout errors, CGContext errors and tests
that take a long time.
I would use Danger to look for Auto Layout errors, we see them all the time, and ignore
them. We've done this for too long, and now we can't go back. Now we have the tech,
but it's not worth the time investment to fix.
I'd also debate banning will and asyncWait for adding extra complications to the testsuite.
44
Developer Operations
There are days when I get really excited about doing some dev-ops. There are some days I
hate it. Let's examine some of the key parts of a day to day work flow for a pragmatic
programmer:
Code Review
Code Review is an important concept because it enforces a strong deliverable. It is a
statement of action and an explanation behind the changes. I use github for both closed and
open source code, so for me Code Review is always done in the form of Pull Requests.
Pulls Requests can be linked to, can close other issues on merging and have an extremely
high-functioning toolset around code discussions.
When you prepare for a code review it is a reminder to refactor, and a chance for you to give
your changes a second look. It's obviously useful for when there are multiple people on a
project ( in which case it is a good idea to have different people do code reviews each time )
but theres also value in using code reviews to keep someone else in the loop for features
that affect them.
45
Finally Code Review is a really useful teaching tool. When new developers were expressed
interest in working on the mobile team then I would assign them merge rights on smaller Pull
Requests and explain everything going on. Giving the merger the chance to get exposure to
the language before having to write any themselves.
When you are working on your own it can be very difficult to maintain this, especially when
you are writing projects on your own. A fellow Artsy programmer, Craig Spaeth does this
beautifully when working on his own, here are some example pull requests. Each Pull
Request is an atomic set of changes so that he can see what the larger goal was each time.
TODO: Craig's adddress ^
Continuous Integration
Once you have some tests running you're going to want a computer to run that code for you.
Pulling someone's code, testing it locally then merging it into master gets boring very quickly.
There are three major options in the Continous Integration (CI) world, and they all have
different tradeoffs.
Self hosted
Jenkins
Jenkins is a popular language agnostic self-hosted CI server. There are many plugins for
Jenkins around getting set up for github authentication, running Xcode projects and
deploying to Hockey. It runs fine on a Mac, and you just need a Mac Mini set up somewhere
that receives calls from a github web-hook. This is well documented on the internet.
The general trade off here is that it is a high-cost on developer time. Jenkins is stable but
requires maintenance around keeping up to date with Xcode.
Buildkite.io
Buildkite lets you run your own Travis CI-like CI system on your own hardware. This means
easily running tests for Xcode betas. It differs from Jenkins in its simplicity. It requires
significantly less setup, and require less maintenance overall. It is a program that is ran on
your hardware
Xcode Bots
46
Xcode bots is still a bit of a mystery, though it looks like with it's second release it is now at a
point where it is usable. I found them tricky to set up, and especially difficult to work with
when working with a remote team and using code review.
An Xcode bot is a service running on a Mac Mini, that periodically pings an external
repository of code. It will download changes, run optional before and after scripts and then
offer the results in a really beautiful user interface directly in Xcode.
Services
It's nice to have a Mac mini to hand, but it can be a lot of maintenance. Usually these are
things you expect like profiles, certificates and signing. A lot of the time though it's problems
with Apple's tooling. This could be Xcode shutting off halfway though a build, the Keychain
locking up, the iOS simulator not launching or the test-suite not loading. For me, working at a
company as an iOS developer I don't enjoy, nor want to waste time with issues like this. So I
have a bias towards letting services deal with this for me.
The flip-side is that you don't have much control, if you need bleeding-edge Xcode features,
and you're not in control of your CI box, then you have to deal with no CI until the provider
provides.
Travis CI
Travis CI is a CI server that is extremely popular in the Open Source World. We liked it so
much in CocoaPods that we decided to include setup for every CocoaPod built with our
toolchain. It is used by most programming communities due to their free-if-open-source
pricing.
Travis CI is configured entirely via a .travis.yml file in your repo which is a YAML file that
lets you override different parts of the install/build/test script. It has support for local
dependency caching. This means build times generally do not include pulling in external
CocoaPods and Gems, making it extremely fast most of the time.
I really like the system of configuring everything via a single file that is held in your
repository. It means all the necessary knowledge for how your application is tested is kept
with the application itself.
Circle CI
We've consolidated on Circle CI for our Apps. It has the same circle.yml config file
advantage as Travis CI, but our builds don't have to wait in an OSS queue. It also seems to
have the best options for supporting simultaneous builds.
47
Bitrise.io
Bitrise is a newcomer to the CI field and is focused exclusively on iOS. This is a great thing.
They have been known to have both stable and betas builds of Xcode on their virtual
machines. This makes it possible to keep your builds green while you add support for the
greatest new things. This has, and continues to be be a major issue with Travis CI in the
past.
Bitrise differs from Travis CI in that it's testing system is ran as a series of steps that you can
run from their website. Because of this it has a much lower barrier to entry. When given
some of my simpler iOS Apps their automatic setup did everything required with no
configuration.
Build
Internal Deployment
We eventually migrated from Hockey for betas to Testflight. In part because it felt like it was
starting to mature, and also because of a bug/feature in iOS.
We deploy via Fastlane.
TODO: Link to Eigen "App Launch Slow"
iTunes deployment
2015 was a good year for deployment to the App Store, as Felix Krause released a series of
command line tools to do everything from generating snapshots to deploying to iTunes. This
suite of tools is called Fastlane and I cant recommend it enough.
Getting past the mental barrier of presuming an iTunes deploy takes a long time means that
you feel more comfortable releasing new builds often. More builds means less major
breaking changes, reducing problem surface area between versions.
48
49
50
51
Get in line
A friend in Sweden passed on a technique he was using to cover complex series of
background jobs. In testing where he would typically use dispatch_async to run some code
he would instead use dispatch_sync or just run a block directly. I took this technique and
turned it into a simple library that allows you to toggle all uses of these functions to be
asychronous or not.
This is not the only example, we built this idea into a network abstraction layer library too. If
you are making stubbed requests then they happen synchronously. This reduced complexity
in our testing.
It will happen
52
Testing frameworks typically have two options for running asynchronous tests, within the
matchers or within the testing scaffolding. For example in Specta/Expecta you can use
Specta's waitUntil() or Expecta's will .
Wait Until
waitUntil is a simple function that blocks the main thread that your tests are running on.
Then after a certain amount of time, it will allow the block inside to run, and you can do your
check. This method of testing will likely result in slow tests, as it will take the full amount of
required time unlike Expecta's will .
Will
A will looks like this: expect(x).will.beNil(); . By default it will fail after 0.3 seconds, but
what will happen is that it constantly runs the expectation during that timeframe. In the above
example it will keep checking if x is nil. If it succeeds then the checks stop and it moves
on. This means it takes as little time as possible.
Downsides
Quite frankly though, async is something you should be avoiding. From a pragmatic
perspective, I'm happy to write extra code in my app's code to make sure that it's possible to
run something synchronously.
For example, we expose a Bool called ARPerformWorkAsynchronously in eigen, so that we
can add animated: flags to things that would be notoriously hard to test.
For example, here is some code that upon tapping a button it will push a view controller. This
can either be tested by stubbing out a the navigationViewController ( or perhaps providing
an easy to test subclass (fake)) or you can allow the real work to happen fast and verify the
real result. I'd be happy with the fake, or the real one.
- (void)tappedArtworkViewInRoom
{
ARViewInRoomViewController *viewInRoomVC = [[ARViewInRoomViewController alloc] ini
tWithArtwork:self.artwork];
[self.navigationController pushViewController:viewInRoomVC animated:ARPerformWorkA
synchronously];
}
53
Eigen
Let's go through the process simplified for how Eigen's stubbed networking HTTP client
works.
We want to have a networking client that can act differently in tests, so create a subclass of
your HTTP client, in my case, the client is called ArtsyAPI . I want to call the subclass
ArtsyOHHTTPAPI - as I want to use the library OHHTTPStubs to make my work easier.
You need to have a way to ensure in your tests that you are using this version in testing.
This can be done via Dependency Injection, or as I did, by using different classes in a
singleton method when the new class is available.
54
+ (ArtsyAPI *)sharedAPI
{
static ArtsyAPI *_sharedController = nil;
static dispatch_once_t oncePredicate;
dispatch_once(&oncePredicate, ^{
Class klass = NSClassFromString(@"ArtsyOHHTTPAPI") ?: self;
_sharedController = [[klass alloc] init];
});
return _sharedController;
}
Next up you need a point of inflection with the generation of networking operations in the
HTTP client. For ArtsyAPI that is this method: - (AFHTTPRequestOperation
*)requestOperation:(NSURLRequest *)request success:(void (^)(NSURLRequest *request,
NSHTTPURLResponse *response, id JSON))success failure:(void (^)(NSURLRequest *request,
NSHTTPURLResponse *response, NSError *error, id JSON))failureCallback
We want to override this function to work synchronously. So let's talk a little about how this
will work.
1.
Request Lookup
We need an API to be able to declare a stubbed route, luckily for me OHHTTPStubs
has been working on this problem for years. So I want to be able to build on top of that
work, rather than write my own stubbed NSURLRequest resolver.
After some digging into OHHTTPStubs, I discovered that it has a private API that does
exactly what I need.
@interface OHHTTPStubs (PrivateButItWasInABookSoItMustBeFine)
+ (instancetype)sharedInstance;
- (OHHTTPStubsDescriptor *)firstStubPassingTestForRequest:(NSURLRequest *)request
;
@end
2.
Operation Variables
55
So we have request look-up working, next up is creating an operation. It's very likely
that you will need to create an API compatible fake version of whatever you're working
with. In my case, that's AFNetworking NSOperation subclasses.
However, first, you'll need to pull out some details from the stub:
// Grab the response by putting in the request
OHHTTPStubsResponse *response = stub.responseBlock(request);
// Open the input stream for in JSON data
[response.inputStream open];
id json = @[];
NSError *error = nil;
if (response.inputStream.hasBytesAvailable) {
json = [NSJSONSerialization JSONObjectWithStream:response.inputStream options:N
SJSONReadingAllowFragments error:&error];
}
This gives us all the details we'll need, the response object will also contain things like
statusCode and httpHeaders that we'll need for determining operation behaviour.
3.
Operation Execution
In my case, I wanted an operation that does barely anything. The best operation that
does barely anything is the trusty NSBlockOperation - which is an operation which
executes a block when something tells it to start. Easy.
@interface ARFakeAFJSONOperation : NSBlockOperation
@property (nonatomic, strong) NSURLRequest *request;
@property (nonatomic, strong) id responseObject;
@property (nonatomic, strong) NSError *error;
@end
@implementation ARFakeAFJSONOperation
@end
Depending on how you use the NSOperation s in your app, you'll need to add more
properties, or methods in order to effectively fake the operation.
For this function to be completed it needs to return an operation, so lets return a
ARFakeAFJSONOperation .
56
So we create an operation, that either calls the success or the failure block in the
inflected method depending on the data from inside the stub. Effectively closing the loop
on our synchronous networking. From here, in the case of ArtsyAPI another object will
tell the ARFakeAFJSONOperation to start, triggering the callback synchronously.
4.
Request Failure
Having a synchronous networking lookup system means that you can also detect when
networking is happening when you don't have a stubbed request.
We have a lot of code here, in order to provide some really useful advice to
programmers writing tests. Ranging from a copy & paste-able version of the function
call to cover the networking, to a full stack trace showing what function triggered the
networking call
TODO: Example of what one looks like
With this in place, all your networking can run synchronously in tests. Assuming they all go
through your point of inflection, it took us a while to iron out the last few requests that
weren't.
AROHHTTPNoStubAssertionBot
I used a simplification of the above in a different project, to ensure that all HTTP requests
we're stubbed. By using the same OHHTTPStubs private API, I could detect when a request
was being ignored by the OHHTTPStubs singleton. Then I could create a stack trace and give
a lot of useful information.
57
Then I used "fancy" runtime trickery to change the class of the OHHTTPStubs singleton at
runtime on the only part of the public API.
This technique is less reliable, as it relies on whatever the underlying networking operation
does. This is generally calling on a background thread, and so you lose a lot of the useful
stack tracing. However, you do get some useful information.
Moya
Given that we know asynchronous networking in tests is trouble, for a fresh project we opted
to imagine what it would look like to have networking stubs as a natural part of the API
description in a HTTP client. The end result of this is Moya.
In Moya you have to provide stubbed response data for every request that you want to map
using the API, and it provides a way to easily do synchronous networking instead.
58
59
60
Network Models
In another chapter, I talk about creating HTTP clients that converts async networking code
into synchronous APIs. Another tactic for dealing with testing out your networking.
There are lots of clever ways to test your networking, I want to talk about the simplest. From
a View Controller's perspective, all networking should go through a networking model.
A network model is a protocol that represents getting and transforming data into something
the view controller can use. In your application, this will perform networking asynchronously in tests, it is primed with data and synchronously returns those values.
Let's look at some code before we add a network model:
class UserNamesTableVC: UITableViewController {
override func viewDidLoad() {
super.viewDidLoad()
MyNetworkingClient.sharedClient().getUsers { users in
self.names = users.map { $0.names }
}
}
}
OK, so it accesses a shared client, which returns a bunch of users - as we only need the
names, we map out the names from the users and do something with that.
Let's start by defining the relationship between UsersNameTableVC and it's data:
protocol UserNamesTableNetworkModelType {
func getUserNames(completion: ([String]) -> ())
}
61
We can then bring this into our ViewController to handle pulling in data:
class UserNamesTableVC: UITableViewController {
var network: UserNamesTableNetworkModelType = UserNamesTableNetworkModel()
override func viewDidLoad() {
super.viewDidLoad()
network.getUserNames { userNames in
self.names = userNames
}
}
}
OK, so we've abstracted it out a little, this is very similar to what happened back in the
Dependency Injection chapter. To use network models to their fullest, we want to make
another object that conforms to the UserNamesTableNetworkModelType protocol.
class StubbedUserNamesTableNetworkModel: UserNamesTableNetworkModelType {
var userNames = []
func getUserNames(completion: ([String]) -> ()) {
completion(userNames)
}
}
This pattern has saved us a lot of trouble over a long time. It's a nice pattern for one-off
networking issues, and can slowly be adopted over time.
62
63
Chapters/En-Uk/Async/Animations
Animations
Animations are notorious for being hard to test. The problem arises from the fact that
normally an animation is a fire and forget change that is handled by Apple.
UIView animations
One way that we deal with animations in tests is by having a strict policy on always including
a animates: bool on any function that could contain animation. We mix this with a
CocoaPod that makes it easy to do animations with a boolean flag,
UIView+BooleanAnimations. This provides the UIView class with an API like:
[UIView animateIf:animates duration:ARAnimationDuration :^{
self.relatedTitle.alpha = 1;
}];
Which gives the control whether to animate in a test or not to the animates BOOL. If this is
being called inside a viewWillAppear: method for example, then you already have a bool to
work with.
Core Animation
It can be tough to test a core animation
64
65
66
Snapshot Testing
The process of taking a snapshot of the view hierarchy of a UIView subclass, then storing
that as a reference for what your app should look like.
Why Bother
TLDR: Fast, easily re-producible tests of visual layouts. If you want a longer introduction,
you should read my objc.io article about Snapshot Testing. I'll be assuming some familiarity
from here on in.
Techniques
We aim for snapshot tests to cover two main areas
1. Overall state for View Controllers
2. Individual States per View Component
As snapshots are the largest testing brush, and as the apps I work on tend to be fancy
perspectives on remote data. Snapshot testing provides easy coverage, for larger complex
objects like view controllers where we can see how all the pieces come together. They can
then provide a finer grained look at individual view components.
Let's use some real-world examples.
Custom Cells
Ideally you should have every state covered at View level. This means for every possible
major style of the view, we want to have a snapshot covering it
67
This means we have a good coverage of all the possible states for the data. This makes it
easy to do code-review as it shows the entire set of possible styles for your data.
The View Controller is where it all comes together, in this case, the View Controller isn't
really doing anything outside of showing a collection of cells. It is given a collection of items,
it does the usual UITableView datasource and delegate bits and it shows the history.
Simple.
So for the View Controller, we only need a simple test:
class LiveAuctionBidHistoryViewControllerTests: QuickSpec {
override func spec() {
describe("view controller") {
it("looks right with example data") {
let subject = LiveAuctionBidHistoryViewController()
// Triggers viewDidLoad (and the rest of the viewXXX methods)
subject.beginAppearanceTransition(true, animated: false)
subject.endAppearanceTransition()
subject.lotViewModel = StubbedLotViewModel()
expect(subject).to( haveValidSnapshot() )
}
}
}
}
68
This may not show all the different types of events that it can show, but those are specifically
handled by the View-level tests, not at the View Controller.
69
70
UIButtons
UIGestures
Target Action
71
Multi-Device Support
There are mainly two ways to have a test-suite handle multiple device-types, and
orientations. The easy way: Run your test-suite multiple times on multiple devices,
simulators, and orientations.
The hard way: Mock and Stub your way to multi-device support in one single test-suite.
Device Fakes
Like a lot of things, this used to be easier in simpler times. When you could just set a device
size, and go from there, you can see this in Eigen's - ARTestContext.m
TODO - Link ^
static OCMockObject *ARPartialScreenMock;
@interface UIScreen (Prvate)
- (CGRect)_applicationFrameForInterfaceOrientation:(long long)arg1 usingStatusbarHeigh
t:(double)arg2 ignoreStatusBar:(BOOL)ignore;
@end
+ (void)runAsDevice:(enum ARDeviceType)device
{
[... setup]
ARPartialScreenMock = [OCMockObject partialMockForObject:UIScreen.mainScreen];
NSValue *phoneSize = [NSValue valueWithCGRect:(CGRect)CGRectMake(0, 0, size.width, s
ize.height)];
[[[ARPartialScreenMock stub] andReturnValue:phoneSize] bounds];
[[[[ARPartialScreenMock stub] andReturnValue:phoneSize] ignoringNonObjectArgs] _appl
icationFrameForInterfaceOrientation:0 usingStatusbarHeight:0 ignoreStatusBar:NO];
}
This ensures all ViewControllers are created at the expected size. Then you can use your
own logic to determine iPhone vs iPad. This works for simple cases, but it isn't optimal in the
current landscape of iOS apps.
Trait Fakes
Trait collections are now the recommended way to distinguish devices, as an iPad could
now be showing your app in a screen the size of an iPhone. You can't rely on having an
application the same size as the screen. This makes it more complex.
72
This is not a space I've devoted a lot of time to, so consider this section a beta. If anyone
wants to dig in, I'd be interested in knowing what the central point of knowledge for train
collections is, and stubbing that in the way I did with
_applicationFrameForInterfaceOrientation:usingStatusbarHeight:ignoreStatusBar: .
Every View or View Controller (V/VC) has a read-only collection of traits, the V/VCs can
listen for trait changes and re-arrange themselves. For example, we have a view that sets
itself up on the collection change:
TODO: Link to AuctionBannerView.swift
class AuctionBannerView: UIView {
override func traitCollectionDidChange(previousTraitCollection: UITraitCollection?)
{
super.traitCollectionDidChange(previousTraitCollection)
// Remove all subviews and call setupViews() again to start from scratch.
subviews.forEach { $0.removeFromSuperview() }
setupViews()
}
}
When we test this view we stub the traitCollection array and trigger
traitCollectionDidChange , this is done in our Forgeries library. It looks pretty much like this,
Giving us the chance to make our V/VC think it's in any type of environment that we want to
write tests for.
73
This example has a class whose responsibility is to deal with getting data, and providing that
to a tableview.
class ORArtworkDataSource, NSObject, UITableViewDataSource {
// Do some networking, pull in some data, make it possible to generate cells
func getData() {
[...]
}
[...]
}
class ORArtworkViewController: UITableViewController {
var dataSource: ORArtworkDataSource!
[...]
override func viewDidLoad() {
dataSource = ORArtworkDataSource()
tableView.dataSource = dataSource
dataSource.getData()
}
}
This implementation is great if you don't want to write any tests, but it can get tricky to find
ways to have your tests perform easy-to-assert on behavior with this tactic.
One of the simplest approaches to making this type of code easy to test is to use lazy
initialisation, and a protocol to define the expectations but not the implementation.
So, define a protocol that says what methods the ORArtworkDataSource should have then
only let the ORArtworkViewController know it's talking to something which conforms to this
protocol.
74
This allows you to create a new object that conforms to ORArtworkDataSourcable which can
have different behaviour in tests. For example:
it("shows a tableview cell") {
subject = ORArtworkViewController()
subject.dataSource = ORStubbedArtworkDataSource()
// [...]
expect(subject.tableView.cellForRowAtIndexPath(index)).to( beTruthy() )
}
There is a great video from Apple called Protocol-Oriented Programming in Swift that covers
this topic, and more. The video has a great example of showing how you can test a graphic
interface by comparing log files because of the abstraction covered here.
The same principals occur in Objective-C too, don't think this is a special Swift thing, the
only major new change for Swift is the ability for a protocol to offer methods, allowing for a
strange kind of multiple inheritence.
75
technique
76
Core Data
Core Data is just another dependency to be injected. It's definitely out of your control, so in
theory you could be fine using stubs and mocks to control it as you would like.
From my perspective though, I've been creating a blank in-memory NSManagedObjectContext
for every test, for years, and I've been happy with this.
Memory Contexts
An in-memory context is a managed object context that is identical to your app's main
managed object context, but instead of having a SQL NSPersistentStoreCoordinator based
on the file system it's done in-memory and once it's out of scope it disappears.
Here's the setup for our in-memory context in Folio:
+ (NSManagedObjectContext *)stubbedManagedObjectContext
{
NSDictionary *options = @{
NSMigratePersistentStoresAutomaticallyOption : @(YES),
NSInferMappingModelAutomaticallyOption : @(YES)
};
NSManagedObjectModel *model = [CoreDataManager managedObjectModel];
NSPersistentStoreCoordinator *persistentStoreCoordinator = [[NSPersistentStoreCoor
dinator alloc] initWithManagedObjectModel:model];
[... Add a memory store to the coordinator]
NSManagedObjectContext *context = [[NSManagedObjectContext alloc] initWithConcurre
ncyType:NSMainQueueConcurrencyType];
context.persistentStoreCoordinator = persistentStoreCoordinator;
return context;
}
This context will act the same as your normal context, but it's cheap and easy to fill. It's also
going to run functions on the main-thread for you to if you use NSMainQueueConcurrencyType
simplifying your work further.
Having one of these is probably the first step for making tests against any code touching
Core Data.
77
It's extremely common to wrap the Core Data APIs, they're similar to XCTest and Auto
Layout in that Apple provides a low-level standard library then everyone builds their own
wrappers above it.
The wrappers for Core Data tend to not be built with DI in mind, offering their own singleton
access for a main managed object context. So you may need to send some PRs to allow
passing an in-memory NSManagedObjectContext instead of a singleton.
This means I ended up writing a lot of functions that looked like this:
@interface NSFetchRequest (ARModels)
/// Gets all artworks of an artwork container that can be found with current user sett
ings with an additional scope predicate
+ (instancetype)ar_allArtworksOfArtworkContainerWithSelfPredicate:(NSPredicate *)selfS
copePredicate inContext:(NSManagedObjectContext *)context defaults:(NSUserDefaults *)d
efaults;
[...]
Which, admittedly would be much simpler in Swift thanks to default initialiser. However, you
get the point. Any time you need to do fetches you need to DI the in-memory version
somehow. This could be as a property on an object, or as an argument in a function. I've
used both, a lot.
78
This is something you want to do early on in writing your tests. The later you do it, the larger
the changes you will have to make to your existing code base.
This makes it much easier to move to all objects to accept in-memory
NSManagedObjectContext via Dependency Injection. It took me two days to migrate all of the
code currently covered by tests to do this. Every now and again, years later, I start adding
tests to an older area of the code-base and find that mainManagedObjectContext was still
being called in a test. It's a great way to save yourself and others some frustrating
debugging time in the future.
79
These would add an object into the context, and also let it return the newly inserted object
in-case you had test-specific modifications to do.
80
SpecEnd
81
Nothing too surprising, but I think it's important to note that these tests are the slowest tests
in the app that hosts them at a whopping 0.191 seconds. I'm very willing to trade a fraction
of a second on every test run to know that I'm not breaking app migrations.
These are tests that presume you still have people using older builds, every now and again
when I'm looking at Analytics I check to see if any of these test can be removed.
Finally, if you don't use Core Data you may still need to be aware of changes around model
migrations when storing using NSKeyedArchiver . It is a lot harder to have generic futureproofed test cases like the ones described here. However, here is an example in eigen.
82
83
84
85
Chapters/En-Uk/Wrap Up/Books
86
87
Recommended Websites
Link
Concept
http://qualitycoding.org
http://iosunittesting.com
objc.io issue 15
88