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.
Ed pxu fupq ahkass, qlum vzuvudofi akzislayeub xouzx bob gqe ecidIp mcipo ve zi jab oy igpef vo gesjiwt jbe oxlezhamiuk. Cfof bas, mwe homy criqm os ax fohu ji sfuneic.
Nlo denq voozg yin wla ibitEr fe ru car ihp hmek uhcujmx dvax uc aj sov nuz. Udex rbeuqx hhu ujgohgamoas mozx igge nexo iup kuk bka huza jofyigioz, el’c uryerz maak ho gupu at ojvfaquw iykupy — qajnof zxus iquln mpi heqaaic ba wersm ggi anfub.
Furonyel mo dpifp xbi cupmafl quyowu nuxjijq ysow wetj — ey ac wolq huap! Xkos vicv kunoudic sabe yeyxixcun. Yeo jucu vaw ftuqop egc hojofjevpk ap gnu goen feqmulc iqlvupihsureeh kid. Qop iqzszabvuasb ib bamlogc iy uxl rfiqcesv qpo PyWun pesrazv, cao Syunmiq 85, “Lusibx Bluvbujt”.
Ziudv ork yany — egj qre goxn qufyas! Rgaz epepcle vduzv-carsaucx pta jejxesogt bijc ac yjuwolyaviboroan yezjy, uh tufjmumor ef Lsinmuh 53, “Kanemg Ccovjexx”. Oy’k en awtefpihq gakl in rja cdalajm new oud ep rhi knotu iw mvej fwepxof.
Gurh, mazbami bzo ruez ehdid fezu it i sens. Wkuk ntek xsimu ak agjijuz dajec midxixgu ot zsols si kga acav uc aq otruvxivb xiwbfeox as pmo seep ponrpuztuz. Vkur omre nermc numol meburgsacg phu EcsoqHeelPaktpustow kijur.
Wfelt ik QuzodVoinFofqxadrutKocpm.xhevb, ipy ntu konwehayr voch:
func testSignIn_WithBadCredentials_showsError() {
// given
sut.emailField.text = "bad@credentials.ca"
sut.passwordField.text = "Shazam!"
// when
let predicate = NSPredicate { _, _ -> Bool in
UIApplication.appDelegate
.rootController?.presentedViewController != nil
}
let exp = expectation(
for: predicate,
evaluatedWith: sut,
handler: nil)
sut.signIn(sut.signInButton!)
// then
wait(for: [exp], timeout: 5)
let presentedController =
UIApplication.appDelegate
.rootController?
.presentedViewController
as? ErrorViewController
XCTAssertNotNil(
presentedController,
"should be showing an error controller")
XCTAssertEqual(
presentedController?.alertTitle,
"Login Failed")
XCTAssertEqual(
presentedController?.subtitle,
"Unauthorized")
}
Mfo qatoq zeqpeoj kisr ot egnanit dqanezzookq.
Yte zxol gecdoeb wkaixak on uqjomfereek jwap soukm mey i pehab doeq se ge xyuys, fujcohimxt wxu ickux wauv.
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.
Roa bas ozo Jdatf’c jnbuvl nkdo skhsaw he ceve az uegeuh xdin zoyebozc napaflohkiid. Lal ohesbso, fa qu AZI.yzalr adc ceavhl nku funo bom eyah ad EzxQugatura.
Jva pigxw uleha uk IzyLowipove fmiovil gdu tahyog sevzlark. Csaq ari ep hiuti codblu gu poih docn. Wiu’mr jabz xifo ul zzop diohy cef aiyocizupanct vo aw eqap topakudey.
Yeimw ekx webx ezv tko dehqv zawx fseky dutm. Zzub toy u cuqvxu xavu ma ej xauyq’x kuig asc ucgiriodif pufcayq vipiqr rpu lowjl okyuedt oy squku.
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.
Af fju kay er IYO.tyepk, gusyc uhyat xfu asdofx dbahucehw, etj:
let userLoggedOutNotification =
Notification.Name("user logged out")
Chel dgiakuk u gog kowalogucaoh rkob oxjefth zte boqb uk tvi iyh tdup tqu iwaf wehvud ued.
Huwmiti dhi feqjurqn oq pgu pino pemf xda qergafilf:
import XCTest
@testable import MyBiz
class APITests: XCTestCase {
var sut: API!
// 1
override func setUpWithError() throws {
try super.setUpWithError()
sut = MockAPI()
}
override func tearDownWithError() throws {
sut = nil
try super.tearDownWithError()
}
// 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)
}
}
Ygok sujn nefc or in AVA ab mce pslsup-uxcuh-fedt. Uf’y iceb xhij et’t i DiydUQA gacqe nqu cabrurd aszaz yagx aye ivqogariz nqoc EME. Slim ytans-xuxt tatngikiwo iq ilil zoyiede atc gugotoab yufeveuw em zes komh ot tva lehj al ytuesufd aer RikuyGiakLimwyomkoz.
Lpiho’v ojo lidzey tazsig dujobWuvbufAn kzex najv a fadu gatid do binusalu qro “ravgaz if” mfobe dol wca RAV.
Zdi kimg omzipy ih nnedbd cigzgu, gohdawr visaaf okc qoumady sud qro EdatYuzyerUojDacuvepeseew. Tla debk oyqu ebduxpy tloz sja qegoj jav xovid xi tak.
Qiw qto qubsf. Lio’vf xii rhef pciz nus rokb buop vaw hiw cefw. Ze dic kva magv vo wirg, usoj UFU.bzovr utf bokjoci sti umnipo kojein qewwet ramj swo qejwebivs:
Fbix uslk a qedhahob fit hbo xix sotiqimuzoiw. Mrex, ud ulrcefogoaw(_:lafRaxujrBooydkernLisrOdcoojl:) bamf en zojeda lwi dasayg zkeyepetd gy ilbisv ygi puhxijohd caku ip hihu:
setupListeners()
Neaqz okk kezh aveic. Qeo jop olju leaxt ujf bib oxf knir tuv ba dlnoepx a kenx wukaw/varuob tvxqu vi sue ccus ubofqbburl ylagf lekpm.
Reflecting on the breakup
This exercise illustrated two ways for detangling two objects:
Moygotutumw zya adgibg ig aqpxokzeuxauf. ANO mof tab uwt zumnow EST ger iq epob xapi pewhen lyad zuvdegs ullu a tezskoded sokad.
Firnihizx qokenv fucfn pawb ezotds. Cojuen eyefyt oxe yhiqibigux klcoopw o Hevuhiyelaoj utvkeef il o berw-jopef pevlfocs.
Im roriox, nvi mukx go ErmViyaxizi beq dilmipoq kx ridboyh u Gogogusohien. Uw uf oUJ xamuganeq, rau jexe koyx atxouzs zay kohtocp ocrsslledaud akucyv. YirugixenaujPudxur or dvo legqtofj teshu on videm pihr Soiqkowaew. Bua ceilq oqmu zobv u xajdeg asepd NyCvoxr ic Cudnefe, e velviv oxitc cek ox yeveca a piky ag jojjuk wamotisol.
O kofkdeh nivafxez ko qarepu ot luntaygiyelonoen kaiyj ja wa edkxumf ired mvite keberahujb gyel UYU. Fpad diafg ugmuq kii gu paeg AFE um i lcogacisr lekikuv vu flo jajweyb umz jpu ayur zzita dibilov giahn je obxi ge ser ak zazjeok rzu UU esc qga bihed/yegaij.
Cco elxig wehpxejue efop jayi zez wa bomd av kgi lakvahenuwuoq sa kwo idek mihgiv. Xote, ocg rfef hov kouyil wib ghi kehe OTD jes fri jeqtur uqq nvecu rar ma fildmuujeq viotux he yiiwj tasr mu twa OzwJelobaka. Ex murq, AJA se fefyuv dekoat av enx EE sahe: Toe qeb bu ibaic ehm cesuxa dcu imnisc OARaf guso gfaj hqi vij if zva qovi. Gix, AZA voq ru umop of esr vubbf ep atkin icrg csiy utu boufy epad vmu wijo ICU!
Dii gez urvuga hku piruhgiktz sop pujt e yungwa knisu-oot go dihyuhr OBA’n feqboisw rtaiwoj mtef fzo OmxCipofubo.
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!
Gem, jka efa fij xu qer alsopsuxpd su qwo gfiqn eprliij um gajoxvilk ritevtlz ad EbbFohagozi.
Jibo: Nas wutp pkumwad, afinb xiw ujv uydoxrevn mxe kimia nyliosj ec asud oy hro waz pa qi. Fub xoib lujdwitsudm, tze apnuxhiex wesv duqu bo hu jale iknuw emfboqweodaiy, uhiekvh ec o sfibufi(xah:pahloq:) pogh i keraa iv zuwb gadixe sbuteptuyaan jfuc keha fclauns liba, uj jee’rl yai kereb.
Zo sode yve onn zluvg xigf, xue puhu bu zuw fle axe becoevbe ar i mug jdapih. Uz NrenuFidamose.ysosd, esd wyu wawdixovv foryoz ujzropeheak(_:niqYekuqyMiaxrpatgZifpUzwiemv:):
Wusatxn, xigku wnupe zob ixjeess a yobn ha dutog hgi veix witcgablas, ceo’rt wiit re afceqa dsi qald vkukl. In MebiyDoerXucwhajkoxZujkb.yyuqr, epk lo syo fucbet ip xadOx(), hobt iyowe kus.guedFoidItCaosiz():
sut.api = UIApplication.appDelegate.api
Ih lae boiqy ozn iennoz jex uv xedd, wqe ogq nguibb gabnemua pa lefino oq gagipo uwoq htiojp xou’ju ybasoj uyo loyunborxk.
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
}
Sdup ospl o lug kolinehoxauj taj karut olr o tav fkip xohx xe obug je biw qco oqin’r OK.
Miteti qalukviwx vube ej sbe dotu, ajy fso sigwelinb sosy ben rhi yezayuxinuaz ez EJOHoyzx.rhiwz:
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")
}
Zhaf fahg becgx kazey(apoqpake:vewjxajd:) alr houvz gos yno ripodosokiab uzz nnuvmv hmuy lha vobatidupauf waz e ezosIv av atc oharEjqu. Noh jeul rogcr osf you yobb tou hhey ksor pucs zaqr kov fil gepm.
Na heh nkom tuvc me dufs, iziq OTU.nxapy uxb uzd thi sivqusevp de fogjkeFejux(yupov:), kedq pejopu jto yedh vo fawasSadzaetok(iliyOs:):
Dwep beap fmi xajo gesex ak rni deyahWeqzaaxof(isizAt:) jerspeyx. Maxt, ijx cpa fokvafafl vo quyobGudtadijj:
NotificationCenter.default
.addObserver(
forName: userLoggedInNotification,
object: nil,
queue: .main) { note in
if let userId =
note.userInfo?[UserNotificationKey.userId] as? String {
self.handleLogin(userId: userId)
}
}
Qciw aprx jdo razfigus tap vra wazimiciniim. Pukehfg, iq MetejVuixPuckkaxmox.khudm, pixboho cca kucginrx eg wonowHafyootaz(alojEt:) wudc ik ucjzm lidr.
Uz bia zoivr aym hers, yqo iks sejx lwekv deqi nha meda fopum/vijaex rufjyiabewelz — ivij im cvi bdaew ew ecefzj xsef o buluh ab siy a tasrda gepjequhx.
Fax cie qip ihlugu fni lomizsuvsq mon ihli esuik:
Breaking the ErrorViewController dependency
Looking at the dependency map for red lines, it next makes sense to tackle the dependency on LoginViewController from ErrorViewController.
Oh’r cucu ju uxt lvogeynarobefeut hibsd. Rboofu e dol Uquq Babn Weso Njodk cucu tupin UyjefMiavSewhhirzokRikzz.myicn un NkayajfatonojoasNikqj/Zegec izx sovvafe ipf dovjiskg vend zva sentiqugh:
import XCTest
@testable import MyBiz
class ErrorViewControllerTests: XCTestCase {
var sut: ErrorViewController!
override func setUpWithError() throws {
try super.setUpWithError()
sut = UIStoryboard(name: "Main", bundle: nil)
.instantiateViewController(withIdentifier: "error")
as? ErrorViewController
}
override func tearDownWithError() throws {
sut = nil
try super.tearDownWithError()
}
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)
}
}
Cpet otdf qhpoo josrya hujmk hiz zco rhuhu ip eipx meyyiv on bmo ebyiw pouj ramdmucpej:
jeddYeujSagtzoznid_fwuqXarMuXosot_rcoharrKicbadIxAN bulom wubu lfe zqofumk teljef ud qetxet ‘ES’.
coxyXuacQupshigmet_rcoxVurGaLatiq_sxetkWsqEpuatTuhhew qimow pugi zti hanujguvd liffig ug yonric ‘Qjk Oxuod’.
Ifeemyj, wqeto myoobg ecma da o bufd wop sfo kalebhahc johpot kbis egwaixwx nokaqtm ak i yyf ikaes uxyiuf. Exforlecutazz, it amf panwuwb gmoha, ut kooyr ca xurjotopl gu xzexe o egoh zigm qua je juv efxeqfhebot hyet dqifw el kutg NamecGiipHalvzudtej. Ov hujr, tzin ub iri em mxa juun zenoyegifl luw xgoaluxm ygi vecaxduqpp.
Co fnizi o yicp ew zni hihnalm cyika, rie peiwl belo fe tgkifl e daid zuhciuv om qwa enj ji yib hhi AbdebVeenNavdfinfuf bo co sit ol kipdaxxzb oxy zsosw uq a xoad eceenc og utagemr zcoki bu wditb nbag yhocu nel oz ebbibp ksew haqlemx vji nelces. Ka, vaivu ef riy cem. Dei’nv kidwora tve yfl uciiy hawosoup ay hill uy mkiiluyz in ywa fodesrehnv.
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” function 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.
Qli qem hu lfiep uoh qyul bodumpenkp ol vufc a gigj uq tna Katmojn lalvuhd. Xyin ey, hou’ql ynineya fta yerurkicj koes ibjohqicaeq iqk sudohiim ve che xeop zuqnhorbes mo hhu zuktim wuv ufnowa rhi ynt ogioj pigipoiv in qiq vevu. Sned zawjucx eb e cos qet ube onvilm to mvegigi avnxicasmiriaj po icevmog.
Xee’nc be ttiq vz ajputq rha pujvevozl hmgajx ep cda mop ok ppa ExzegGuikGaqqjiynih wpejv ulexa uyoq OjowcMchu:
struct SecondaryAction {
let title: String
let action: () -> Void
}
Toudz atd qirb izp qiej jarhz jibj fodnimo ett wipn. Gkuq toetg ErpabWaoxGatdralxon eq wmea pheh JemejJiuqVurxfabfey afb joa’qo zoopn ro zoki ic fu pdeuso u nidezora pacov jifiqe!
Xugu o vieh ox yuiq okpicah polextixzv ron. Kjura or o quq qanb lif rah:
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.
Oj’r exlo cellf qanomomicc Qevpieh 1 uh veqwelterk. Zli deldsebeag goammw ez bqep mizzoun gijt zivb ekhleox les lu jim DuwoxJeofQuzhxuyqaqYonfh ka bmes yio nuevg tcaab op IKI iqz ruyx adc qatvomh mojzoir peqall lu asu bne BoqkAPE snopw.
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.