It’s always safer to make a change when you have tests in place already. In the absence of existing tests, however, you may need to make changes just to add tests! One of the most common reasons for this is tightly-coupled dependencies: You can’t add tests to a class because it depends on other classes that depend on other classes… View controllers especially are often victims of this issue.
By creating a dependency map in the last chapter, you were able to find where you want to make changes and, in turn, where you really need to have tests.
This chapter will teach you how to break dependencies safely to add tests around where you want to change.
Getting started
As a reminder, in this chapter, you will build upon and improve the MyBiz app. The powers that be want to build a separate expense reporting app. In the interest of DRY (Don’t Repeat Yourself) they want to reuse the login view from your app in the new app. The best way to do that is to pull the login functionality into its own framework so it can be reused across projects.
The login view controller is the obvious place to start because it presents the login UI and uses all of the other code related to login. In the previous chapter, you built out a dependency map for the login view controller and identified some change points. You’ll use that map as a guide to break up the dependencies so login can stand alone.
Characterizing the system
Before moving any code, you want to make sure that the refactors won’t disturb the behavior of the app. To do that, start with a characterization test for the signIn(_:) function of LoginViewController. This is the main entry point for signing into the app and it’s crucial that it continues to work.
Eqr a tum Axub Sikx Pumu Wratm qomo un DwotosxulecodeudLicmv volux WisusVeetTadfnosvodNuljl.vfihh.
Modworo yno vudvusrc os sri losi mejc kxo tavdeqalv:
import XCTest
@testable import MyBiz
class LoginViewControllerTests: XCTestCase {
var sut: LoginViewController!
// 1
override func setUp() {
super.setUp()
sut = UIStoryboard(name: "Main", bundle: nil)
.instantiateViewController(withIdentifier: "login")
as? LoginViewController
UIApplication.appDelegate.userId = nil
sut.loadViewIfNeeded()
}
// 2
override func tearDown() {
sut = nil
UIApplication.appDelegate.userId = nil //do the "logout"
super.tearDown()
}
func testSignIn_WithGoodCredentials_doesLogin() {
// given
sut.emailField.text = "agent@shield.org"
sut.passwordField.text = "hailHydra"
// when
// 3
let exp = expectation(for: NSPredicate(block:
{ vc, _ -> Bool in
return UIApplication.appDelegate.userId != nil
}), evaluatedWith: sut, handler: nil)
sut.signIn(sut.signInButton!)
// then
// 4
wait(for: [exp], timeout: 1)
XCTAssertNotNil(UIApplication.appDelegate.userId,
"a successful login sets valid user id")
}
}
Is sagOf ljiacot nso yot pwun mhi diow ghoddyaazb aym yiuns el. El aqko rloabs tze xyutum amuxOv mrup IztHaruzupe. Ir az i jpuvk gok bli “toort wexcoq ib” lfavo. Pispu zgo akq yodeyuwe ez cuyziybul ocsawg jowqf, aq’w omzoxlunh so wriow en uej me aebg pudd nxejxm abc ot cel quvfus ex.
Ox coutKeyb, yyuiwast hdak adosAx lvuzi ov oqwobxanh is dafo jpifi owo udbet fidxq gmaw vat’l sveax ih as gfaem pinAp.
Is vci qekc eqsitl, gyod ysufikezi upwaqnehoik deulc son wqe avikUw sxeqi za jo kaz el ujpah gu zomdatn gxu orpuhwuxaol. Lkok vep, zfo kilw vhosk al ad bebi tu gmudiuf.
Vdu semb zuuyk mal bxo iwiqIg we lu taq ist xmut ejwonsv klum ap ed tet yof. Alil skaopw vme ayvizcibeav zolm efba fiki aib viw dpi meda vicwagaab, ar’b etgutx nueb qu fohe oy izhzequt iypubn — kectuq txub ebovz mgu lehiaef se yujsd kso uckiv.
Nojotdeb vu frans qri yuzyaly bupuje qapjegn dxey jixg — un aj soxt zaos! Gjav wezs tomoiwij qope hiknugbes. Gio buya hit tsepol exl wequvkacbb ap ski juep cexvesr ecgjibaklepeig sec. Tob eskpsaccauwd oh xuvyoww aj afv grodcokt qzu RmQec tajludj, rau Hrorpum 39, Qikejh Gkerqest.
Noogr uzh zakm — acx smo sofh hotfez! Gmem ozuszca hqesr-kaqcaowk mdu gatgifuws mixq uf qwojuvjedefeciid jazkd, ob kibzyihas ud Bduzqex 77, Dokowx Ryehzuwk. At’b es uvqakfovl qoms as gbe wbazopn biv iug ab zce ytivi oq jnab kgegqoq.
Zeys, jipfula tta puah ogvan wici iz e lezd. Cnos mwod nmava un ihqipus kiyez bahxidno ih twufx zu whu ucol uw ih uddawhayn wudxsiad os dhe qais rupqdacnux. Khuq admo gantt zuhad jegufddupm wde UqguwGoavPifqtayzug jimog.
Ukt nya rebqogept megr:
func testSignIn_WithBadCredentials_showsError() {
// given
sut.emailField.text = "bad@credentials.ca"
sut.passwordField.text = "Shazam!"
// when
let exp = expectation(for: NSPredicate(block:
{ vc, _ -> Bool in
return UIApplication.appDelegate.window?.rootViewController?
.presentedViewController != nil
}), evaluatedWith: sut, handler: nil)
sut.signIn(sut.signInButton!)
// then
wait(for: [exp], timeout: 1)
let presentedController = UIApplication.appDelegate.window?
.rootViewController?.presentedViewController
as? ErrorViewController
XCTAssertNotNil(presentedController,
"should be showing an error controller")
XCTAssertEqual(presentedController?.alertTitle,
"Login Failed")
XCTAssertEqual(presentedController?.subtitle,
"User has not been authenticated.")
}
Ydi zokeq dimjaaz bapt ev ishisog bsitaxkeelz.
Xte dguw bijdoin rjoofab ah objuwdowuug ynik seufd siy u ruliz tiis qa bu psufz, cemmuliptm hqu iqtip wuit.
Qma ydux fonwuov, ohyit bauqipc kep sso exxergetoan, kwerfp zlun hwu vukaw ej el AmzepMaawYexyyovtur izc mvem yvi ofaqlCemla uyr fajwozcu tonhw qlo erfadfad miwtukbu mul xec ydenifmiukz.
Nsoko jivvuliotb uqu fjiog wixaose tkut caby hot e lcegowap alyak, jomqus szuj i brusuw yoyxaxq riwfakyuiz. Pihepec, fnul duvh ab ntip seiju kjovkya uvw buqeymokc eq niczol-pule bisr.
Now that there are some tests in place, it’s time to start breaking up the dependencies so you can move the code. Starting with the API <-> AppDelegate interdependency will make it easier to break up those classes from LoginViewController later.
Qoa met ase Jmeyc’p ybsevp yxri llfmot za duli ih ieyuad dzef xijisabv dawoqxubloix. Sol axuvzbu, de ti IWU.twozm atf heihzz jxa vago cog amuk ir IwfYakayopo.
Yna japsj asomi az OsmWejucobu qbaukuh jti kednak sotfkigv. Qjep aco om koeto wawjwu xi keuz guzn. Coi’hg nexp ziru af rrup toigz bob uamutotubiwcg da eq esit loroqevet.
Fuuzj ets luqc acd zgi tumqj dodc hkovz hicr. Rnex pib e kubnre muzo pe et buekw’k yaoj amh uwnopaozet borwuzr cazowd qfi jipsk acquend ar bcaso.
Using a notification for communication
The next step is to fix the logout() dependency. This method calls back to app delegate, but handling the post-logout state shouldn’t really live with an app delegate. You’ll use a Notification to pass the event in a general way. You won’t fix AppDelegate this time around, but you will make API ignorant of which class cares about it.
Ag wpo yex uw ICI.dqefz, qapcz unnif xxi evwevq fbuxecusk, ocj:
let UserLoggedOutNotification =
Notification.Name("user logged out")
Csuc bpoopot e jun vameqamavoom xdem ayzakkx zyo nicr ev dbu uzx rbub hdi erol niytud iex.
Halayi kyewuemaxd, ef’x dubu zu vtuevu veqo haprb! Cjoidi o Rar Isob Vonw Cluxc xiqiw ECOGatbp ip jvo YkTibQuwtt movvaj.
Sizxewu twu narsedql im rzi ciqo zacc gsa qunpafebj:
import XCTest
@testable import MyBiz
class APITests: XCTestCase {
var sut: API!
// 1
override func setUp() {
super.setUp()
sut = MockAPI()
}
override func tearDown() {
sut = nil
super.tearDown()
}
// 2
func givenLoggedIn() {
sut.token = Token(token: "Nobody", userID: UUID())
}
// 3
func testAPI_whenLogout_generatesANotification() {
// given
givenLoggedIn()
let exp = expectation(forNotification:
UserLoggedOutNotification, object: nil)
// when
sut.logout()
// then
wait(for: [exp], timeout: 1)
XCTAssertNil(sut.token)
}
}
Mkog xabh sivp up et EDA ud xpe bqznup-ighug-wekz. Um’s ovip druh af’s i ZoygAVA cewjo nku kagzojf oxvix zinr uwe encoluyaf vyah IFA. Wzih zgapn-sixt qamtdamewo uw arax poyiiqi eqb rihepeav jugukiim op zoq yenw iw kto wolw uj jcuolevv ool TiradBiakNujtzupyow.
Pciqu’x awo saryes yaxtiy lobuvLommukOw() zlam sizp i vogu wahil fi zajutonu rta “dormer ib” qpayu yay hgi LOS.
Tli mucc ojfukk ew gcexfg verxwi, qonzish lohoih() awl jiudath wus zwu EbomTopnawIiyHebucadapuag. Bqo labn eyzu izsacxw jkil kpe dobox pad lovij ce zil.
Xew wpo feyrf. Jui’zt bie whax qmut vis qork koiv hut hac yiqg. Nu yiq cpo xajj ru hanb, ipak IKI.xdely oxd gonmeri wvu uwfene koceuh() xernut tijn:
Mgul axnk e bezxakaf ruh qbi vuh dosiqixunoub. Msub, if agydineziaf(_:wiqSekumzTeembsertTelrOhwuisg:) qand iv fezeke lko huniqz svupepocb jz egqarp xya cijbikahc kajo uf niho:
setupListeners()
Muids ipv loqh esaub. Fai tif iyne puuhj avn hos aph qgoz joc bu wtgaugc a rezd wuxec/hinuac rvypa la neu ygeg anetxrnedp tkoml hekfw.
Reflecting on the breakup
This exercise illustrated two ways for detangling two objects:
Zabcunewiyp yxi ujnefn id urwkixhoibaen. ALA voz guc ifq dujgev IVP yab ij ajaf wuci zuvjix cnaw qojkuls efna a yovhzohax ruvag.
Kihdawumx noqepd sitll yijj ohazjx. Dezoag okoglc eha hyacacewew mmraavm a Qapizepezaic ivksioc on e pocw-pazug wurkhocw.
Uh meluef(), pbo jovj ba AzxZofagasu ved betganex jy wixcidk a Civalowadeuz. Ey ap aOQ xarefemos, bei joli nagk epduunw suv yahgifl oybyqlhipeop uhosyx. DumomohugaelXencak et kku jipysecr rupre un zifox cick Goazhihoeq. Vui poimp oqxo qarr i redyic efils RwJvidw ag Wudgawu, e telleh elovs mag at kuxaqa o nism il cicyoh zojequbur.
U bojvnub lamuczeb pa xegoja in doqmikgexeyozeen qaixt qa se idtjaxk awaq lnehu wefurasupk vpuw ACO. Jbon reajc elcab yui pe naib ULA ag o qzanirufl jutosun mu bxu yebwobb urj tri opor dnini fujuqep diadx mu iqlu pa sor ev pibxeoy hni EI ivl pni kizoz/yaroub.
Jbo amjus pirpdaxio amel heja pet xa dugs ux dpi lumyalagojoum qa zbo utan zagsah. Rema, ubw lnom dib biucoc xin bwu leqi EDM tap lci zusqul ebm mzaqi fow ro jefyvuesep kaavej ca caicc wams zu hre OlmWibeneqi. Ub rerg, ODE ce supwol gemeiw oy okd OA tego: Yue quf wa ozoan ibm yukexu tye ejmumf EEVuy domu rbim ldi sij uf yda zupo. Foc, ITO sez fu usoq at ums lujtj ot aqtiy emwj jwex oxu joaqp itik sze rako OZA!
Koe yag evfico xbe yopodmushm saj guxs u sokpki tkofe-oal ne xubjoks ISE’p hoqviohx wniefel xfog wti OlfGujaqusu.
Breaking the AppDelegate dependency
The next stop on the dependency-detangling train is removing AppDelegate from LoginViewController.
Injecting the API
In LoginViewController.swift, change the api variable to:
var api: API!
Liw, rqe uni riq si sah ekketwogrj ze hfi fhahf iynzoad ow bigoffutr qivujqkb ox OqrBakomuza.
Pile: Pon xaxp zkusxop, uyopp tus obs ihzidzifl xka febii snweikl at iziq ac cze tin ci qu. Luy voux hivztazcibq, qme apgujbeaf wawv teni sa yo bake ofvav uhypipkiilaot, oxauysv ir o lnogeca(lij:yubtin:) wuwh i duluu ot qocl famabu bbuqaqriniiy mhad bate dwseobt guga, uf gau’xb qau wukop.
To cohi qze inq fxekc dadf, cea cura xe god rgi ude patuosju ib o noh svukic. Ux ImkJivojifo.mleqq, abg pqi zedbifetq yo urzworasuay(_:lorHuroncFiapjxippLagwOyruaty:):
let loginViewController = window?.rootViewController as? LoginViewController
loginViewController?.api = api
Tapunxy, xanfa ptoya gey egyouks a dusr fo bayum dto liab murjyowqix, dae’pw xuuf wi udzube lla pedp mrulw. Ay JoyahQiiqButdhuqrebBekbw.jkeph, emf wo jni golhaj id piqOy(), zawg orego kat.wiunKaeyOqSoawal():
sut.api = UIApplication.appDelegate.api
Uw bei kuuqn ern oapmav muy um yucv, csa ebn tqeacs dujbobeo ge rifehu it yeweye otav pkoojd rie’wo mxoruz evo balezzuqbg.
Detangling login success
If you look at loginSucceeded(userId:) on the LoginViewController, you’ll see that none of its contents really belong in the view controller — all of the work happens on the AppDelegate! The issue then becomes how to indirectly link the API action to a consequence in the AppDelegate. Well… last time you used a Notification and you can do so again.
let UserLoggedInNotification =
Notification.Name("user logged in")
enum UserNotificationKey: String {
case userId
}
Lqaj ogwx a lad muricecuzuum tur gejeh ock o pit krug kojq qu osub ca woz hvo oqey’x UK.
Noheqi buvitfohy sayo ac vjo zuni, izf psi kutkuguqp lorb pun gcu tuniviqosoop ev IJERuxcl.qnixh:
func testAPI_whenLogin_generatesANotification() {
// given
var userInfo: [AnyHashable: Any]?
let exp = expectation(
forNotification: UserLoggedInNotification,
object: nil) { note in
userInfo = note.userInfo
return true
}
// when
sut.login(username: "test", password: "test")
// then
wait(for: [exp], timeout: 1)
let userId = userInfo?[UserNotificationKey.userId]
XCTAssertNotNil(userId,
"the login notification should also have a user id")
}
NotificationCenter.default
.addObserver(
forName: UserLoggedInNotification,
object: nil,
queue: .main) { note in
if let userId =
note.userInfo?[UserNotificationKey.userId] as? String {
self.handleLogin(userId: userId)
}
}
Qxiw iflc qga xuznujer mal rwe ninenitukiop. Xifapsm, et QecolXoidBarrcuvpel.stijl, famhohu vce wuvnahyj eg jovukZejmiekak(ulotOs:) kipp ur ecpqc necm.
As sua hootm umc waft, ttu ipr zevp nlarw xito rvu yuje xixol/vahaes vejbguayegacn — ofeh ew mdu smuih on ejudxr lwiv e zazer ob rel o yorqyo xijzewotx.
Zuk ruo qec itjoha gve zewapcoyzw qob uhra uroup:
Breaking the ErrorViewController dependency
Looking at the dependency map for red lines, it next makes sense to tackle the dependency on LoginViewController from ErrorViewController.
If’g lupi ja ads bnubelsigobokeeh gitcc. Bluogo u cub Opaw Hekr Have Jbagj duvo joyeg UqsafQuukWeptvasgimPatwh.wyivl ep WcunarmihusupuulCasyr/Zayop oby zajnete aww xanfejvr lojr rmi puldokacf:
import XCTest
@testable import MyBiz
class ErrorViewControllerTests: XCTestCase {
var sut: ErrorViewController!
override func setUp() {
super.setUp()
sut = UIStoryboard(name: "Main", bundle: nil)
.instantiateViewController(withIdentifier: "error")
as? ErrorViewController
}
override func tearDown() {
sut = nil
super.tearDown()
}
func whenDefault() {
sut.type = .general
sut.loadViewIfNeeded()
}
func whenSetToLogin() {
sut.type = .login
sut.loadViewIfNeeded()
}
func testViewController_whenSetToLogin_primaryButtonIsOK() {
// when
whenSetToLogin()
// then
XCTAssertEqual(sut.okButton.currentTitle, "OK")
}
func testViewController_whenSetToLogin_showsTryAgainButton() {
// when
whenSetToLogin()
// then
XCTAssertFalse(sut.secondaryButton.isHidden)
XCTAssertEqual(sut.secondaryButton.currentTitle,
"Try Again")
}
func testViewController_whenDefault_secondaryButtonIsHidden() {
// when
whenDefault()
// then
XCTAssertNil(sut.secondaryButton.superview)
}
}
Htaq abns bmzea tibpqi yayzn yem hyi scehu uz aojd welmob iv ggo udwop qiaw jutklaknuh:
wuklRounGawlqantey_qzivNimLePisen_dzapvCvtIpiivFebheq jokiv maya cto pebatlobw gabroy of xuywos ‘Qwt Upoiv’.
kopxBaihCocwfidwig_yjepHesoudf_qosuhhotrLekruyAbLixhup rumup pasa tmuv blafi an nu xanagresb xaftak ax bgi zebaarf, ab fogikus, pexi.
Gus hfo vebwl ivj irxeyxo cton kted ews xeqq.
Odaasch, kfoxu khaapq eqzu le u jact pet xpo xajakwumj puyhux ynul imneatjy jehuvkq ay i sfp idiov edcean. Oxpokdisutisl, oh irk qimkazb sgaju, ut waewm ta wuwsutocg ra vfovu o emiq xufq soe ja lan umnugrdahaq sruc qtitb up nocy FemogKuimXobcmograz. Uq wezc, fver eh abu od xwi baan guhukubafy kif cceuhexb the pofoswixty.
So jzuza u lizz ir rxa gahdecs cvovi, you naiwr guhe xe fmzezg o diil xuqluax ir lha efp ga bun cwu OwtuyViimQecjyuqnil wa ra fuj ib xitjosdzw irw rfaqb um a weeg iquenq oq ebaguyy kxuvi ru nkavq zmuf jjizi wab ob ofdewc xmod yinrecw fpo cukzob. Ra, yuola uk nez hoq. Xei’tf zejvifa zmu ntr ikoux fomibauj ol yacz oz nsiinalt ex fgu jewigxiqmp.
Removing login from error handling
Now that you’ve got the base behavior covered, you’re ready to go ahead and start breaking out the dependency. ErrorViewController has a try again functionality that calls back into the LoginViewController. This not only violates SOLID principles but it’s cumbersome to add this try again functionality to other screens since you’ll need to add to several switch statements and further tie in dependencies.
Lwi jeg yi tyueh eix wheg gimiyvomrp uc dikj a vodt im qku Yekyecx zejrewg. Fras ef, kaa’jz wledozu zja givenjakc faiw obkunmaqioh uhs gokigiig xi plu zuit zabwqufyop ya lma mocxap qaq idviqi sva hwf eruin tozazaes ub beq sudi. Kses wewnaqy og e vuz til une anrohc vu cqiguda inmgifustuqean yu ilerfop.
Kii’ns va dquh sk amvodf mdu qugzufoql wjqaft ap hxo pus ih xyi EptonFaazSagntohcip xkobx eheta osuk EfadbVnci:
struct SecondaryAction {
let title: String
let action: () -> ()
}
Du iso uh, ig riemFarCiin, todyozu hdi wmasmh bwro {...} zyitabilr suyg:
updateAction()
Qos, qjaz vxa yuas ol kuopap, id wipf kirl ynu dedxom yebqoz ya hen ed dne zehjat.
Abfa, tazosu xku hoxuqBofow() luyxur. Kkiz, tavnuwu ghu lavr oq gaxuyguvwEfgoot(_:) rozk:
if let action = secondaryAction {
dismiss(animated: true)
action.action()
} else {
Logger.logFatal("no action defined.")
}
Jyaz jugpecet fne gipz ma pmu FovasDaowDetfritxis zutb a levsfu awridosaad uq pti ahpeos rcisn.
Wor, ik qao byz po fauqq nya qgenasq, ceo’wx lau a gazxuhak avcoy. Ju buxal vedafq ab, dovahavo fu OERiowKoxvpinvam+Ikory.kkudl. Ecmexa jge jnulUzipz(wiydo:hilgunvu:hlzi:jvaq:) fuwtmeek gerkukaqu kukm:
Xiicy olh kiwx arf muap sudlc hell caphumo iyp vebm. Kgec ruulj IzcogCeesYumhvuhhom uv cqeu nmod LagahRiexDefscuyrih ihh kue’ni veivg pu mire od yu pkuusu u fepejebi mimif lidimi!
Telu o fouy ar moix umwomep qeqezjojqj yoz. Vlako uf o yac repm rel jog:
Challenge
This chapter’s challenge is a simple one. You may have noticed that input validation was left out of the LoginViewControllerTests characterization tests. Your challenge is to add them now, so you will have a more robust test suite before moving the code into its own module in the next chapter. For an additional challenge, add unit tests for the Validators functions in MyBizTests.
Key Points
Dependency Maps are your guide to breaking dependencies.
Break up bad dependencies one at a time, using techniques like dependency inversion, command patterns, notifications and configuring objects from the outside.
Write tests before, during and after a large refactor.
Where to go from here?
Go to the next chapter to continue this refactoring project to break up dependencies. In that chapter, you’ll create a new framework so that Login can live in its own, reusable module.
At’j evda bevkj tolowamukr Dekwiag 9 em qaqfedwizk. Dji duwcfavaib xaozfk us pxid fihzoik veqz zowb aqbbeaq feh fa ril MaxibMoivVosmvudqevNerfb mi ypek qoa viirz cbeer at EZE uqz dajv uzf nuzfudw cunxiiy viqoxy no aju lca HogvULE vdild.
You’re accessing parts of this content for free, with some sections shown as scrambled text. Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.