CINXE.COM
<!doctype html><html lang="en"><head><title data-rh="true">Better Android Testing at Airbnb — Part 4: Testing ViewModels | by Eli Hart | The Airbnb Tech Blog | Medium</title><meta data-rh="true" charset="utf-8"/><meta data-rh="true" name="viewport" content="width=device-width,minimum-scale=1,initial-scale=1,maximum-scale=1"/><meta data-rh="true" name="theme-color" content="#000000"/><meta data-rh="true" name="twitter:app:name:iphone" content="Medium"/><meta data-rh="true" name="twitter:app:id:iphone" content="828256236"/><meta data-rh="true" property="al:ios:app_name" content="Medium"/><meta data-rh="true" property="al:ios:app_store_id" content="828256236"/><meta data-rh="true" property="al:android:package" content="com.medium.reader"/><meta data-rh="true" property="fb:app_id" content="542599432471018"/><meta data-rh="true" property="og:site_name" content="Medium"/><meta data-rh="true" property="og:type" content="article"/><meta data-rh="true" property="article:published_time" content="2021-07-12T20:10:30.921Z"/><meta data-rh="true" name="title" content="Better Android Testing at Airbnb — Part 4: Testing ViewModels | by Eli Hart | The Airbnb Tech Blog | Medium"/><meta data-rh="true" property="og:title" content="Better Android Testing at Airbnb, Part 4"/><meta data-rh="true" property="al:android:url" content="medium://p/550d929126c8"/><meta data-rh="true" property="al:ios:url" content="medium://p/550d929126c8"/><meta data-rh="true" property="al:android:app_name" content="Medium"/><meta data-rh="true" name="description" content="In the fourth installment of our series on Android Testing at Airbnb, we look at a framework for unit testing logic in ViewModels."/><meta data-rh="true" property="og:description" content="Testing ViewModels"/><meta data-rh="true" property="og:url" content="https://medium.com/airbnb-engineering/better-android-testing-at-airbnb-part-4-testing-viewmodels-550d929126c8"/><meta data-rh="true" property="al:web:url" content="https://medium.com/airbnb-engineering/better-android-testing-at-airbnb-part-4-testing-viewmodels-550d929126c8"/><meta data-rh="true" property="og:image" content="https://miro.medium.com/v2/resize:fit:1200/1*SzUfC_eUHmH-O9CZwUDMtQ.jpeg"/><meta data-rh="true" property="article:author" content="https://medium.com/@konakid"/><meta data-rh="true" name="author" content="Eli Hart"/><meta data-rh="true" name="robots" content="index,noarchive,follow,max-image-preview:large"/><meta data-rh="true" name="referrer" content="unsafe-url"/><meta data-rh="true" property="twitter:title" content="Better Android Testing at Airbnb, Part 4"/><meta data-rh="true" name="twitter:site" content="@AirbnbEng"/><meta data-rh="true" name="twitter:app:url:iphone" content="medium://p/550d929126c8"/><meta data-rh="true" property="twitter:description" content="Testing ViewModels"/><meta data-rh="true" name="twitter:image:src" content="https://miro.medium.com/v2/resize:fit:1200/1*SzUfC_eUHmH-O9CZwUDMtQ.jpeg"/><meta data-rh="true" name="twitter:card" content="summary_large_image"/><meta data-rh="true" name="twitter:label1" content="Reading time"/><meta data-rh="true" name="twitter:data1" content="7 min read"/><link data-rh="true" rel="icon" href="https://miro.medium.com/v2/5d8de952517e8160e40ef9841c781cdc14a5db313057fa3c3de41c6f5b494b19"/><link data-rh="true" rel="search" type="application/opensearchdescription+xml" title="Medium" href="/osd.xml"/><link data-rh="true" rel="apple-touch-icon" sizes="152x152" href="https://miro.medium.com/v2/resize:fill:304:304/10fd5c419ac61637245384e7099e131627900034828f4f386bdaa47a74eae156"/><link data-rh="true" rel="apple-touch-icon" sizes="120x120" href="https://miro.medium.com/v2/resize:fill:240:240/10fd5c419ac61637245384e7099e131627900034828f4f386bdaa47a74eae156"/><link data-rh="true" rel="apple-touch-icon" sizes="76x76" href="https://miro.medium.com/v2/resize:fill:152:152/10fd5c419ac61637245384e7099e131627900034828f4f386bdaa47a74eae156"/><link data-rh="true" rel="apple-touch-icon" sizes="60x60" href="https://miro.medium.com/v2/resize:fill:120:120/10fd5c419ac61637245384e7099e131627900034828f4f386bdaa47a74eae156"/><link data-rh="true" rel="mask-icon" href="https://miro.medium.com/v2/resize:fill:1000:1000/7*GAOKVe--MXbEJmV9230oOQ.png" color="#171717"/><link data-rh="true" rel="preconnect" href="https://glyph.medium.com" crossOrigin=""/><link data-rh="true" id="glyph_preload_link" rel="preload" as="style" type="text/css" href="https://glyph.medium.com/css/unbound.css"/><link data-rh="true" id="glyph_link" rel="stylesheet" type="text/css" href="https://glyph.medium.com/css/unbound.css"/><link data-rh="true" rel="author" href="https://medium.com/@konakid"/><link data-rh="true" rel="canonical" href="https://medium.com/airbnb-engineering/better-android-testing-at-airbnb-part-4-testing-viewmodels-550d929126c8"/><link data-rh="true" rel="alternate" href="android-app://com.medium.reader/https/medium.com/p/550d929126c8"/><script data-rh="true" type="application/ld+json">{"@context":"http:\u002F\u002Fschema.org","@type":"NewsArticle","image":["https:\u002F\u002Fmiro.medium.com\u002Fv2\u002Fresize:fit:1200\u002F1*SzUfC_eUHmH-O9CZwUDMtQ.jpeg"],"url":"https:\u002F\u002Fmedium.com\u002Fairbnb-engineering\u002Fbetter-android-testing-at-airbnb-part-4-testing-viewmodels-550d929126c8","dateCreated":"2019-12-13T18:39:41.583Z","datePublished":"2019-12-13T18:39:41.583Z","dateModified":"2021-12-12T22:42:50.993Z","headline":"Better Android Testing at Airbnb — Part 4: Testing ViewModels","name":"Better Android Testing at Airbnb — Part 4: Testing ViewModels","description":"In the fourth installment of our series on Android Testing at Airbnb, we look at a framework for unit testing logic in ViewModels.","identifier":"550d929126c8","author":{"@type":"Person","name":"Eli Hart","url":"https:\u002F\u002Fmedium.com\u002F@konakid"},"creator":["Eli Hart"],"publisher":{"@type":"Organization","name":"The Airbnb Tech Blog","url":"https:\u002F\u002Fmedium.com\u002Fairbnb-engineering","logo":{"@type":"ImageObject","width":60,"height":60,"url":"https:\u002F\u002Fmiro.medium.com\u002Fv2\u002Fresize:fit:120\u002F1*JZl-TXoSiG0VmYn3qWLdTA.png"}},"mainEntityOfPage":"https:\u002F\u002Fmedium.com\u002Fairbnb-engineering\u002Fbetter-android-testing-at-airbnb-part-4-testing-viewmodels-550d929126c8"}</script><style type="text/css" data-fela-rehydration="540" data-fela-type="STATIC">html{box-sizing:border-box;-webkit-text-size-adjust:100%}*, *:before, *:after{box-sizing:inherit}body{margin:0;padding:0;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased;color:rgba(0,0,0,0.8);position:relative;min-height:100vh}h1, h2, h3, h4, h5, h6, dl, dd, ol, ul, menu, figure, blockquote, p, pre, form{margin:0}menu, ol, ul{padding:0;list-style:none;list-style-image:none}main{display:block}a{color:inherit;text-decoration:none}a, button, input{-webkit-tap-highlight-color:transparent}img, svg{vertical-align:middle}button{background:transparent;overflow:visible}button, input, optgroup, select, textarea{margin:0}:root{--reach-tabs:1;--reach-menu-button:1}#speechify-root{font-family:Sohne, sans-serif}div[data-popper-reference-hidden="true"]{visibility:hidden;pointer-events:none}.grecaptcha-badge{visibility:hidden} /*XCode style (c) Angel Garcia <angelgarcia.mail@gmail.com>*/.hljs {background: #fff;color: black; }/* Gray DOCTYPE selectors like WebKit */ .xml .hljs-meta {color: #c0c0c0; }.hljs-comment, .hljs-quote {color: #007400; }.hljs-tag, .hljs-attribute, .hljs-keyword, .hljs-selector-tag, .hljs-literal, .hljs-name {color: #aa0d91; }.hljs-variable, .hljs-template-variable {color: #3F6E74; }.hljs-code, .hljs-string, .hljs-meta .hljs-string {color: #c41a16; }.hljs-regexp, .hljs-link {color: #0E0EFF; }.hljs-title, .hljs-symbol, .hljs-bullet, .hljs-number {color: #1c00cf; }.hljs-section, .hljs-meta {color: #643820; }.hljs-title.class_, .hljs-class .hljs-title, .hljs-type, .hljs-built_in, .hljs-params {color: #5c2699; }.hljs-attr {color: #836C28; }.hljs-subst {color: #000; }.hljs-formula {background-color: #eee;font-style: italic; }.hljs-addition {background-color: #baeeba; }.hljs-deletion {background-color: #ffc8bd; }.hljs-selector-id, .hljs-selector-class {color: #9b703f; }.hljs-doctag, .hljs-strong {font-weight: bold; }.hljs-emphasis {font-style: italic; } </style><style type="text/css" data-fela-rehydration="540" data-fela-type="KEYFRAME">@-webkit-keyframes k1{0%{opacity:0.8}50%{opacity:0.5}100%{opacity:0.8}}@-moz-keyframes k1{0%{opacity:0.8}50%{opacity:0.5}100%{opacity:0.8}}@keyframes k1{0%{opacity:0.8}50%{opacity:0.5}100%{opacity:0.8}}</style><style type="text/css" data-fela-rehydration="540" data-fela-type="RULE">.a{font-family:medium-content-sans-serif-font, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif}.b{font-weight:400}.c{background-color:rgba(255, 255, 255, 1)}.l{display:block}.m{position:sticky}.n{top:0}.o{z-index:500}.p{padding:0 24px}.q{align-items:center}.r{border-bottom:solid 1px #F2F2F2}.y{height:41px}.z{line-height:20px}.ab{display:flex}.ac{height:57px}.ae{flex:1 0 auto}.af{color:inherit}.ag{fill:inherit}.ah{font-size:inherit}.ai{border:inherit}.aj{font-family:inherit}.ak{letter-spacing:inherit}.al{font-weight:inherit}.am{padding:0}.an{margin:0}.ao{cursor:pointer}.ap:disabled{cursor:not-allowed}.aq:disabled{color:#6B6B6B}.ar:disabled{fill:#6B6B6B}.au{width:auto}.av path{fill:#242424}.aw{height:25px}.ax{margin-left:16px}.ay{border:none}.az{border-radius:20px}.ba{width:240px}.bb{background:#F9F9F9}.bc path{fill:#6B6B6B}.be{outline:none}.bf{font-family:sohne, "Helvetica Neue", Helvetica, Arial, sans-serif}.bg{font-size:14px}.bh{width:100%}.bi{padding:10px 20px 10px 0}.bj{background-color:transparent}.bk{color:#242424}.bl::placeholder{color:#6B6B6B}.bm{display:inline-block}.bn{margin-left:12px}.bo{margin-right:12px}.bp{border-radius:4px}.bq{margin-left:24px}.br{height:24px}.bx{background-color:#F9F9F9}.by{border-radius:50%}.bz{height:32px}.ca{width:32px}.cb{justify-content:center}.ch{max-width:680px}.ci{min-width:0}.cj{animation:k1 1.2s ease-in-out infinite}.ck{height:100vh}.cl{margin-bottom:16px}.cm{margin-top:48px}.cn{align-items:flex-start}.co{flex-direction:column}.cp{justify-content:space-between}.cq{margin-bottom:24px}.cw{width:80%}.cx{background-color:#F2F2F2}.dd{height:44px}.de{width:44px}.df{margin:auto 0}.dg{margin-bottom:4px}.dh{height:16px}.di{width:120px}.dj{width:80px}.dp{margin-bottom:8px}.dq{width:96%}.dr{width:98%}.ds{width:81%}.dt{margin-left:8px}.du{color:#6B6B6B}.dv{font-size:13px}.dw{height:100%}.ep{color:#FFFFFF}.eq{fill:#FFFFFF}.er{background:rgba(48, 150, 154, 1)}.es{border-color:rgba(48, 150, 154, 1)}.ew:disabled{cursor:inherit !important}.ex:disabled{opacity:0.3}.ey:disabled:hover{background:rgba(48, 150, 154, 1)}.ez:disabled:hover{border-color:rgba(48, 150, 154, 1)}.fa{border-radius:99em}.fb{border-width:1px}.fc{border-style:solid}.fd{box-sizing:border-box}.fe{text-decoration:none}.ff{text-align:center}.fi{margin-right:32px}.fj{position:relative}.fk{fill:#6B6B6B}.fn{background:transparent}.fo svg{margin-left:4px}.fp svg{fill:#6B6B6B}.fr{box-shadow:inset 0 0 0 1px rgba(0, 0, 0, 0.05)}.fs{position:absolute}.fz{margin:0 24px}.gd{background:rgba(255, 255, 255, 1)}.ge{border:1px solid #F2F2F2}.gf{box-shadow:0 1px 4px #F2F2F2}.gg{max-height:100vh}.gh{overflow-y:auto}.gi{left:0}.gj{top:calc(100vh + 100px)}.gk{bottom:calc(100vh + 100px)}.gl{width:10px}.gm{pointer-events:none}.gn{word-break:break-word}.go{word-wrap:break-word}.gp:after{display:block}.gq:after{content:""}.gr:after{clear:both}.gs{line-height:1.23}.gt{letter-spacing:0}.gu{font-style:normal}.gv{font-weight:700}.ia{align-items:baseline}.ib{width:48px}.ic{height:48px}.id{border:2px solid rgba(255, 255, 255, 1)}.ie{z-index:0}.if{box-shadow:none}.ig{border:1px solid rgba(0, 0, 0, 0.05)}.ih{margin-left:-12px}.ii{width:28px}.ij{height:28px}.ik{z-index:1}.il{width:24px}.im{margin-bottom:2px}.in{flex-wrap:nowrap}.io{font-size:16px}.ip{line-height:24px}.ir{margin:0 8px}.is{display:inline}.it{color:rgba(48, 150, 154, 1)}.iu{fill:rgba(48, 150, 154, 1)}.ix{flex:0 0 auto}.ja{flex-wrap:wrap}.jd{white-space:pre-wrap}.je{margin-right:4px}.jf{overflow:hidden}.jg{max-height:20px}.jh{text-overflow:ellipsis}.ji{display:-webkit-box}.jj{-webkit-line-clamp:1}.jk{-webkit-box-orient:vertical}.jl{word-break:break-all}.jn{padding-left:8px}.jo{padding-right:8px}.kp> *{flex-shrink:0}.kq{overflow-x:scroll}.kr::-webkit-scrollbar{display:none}.ks{scrollbar-width:none}.kt{-ms-overflow-style:none}.ku{width:74px}.kv{flex-direction:row}.kw{z-index:2}.kz{-webkit-user-select:none}.la{border:0}.lb{fill:rgba(117, 117, 117, 1)}.le{outline:0}.lf{user-select:none}.lg> svg{pointer-events:none}.lp{cursor:progress}.lq{opacity:1}.lr{padding:4px 0}.lu{margin-top:0px}.lv{width:16px}.lx{display:inline-flex}.md{max-width:100%}.me{padding:8px 2px}.mf svg{color:#6B6B6B}.mw{line-height:1.58}.mx{letter-spacing:-0.004em}.my{font-family:source-serif-pro, Georgia, Cambria, "Times New Roman", Times, serif}.nt{margin-bottom:-0.46em}.nu{font-style:italic}.nv{clear:both}.ob{height:auto}.oc{text-decoration:underline}.od{line-height:1.12}.oe{letter-spacing:-0.022em}.of{font-weight:600}.pa{margin-bottom:-0.28em}.pg{list-style-type:disc}.ph{margin-left:30px}.pi{padding-left:0px}.po{margin:auto}.pp{padding-bottom:100%}.pq{height:0}.pr{list-style-type:decimal}.ps{line-height:1.18}.qg{margin-bottom:-0.31em}.qh{margin-bottom:26px}.qi{margin-top:6px}.qj{margin-top:8px}.qk{margin-right:8px}.ql{padding:8px 16px}.qm{border-radius:100px}.qn{transition:background 300ms ease}.qp{white-space:nowrap}.qq{border-top:none}.qr{margin-bottom:14px}.qs{height:52px}.qt{max-height:52px}.qu{box-sizing:content-box}.qv{position:static}.qx{max-width:155px}.rd{margin-right:20px}.rj{height:0px}.rk{margin-bottom:40px}.rl{margin-bottom:48px}.rz{border-radius:2px}.sb{height:64px}.sc{width:64px}.sd{align-self:flex-end}.se{flex:1 1 auto}.sk{padding-right:4px}.sl{font-weight:500}.ss{margin-top:16px}.st{color:rgba(255, 255, 255, 1)}.su{fill:rgba(255, 255, 255, 1)}.sv{background:rgba(25, 25, 25, 1)}.sw{border-color:rgba(25, 25, 25, 1)}.sz:disabled{opacity:0.1}.ta:disabled:hover{background:rgba(25, 25, 25, 1)}.tb:disabled:hover{border-color:rgba(25, 25, 25, 1)}.tc{margin-bottom:54px}.ti{gap:18px}.tj{fill:rgba(61, 61, 61, 1)}.tq{border-bottom:solid 1px #E5E5E5}.tr{margin-top:72px}.ts{padding:24px 0}.tt{margin-bottom:0px}.tu{margin-right:16px}.as:hover:not(:disabled){color:rgba(25, 25, 25, 1)}.at:hover:not(:disabled){fill:rgba(25, 25, 25, 1)}.et:hover{background:rgba(51, 128, 131, 1)}.eu:hover{border-color:rgba(51, 128, 131, 1)}.ev:hover{cursor:pointer}.fl:hover{color:#242424}.fm:hover{fill:#242424}.fq:hover svg{fill:#242424}.ft:hover{background-color:rgba(0, 0, 0, 0.1)}.iq:hover{text-decoration:underline}.iv:hover:not(:disabled){color:rgba(51, 128, 131, 1)}.iw:hover:not(:disabled){fill:rgba(51, 128, 131, 1)}.ld:hover{fill:rgba(8, 8, 8, 1)}.ls:hover{fill:#000000}.lt:hover p{color:#000000}.lw:hover{color:#000000}.mg:hover svg{color:#000000}.qo:hover{background-color:#F2F2F2}.sa:hover{background-color:none}.sx:hover{background:#000000}.sy:hover{border-color:#242424}.tk:hover{fill:rgba(25, 25, 25, 1)}.bd:focus-within path{fill:#242424}.lc:focus{fill:rgba(8, 8, 8, 1)}.mh:focus svg{color:#000000}.lh:active{border-style:none}</style><style type="text/css" data-fela-rehydration="540" data-fela-type="RULE" media="all and (min-width: 1080px)">.d{display:none}.bw{width:64px}.cg{margin:0 64px}.cv{height:48px}.dc{margin-bottom:52px}.do{margin-bottom:48px}.ef{font-size:14px}.eg{line-height:20px}.em{font-size:13px}.eo{padding:5px 12px}.fh{display:flex}.fy{margin-bottom:68px}.gc{max-width:680px}.hq{font-size:42px}.hr{margin-top:1.19em}.hs{margin-bottom:32px}.ht{line-height:52px}.hu{letter-spacing:-0.011em}.hz{align-items:center}.kb{border-top:solid 1px #F2F2F2}.kc{border-bottom:solid 1px #F2F2F2}.kd{margin:32px 0 0}.ke{padding:3px 8px}.kn> *{margin-right:24px}.ko> :last-child{margin-right:0}.lo{margin-top:0px}.mc{margin:0}.np{font-size:20px}.nq{margin-top:2.14em}.nr{line-height:32px}.ns{letter-spacing:-0.003em}.oa{margin-top:56px}.ow{font-size:24px}.ox{margin-top:1.95em}.oy{line-height:30px}.oz{letter-spacing:-0.016em}.pf{margin-top:0.94em}.pn{margin-top:1.14em}.qd{margin-top:1.72em}.qe{line-height:24px}.qf{letter-spacing:0}.rc{display:inline-block}.ri{margin-bottom:104px}.rm{flex-direction:row}.rp{margin-bottom:0}.rq{margin-right:20px}.sf{max-width:500px}.th{margin-bottom:72px}.tp{padding-top:72px}</style><style type="text/css" data-fela-rehydration="540" data-fela-type="RULE" media="all and (max-width: 1079.98px)">.e{display:none}.ln{margin-top:0px}.rb{display:inline-block}</style><style type="text/css" data-fela-rehydration="540" data-fela-type="RULE" media="all and (max-width: 903.98px)">.f{display:none}.lm{margin-top:0px}.ra{display:inline-block}</style><style type="text/css" data-fela-rehydration="540" data-fela-type="RULE" media="all and (max-width: 727.98px)">.g{display:none}.lk{margin-top:0px}.ll{margin-right:0px}.qz{display:inline-block}</style><style type="text/css" data-fela-rehydration="540" data-fela-type="RULE" media="all and (max-width: 551.98px)">.h{display:none}.s{display:flex}.t{justify-content:space-between}.bs{width:24px}.cc{margin:0 24px}.cr{height:40px}.cy{margin-bottom:44px}.dk{margin-bottom:32px}.dx{font-size:13px}.dy{line-height:20px}.eh{padding:0px 8px 1px}.fu{margin-bottom:4px}.gw{font-size:32px}.gx{margin-top:1.01em}.gy{margin-bottom:24px}.gz{line-height:38px}.ha{letter-spacing:-0.014em}.hv{align-items:flex-start}.iy{flex-direction:column}.jb{margin-bottom:2px}.jp{margin:24px -24px 0}.jq{padding:0}.kf> *{margin-right:8px}.kg> :last-child{margin-right:24px}.kx{margin-left:0px}.li{margin-top:0px}.lj{margin-right:0px}.ly{margin:0}.mi{border:1px solid #F2F2F2}.mj{border-radius:99em}.mk{padding:0px 16px 0px 12px}.ml{height:38px}.mm{align-items:center}.mo svg{margin-right:8px}.mz{font-size:18px}.na{margin-top:1.56em}.nb{line-height:28px}.nc{letter-spacing:-0.003em}.nw{margin-top:40px}.og{font-size:20px}.oh{margin-top:1.2em}.oi{line-height:24px}.oj{letter-spacing:0}.pb{margin-top:0.67em}.pj{margin-top:1.34em}.pt{font-size:16px}.pu{margin-top:1.23em}.qy{display:inline-block}.re{margin-bottom:96px}.rx{margin-bottom:20px}.ry{margin-right:0}.sj{max-width:100%}.sm{font-size:24px}.sn{line-height:30px}.so{letter-spacing:-0.016em}.td{margin-bottom:64px}.tl{padding-top:48px}.mn:hover{border-color:#E5E5E5}</style><style type="text/css" data-fela-rehydration="540" data-fela-type="RULE" media="all and (min-width: 904px) and (max-width: 1079.98px)">.i{display:none}.bv{width:64px}.cf{margin:0 64px}.cu{height:48px}.db{margin-bottom:52px}.dn{margin-bottom:48px}.ed{font-size:14px}.ee{line-height:20px}.ek{font-size:13px}.el{padding:5px 12px}.fg{display:flex}.fx{margin-bottom:68px}.gb{max-width:680px}.hl{font-size:42px}.hm{margin-top:1.19em}.hn{margin-bottom:32px}.ho{line-height:52px}.hp{letter-spacing:-0.011em}.hy{align-items:center}.jx{border-top:solid 1px #F2F2F2}.jy{border-bottom:solid 1px #F2F2F2}.jz{margin:32px 0 0}.ka{padding:3px 8px}.kl> *{margin-right:24px}.km> :last-child{margin-right:0}.mb{margin:0}.nl{font-size:20px}.nm{margin-top:2.14em}.nn{line-height:32px}.no{letter-spacing:-0.003em}.nz{margin-top:56px}.os{font-size:24px}.ot{margin-top:1.95em}.ou{line-height:30px}.ov{letter-spacing:-0.016em}.pe{margin-top:0.94em}.pm{margin-top:1.14em}.qa{margin-top:1.72em}.qb{line-height:24px}.qc{letter-spacing:0}.rh{margin-bottom:104px}.rn{flex-direction:row}.rr{margin-bottom:0}.rs{margin-right:20px}.sg{max-width:500px}.tg{margin-bottom:72px}.to{padding-top:72px}</style><style type="text/css" data-fela-rehydration="540" data-fela-type="RULE" media="all and (min-width: 728px) and (max-width: 903.98px)">.j{display:none}.w{display:flex}.x{justify-content:space-between}.bu{width:64px}.ce{margin:0 48px}.ct{height:48px}.da{margin-bottom:52px}.dm{margin-bottom:48px}.eb{font-size:13px}.ec{line-height:20px}.ej{padding:0px 8px 1px}.fw{margin-bottom:68px}.ga{max-width:680px}.hg{font-size:42px}.hh{margin-top:1.19em}.hi{margin-bottom:32px}.hj{line-height:52px}.hk{letter-spacing:-0.011em}.hx{align-items:center}.jt{border-top:solid 1px #F2F2F2}.ju{border-bottom:solid 1px #F2F2F2}.jv{margin:32px 0 0}.jw{padding:3px 8px}.kj> *{margin-right:24px}.kk> :last-child{margin-right:0}.ma{margin:0}.nh{font-size:20px}.ni{margin-top:2.14em}.nj{line-height:32px}.nk{letter-spacing:-0.003em}.ny{margin-top:56px}.oo{font-size:24px}.op{margin-top:1.95em}.oq{line-height:30px}.or{letter-spacing:-0.016em}.pd{margin-top:0.94em}.pl{margin-top:1.14em}.px{margin-top:1.72em}.py{line-height:24px}.pz{letter-spacing:0}.rg{margin-bottom:104px}.ro{flex-direction:row}.rt{margin-bottom:0}.ru{margin-right:20px}.sh{max-width:500px}.tf{margin-bottom:72px}.tn{padding-top:72px}</style><style type="text/css" data-fela-rehydration="540" data-fela-type="RULE" media="all and (min-width: 552px) and (max-width: 727.98px)">.k{display:none}.u{display:flex}.v{justify-content:space-between}.bt{width:24px}.cd{margin:0 24px}.cs{height:40px}.cz{margin-bottom:44px}.dl{margin-bottom:32px}.dz{font-size:13px}.ea{line-height:20px}.ei{padding:0px 8px 1px}.fv{margin-bottom:4px}.hb{font-size:32px}.hc{margin-top:1.01em}.hd{margin-bottom:24px}.he{line-height:38px}.hf{letter-spacing:-0.014em}.hw{align-items:flex-start}.iz{flex-direction:column}.jc{margin-bottom:2px}.jr{margin:24px 0 0}.js{padding:0}.kh> *{margin-right:8px}.ki> :last-child{margin-right:8px}.ky{margin-left:0px}.lz{margin:0}.mp{border:1px solid #F2F2F2}.mq{border-radius:99em}.mr{padding:0px 16px 0px 12px}.ms{height:38px}.mt{align-items:center}.mv svg{margin-right:8px}.nd{font-size:18px}.ne{margin-top:1.56em}.nf{line-height:28px}.ng{letter-spacing:-0.003em}.nx{margin-top:40px}.ok{font-size:20px}.ol{margin-top:1.2em}.om{line-height:24px}.on{letter-spacing:0}.pc{margin-top:0.67em}.pk{margin-top:1.34em}.pv{font-size:16px}.pw{margin-top:1.23em}.rf{margin-bottom:96px}.rv{margin-bottom:20px}.rw{margin-right:0}.si{max-width:100%}.sp{font-size:24px}.sq{line-height:30px}.sr{letter-spacing:-0.016em}.te{margin-bottom:64px}.tm{padding-top:48px}.mu:hover{border-color:#E5E5E5}</style><style type="text/css" data-fela-rehydration="540" data-fela-type="RULE" media="print">.qw{display:none}</style><style type="text/css" data-fela-rehydration="540" data-fela-type="RULE" media="(orientation: landscape) and (max-width: 903.98px)">.jm{max-height:none}</style></head><body><div id="root"><div class="a b c"><div class="d e f g h i j k"></div><script>document.domain = document.domain;</script><div class="l c"><div class="l m n o c"><div class="p q r s t u v w x i d y z"><a class="du ag dv bf ak b am an ao ap aq ar as at s u w i d q dw z" href="https://rsci.app.link/?%24canonical_url=https%3A%2F%2Fmedium.com%2Fp%2F550d929126c8&%7Efeature=LoOpenInAppButton&%7Echannel=ShowPostUnderCollection&source=---top_nav_layout_nav----------------------------------" rel="noopener follow">Open in app<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10" fill="none" viewBox="0 0 10 10" class="dt"><path fill="currentColor" d="M.985 8.485a.375.375 0 1 0 .53.53zM8.75 1.25h.375A.375.375 0 0 0 8.75.875zM8.375 6.5a.375.375 0 1 0 .75 0zM3.5.875a.375.375 0 1 0 0 .75zm-1.985 8.14 7.5-7.5-.53-.53-7.5 7.5zm6.86-7.765V6.5h.75V1.25zM3.5 1.625h5.25v-.75H3.5z"></path></svg></a><div class="ab q"><p class="bf b dx dy dz ea eb ec ed ee ef eg du"><span><button class="bf b dx dy eh dz ea ei eb ec ej ek ee el em eg eo ep eq er es et eu ev ew ex ey ez fa fb fc fd bm fe ff" data-testid="headerSignUpButton">Sign up</button></span></p><div class="ax l"><p class="bf b dx dy dz ea eb ec ed ee ef eg du"><span><a class="af ag ah ai aj ak al am an ao ap aq ar as at" data-testid="headerSignInButton" rel="noopener follow" href="/m/signin?operation=login&redirect=https%3A%2F%2Fmedium.com%2Fairbnb-engineering%2Fbetter-android-testing-at-airbnb-part-4-testing-viewmodels-550d929126c8&source=post_page---top_nav_layout_nav-----------------------global_nav-----------">Sign in</a></span></p></div></div></div><div class="p q r ab ac"><div class="ab q ae"><a class="af ag ah ai aj ak al am an ao ap aq ar as at ab" aria-label="Homepage" data-testid="headerMediumLogo" rel="noopener follow" href="/?source=---top_nav_layout_nav----------------------------------"><svg xmlns="http://www.w3.org/2000/svg" width="719" height="160" fill="none" viewBox="0 0 719 160" class="au av aw"><path fill="#242424" d="m174.104 9.734.215-.047V8.02H130.39L89.6 103.89 48.81 8.021H1.472v1.666l.212.047c8.018 1.81 12.09 4.509 12.09 14.242V137.93c0 9.734-4.087 12.433-12.106 14.243l-.212.047v1.671h32.118v-1.665l-.213-.048c-8.018-1.809-12.089-4.509-12.089-14.242V30.586l52.399 123.305h2.972l53.925-126.743V140.75c-.687 7.688-4.721 10.062-11.982 11.701l-.215.05v1.652h55.948v-1.652l-.215-.05c-7.269-1.639-11.4-4.013-12.087-11.701l-.037-116.774h.037c0-9.733 4.071-12.432 12.087-14.242m25.555 75.488c.915-20.474 8.268-35.252 20.606-35.507 3.806.063 6.998 1.312 9.479 3.714 5.272 5.118 7.751 15.812 7.368 31.793zm-.553 5.77h65.573v-.275c-.186-15.656-4.721-27.834-13.466-36.196-7.559-7.227-18.751-11.203-30.507-11.203h-.263c-6.101 0-13.584 1.48-18.909 4.16-6.061 2.807-11.407 7.003-15.855 12.511-7.161 8.874-11.499 20.866-12.554 34.343q-.05.606-.092 1.212a50 50 0 0 0-.065 1.151 85.807 85.807 0 0 0-.094 5.689c.71 30.524 17.198 54.917 46.483 54.917 25.705 0 40.675-18.791 44.407-44.013l-1.886-.664c-6.557 13.556-18.334 21.771-31.738 20.769-18.297-1.369-32.314-19.922-31.042-42.395m139.722 41.359c-2.151 5.101-6.639 7.908-12.653 7.908s-11.513-4.129-15.418-11.63c-4.197-8.053-6.405-19.436-6.405-32.92 0-28.067 8.729-46.22 22.24-46.22 5.657 0 10.111 2.807 12.236 7.704zm43.499 20.008c-8.019-1.897-12.089-4.722-12.089-14.951V1.309l-48.716 14.353v1.757l.299-.024c6.72-.543 11.278.386 13.925 2.83 2.072 1.915 3.082 4.853 3.082 8.987v18.66c-4.803-3.067-10.516-4.56-17.448-4.56-14.059 0-26.909 5.92-36.176 16.672-9.66 11.205-14.767 26.518-14.767 44.278-.003 31.72 15.612 53.039 38.851 53.039 13.595 0 24.533-7.449 29.54-20.013v16.865h43.711v-1.746zM424.1 19.819c0-9.904-7.468-17.374-17.375-17.374-9.859 0-17.573 7.632-17.573 17.374s7.721 17.374 17.573 17.374c9.907 0 17.375-7.47 17.375-17.374m11.499 132.546c-8.019-1.897-12.089-4.722-12.089-14.951h-.035V43.635l-43.714 12.551v1.705l.263.024c9.458.842 12.047 4.1 12.047 15.152v81.086h43.751v-1.746zm112.013 0c-8.018-1.897-12.089-4.722-12.089-14.951V43.635l-41.621 12.137v1.71l.246.026c7.733.813 9.967 4.257 9.967 15.36v59.279c-2.578 5.102-7.415 8.131-13.274 8.336-9.503 0-14.736-6.419-14.736-18.073V43.638l-43.714 12.55v1.703l.262.024c9.459.84 12.05 4.097 12.05 15.152v50.17a56.3 56.3 0 0 0 .91 10.444l.787 3.423c3.701 13.262 13.398 20.197 28.59 20.197 12.868 0 24.147-7.966 29.115-20.43v17.311h43.714v-1.747zm169.818 1.788v-1.749l-.213-.05c-8.7-2.006-12.089-5.789-12.089-13.49v-63.79c0-19.89-11.171-31.761-29.883-31.761-13.64 0-25.141 7.882-29.569 20.16-3.517-13.01-13.639-20.16-28.606-20.16-13.146 0-23.449 6.938-27.869 18.657V43.643L545.487 55.68v1.715l.263.024c9.345.829 12.047 4.181 12.047 14.95v81.784h40.787v-1.746l-.215-.053c-6.941-1.631-9.181-4.606-9.181-12.239V66.998c1.836-4.289 5.537-9.37 12.853-9.37 9.086 0 13.692 6.296 13.692 18.697v77.828h40.797v-1.746l-.215-.053c-6.94-1.631-9.18-4.606-9.18-12.239V75.066a42 42 0 0 0-.578-7.26c1.947-4.661 5.86-10.177 13.475-10.177 9.214 0 13.691 6.114 13.691 18.696v77.828z"></path></svg></a><div class="ax h"><div class="ab ay az ba bb q bc bd"><div class="bm" aria-hidden="false" aria-describedby="searchResults" aria-labelledby="searchResults"></div><div class="bn bo ab"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path fill="currentColor" fill-rule="evenodd" d="M4.092 11.06a6.95 6.95 0 1 1 13.9 0 6.95 6.95 0 0 1-13.9 0m6.95-8.05a8.05 8.05 0 1 0 5.13 14.26l3.75 3.75a.56.56 0 1 0 .79-.79l-3.73-3.73A8.05 8.05 0 0 0 11.042 3z" clip-rule="evenodd"></path></svg></div><input role="combobox" aria-controls="searchResults" aria-expanded="false" aria-label="search" data-testid="headerSearchInput" tabindex="0" class="ay be bf bg z bh bi bj bk bl" placeholder="Search" value=""/></div></div></div><div class="h k w fg fh"><div class="fi ab"><span><a class="af ag ah ai aj ak al am an ao ap aq ar as at" data-testid="headerWriteButton" rel="noopener follow" href="/m/signin?operation=register&redirect=https%3A%2F%2Fmedium.com%2Fnew-story&source=---top_nav_layout_nav-----------------------new_post_topnav-----------"><div class="bf b bg z du fj fk ab q fl fm"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24" aria-label="Write"><path fill="currentColor" d="M14 4a.5.5 0 0 0 0-1zm7 6a.5.5 0 0 0-1 0zm-7-7H4v1h10zM3 4v16h1V4zm1 17h16v-1H4zm17-1V10h-1v10zm-1 1a1 1 0 0 0 1-1h-1zM3 20a1 1 0 0 0 1 1v-1zM4 3a1 1 0 0 0-1 1h1z"></path><path stroke="currentColor" d="m17.5 4.5-8.458 8.458a.25.25 0 0 0-.06.098l-.824 2.47a.25.25 0 0 0 .316.316l2.47-.823a.25.25 0 0 0 .098-.06L19.5 6.5m-2-2 2.323-2.323a.25.25 0 0 1 .354 0l1.646 1.646a.25.25 0 0 1 0 .354L19.5 6.5m-2-2 2 2"></path></svg><div class="dt l">Write</div></div></a></span></div></div><div class="k j i d"><div class="fi ab"><a class="af ag ah ai aj ak al am an ao ap aq ar as at" data-testid="headerSearchButton" rel="noopener follow" href="/search?source=---top_nav_layout_nav----------------------------------"><div class="bf b bg z du fj fk ab q fl fm"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24" aria-label="Search"><path fill="currentColor" fill-rule="evenodd" d="M4.092 11.06a6.95 6.95 0 1 1 13.9 0 6.95 6.95 0 0 1-13.9 0m6.95-8.05a8.05 8.05 0 1 0 5.13 14.26l3.75 3.75a.56.56 0 1 0 .79-.79l-3.73-3.73A8.05 8.05 0 0 0 11.042 3z" clip-rule="evenodd"></path></svg></div></a></div></div><div class="fi h k j"><div class="ab q"><p class="bf b dx dy dz ea eb ec ed ee ef eg du"><span><button class="bf b dx dy eh dz ea ei eb ec ej ek ee el em eg eo ep eq er es et eu ev ew ex ey ez fa fb fc fd bm fe ff" data-testid="headerSignUpButton">Sign up</button></span></p><div class="ax l"><p class="bf b dx dy dz ea eb ec ed ee ef eg du"><span><a class="af ag ah ai aj ak al am an ao ap aq ar as at" data-testid="headerSignInButton" rel="noopener follow" href="/m/signin?operation=login&redirect=https%3A%2F%2Fmedium.com%2Fairbnb-engineering%2Fbetter-android-testing-at-airbnb-part-4-testing-viewmodels-550d929126c8&source=post_page---top_nav_layout_nav-----------------------global_nav-----------">Sign in</a></span></p></div></div></div><div class="l" aria-hidden="false"><button class="ay fn am ab q ao fo fp fq" aria-label="user options menu" data-testid="headerUserIcon"><div class="l fj"><img alt="" class="l fd by bz ca cx" src="https://miro.medium.com/v2/resize:fill:64:64/1*dmbNkD5D-u45r44go_cf0g.png" width="32" height="32" loading="lazy" role="presentation"/><div class="fr by l bz ca fs n ay ft"></div></div></button></div></div></div><div class="l"><div class="fu fv fw fx fy l"><div class="ab cb"><div class="ci bh fz ga gb gc"></div></div><article><div class="l"><div class="l"><span class="l"></span><section><div><div class="fs gi gj gk gl gm"></div><div class="gn go gp gq gr"><div class="ab cb"><div class="ci bh fz ga gb gc"><div><h1 id="1fc1" class="pw-post-title gs gt gu bf gv gw gx gy gz ha hb hc hd he hf hg hh hi hj hk hl hm hn ho hp hq hr hs ht hu bk" data-testid="storyTitle">Better Android Testing at Airbnb — Part 4: Testing ViewModels</h1><div><div class="speechify-ignore ab cp"><div class="speechify-ignore bh l"><div class="hv hw hx hy hz ab"><div><div class="ab ia"><div><div class="bm" aria-hidden="false"><a rel="noopener follow" href="/@konakid?source=post_page---byline--550d929126c8--------------------------------"><div class="l ib ic by id ie"><div class="l fj"><img alt="Eli Hart" class="l fd by dd de cx" src="https://miro.medium.com/v2/resize:fill:88:88/2*qR91fuLzUz5PI59hjTTcRQ.jpeg" width="44" height="44" loading="lazy" data-testid="authorPhoto"/><div class="if by l dd de fs n ig ft"></div></div></div></a></div></div><div class="ih ab fj"><div><div class="bm" aria-hidden="false"><a href="https://medium.com/airbnb-engineering?source=post_page---byline--550d929126c8--------------------------------" rel="noopener follow"><div class="l ii ij by id ik"><div class="l fj"><img alt="The Airbnb Tech Blog" class="l fd by br il cx" src="https://miro.medium.com/v2/resize:fill:48:48/1*MlNQKg-sieBGW5prWoe9HQ.jpeg" width="24" height="24" loading="lazy" data-testid="publicationPhoto"/><div class="if by l br il fs n ig ft"></div></div></div></a></div></div></div></div></div><div class="bn bh l"><div class="ab"><div style="flex:1"><span class="bf b bg z bk"><div class="im ab q"><div class="ab q in"><div class="ab q"><div><div class="bm" aria-hidden="false"><p class="bf b io ip bk"><a class="af ag ah ai aj ak al am an ao ap aq ar iq" data-testid="authorName" rel="noopener follow" href="/@konakid?source=post_page---byline--550d929126c8--------------------------------">Eli Hart</a></p></div></div></div><span class="ir is" aria-hidden="true"><span class="bf b bg z du">·</span></span><p class="bf b io ip du"><span><a class="it iu ah ai aj ak al am an ao ap aq ar ex iv iw" rel="noopener follow" href="/m/signin?actionUrl=https%3A%2F%2Fmedium.com%2F_%2Fsubscribe%2Fuser%2F9f3427a69792&operation=register&redirect=https%3A%2F%2Fmedium.com%2Fairbnb-engineering%2Fbetter-android-testing-at-airbnb-part-4-testing-viewmodels-550d929126c8&user=Eli+Hart&userId=9f3427a69792&source=post_page-9f3427a69792--byline--550d929126c8---------------------post_header-----------">Follow</a></span></p></div></div></span></div></div><div class="l ix"><span class="bf b bg z du"><div class="ab cn iy iz ja"><div class="jb jc ab"><div class="bf b bg z du ab jd"><span class="je l ix">Published in</span><div><div class="l" aria-hidden="false"><a class="af ag ah ai aj ak al am an ao ap aq ar iq ab q" data-testid="publicationName" href="https://medium.com/airbnb-engineering?source=post_page---byline--550d929126c8--------------------------------" rel="noopener follow"><p class="bf b bg z jf jg jh ji jj jk jl jm bk">The Airbnb Tech Blog</p></a></div></div></div><div class="h k"><span class="ir is" aria-hidden="true"><span class="bf b bg z du">·</span></span></div></div><span class="bf b bg z du"><div class="ab ae"><span data-testid="storyReadTime">7 min read</span><div class="jn jo l" aria-hidden="true"><span class="l" aria-hidden="true"><span class="bf b bg z du">·</span></span></div><span data-testid="storyPublishDate">Dec 13, 2019</span></div></span></div></span></div></div></div><div class="ab cp jp jq jr js jt ju jv jw jx jy jz ka kb kc kd ke"><div class="h k w fg fh q"><div class="ku l"><div class="ab q kv kw"><div class="pw-multi-vote-icon fj je kx ky kz"><span><a class="af ag ah ai aj ak al am an ao ap aq ar as at" data-testid="headerClapButton" rel="noopener follow" href="/m/signin?actionUrl=https%3A%2F%2Fmedium.com%2F_%2Fvote%2Fairbnb-engineering%2F550d929126c8&operation=register&redirect=https%3A%2F%2Fmedium.com%2Fairbnb-engineering%2Fbetter-android-testing-at-airbnb-part-4-testing-viewmodels-550d929126c8&user=Eli+Hart&userId=9f3427a69792&source=---header_actions--550d929126c8---------------------clap_footer-----------"><div><div class="bm" aria-hidden="false"><div class="la ao lb lc ld le am lf lg lh kz"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" aria-label="clap"><path fill-rule="evenodd" d="M11.37.828 12 3.282l.63-2.454zM13.916 3.953l1.523-2.112-1.184-.39zM8.589 1.84l1.522 2.112-.337-2.501zM18.523 18.92c-.86.86-1.75 1.246-2.62 1.33a6 6 0 0 0 .407-.372c2.388-2.389 2.86-4.951 1.399-7.623l-.912-1.603-.79-1.672c-.26-.56-.194-.98.203-1.288a.7.7 0 0 1 .546-.132c.283.046.546.231.728.5l2.363 4.157c.976 1.624 1.141 4.237-1.324 6.702m-10.999-.438L3.37 14.328a.828.828 0 0 1 .585-1.408.83.83 0 0 1 .585.242l2.158 2.157a.365.365 0 0 0 .516-.516l-2.157-2.158-1.449-1.449a.826.826 0 0 1 1.167-1.17l3.438 3.44a.363.363 0 0 0 .516 0 .364.364 0 0 0 0-.516L5.293 9.513l-.97-.97a.826.826 0 0 1 0-1.166.84.84 0 0 1 1.167 0l.97.968 3.437 3.436a.36.36 0 0 0 .517 0 .366.366 0 0 0 0-.516L6.977 7.83a.82.82 0 0 1-.241-.584.82.82 0 0 1 .824-.826c.219 0 .43.087.584.242l5.787 5.787a.366.366 0 0 0 .587-.415l-1.117-2.363c-.26-.56-.194-.98.204-1.289a.7.7 0 0 1 .546-.132c.283.046.545.232.727.501l2.193 3.86c1.302 2.38.883 4.59-1.277 6.75-1.156 1.156-2.602 1.627-4.19 1.367-1.418-.236-2.866-1.033-4.079-2.246M10.75 5.971l2.12 2.12c-.41.502-.465 1.17-.128 1.89l.22.465-3.523-3.523a.8.8 0 0 1-.097-.368c0-.22.086-.428.241-.584a.847.847 0 0 1 1.167 0m7.355 1.705c-.31-.461-.746-.758-1.23-.837a1.44 1.44 0 0 0-1.11.275c-.312.24-.505.543-.59.881a1.74 1.74 0 0 0-.906-.465 1.47 1.47 0 0 0-.82.106l-2.182-2.182a1.56 1.56 0 0 0-2.2 0 1.54 1.54 0 0 0-.396.701 1.56 1.56 0 0 0-2.21-.01 1.55 1.55 0 0 0-.416.753c-.624-.624-1.649-.624-2.237-.037a1.557 1.557 0 0 0 0 2.2c-.239.1-.501.238-.715.453a1.56 1.56 0 0 0 0 2.2l.516.515a1.556 1.556 0 0 0-.753 2.615L7.01 19c1.32 1.319 2.909 2.189 4.475 2.449q.482.08.971.08c.85 0 1.653-.198 2.393-.579.231.033.46.054.686.054 1.266 0 2.457-.52 3.505-1.567 2.763-2.763 2.552-5.734 1.439-7.586z" clip-rule="evenodd"></path></svg></div></div></div></a></span></div><div class="pw-multi-vote-count l li lj lk ll lm ln lo"><p class="bf b dv z du"><span class="lp">--</span></p></div></div></div><div><div class="bm" aria-hidden="false"><button class="ao la lq lr ab q fk ls lt" aria-label="responses"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" class="lu"><path d="M18.006 16.803c1.533-1.456 2.234-3.325 2.234-5.321C20.24 7.357 16.709 4 12.191 4S4 7.357 4 11.482c0 4.126 3.674 7.482 8.191 7.482.817 0 1.622-.111 2.393-.327.231.2.48.391.744.559 1.06.693 2.203 1.044 3.399 1.044.224-.008.4-.112.486-.287a.49.49 0 0 0-.042-.518c-.495-.67-.845-1.364-1.04-2.057a4 4 0 0 1-.125-.598zm-3.122 1.055-.067-.223-.315.096a8 8 0 0 1-2.311.338c-4.023 0-7.292-2.955-7.292-6.587 0-3.633 3.269-6.588 7.292-6.588 4.014 0 7.112 2.958 7.112 6.593 0 1.794-.608 3.469-2.027 4.72l-.195.168v.255c0 .056 0 .151.016.295.025.231.081.478.154.733.154.558.398 1.117.722 1.659a5.3 5.3 0 0 1-2.165-.845c-.276-.176-.714-.383-.941-.59z"></path></svg></button></div></div></div><div class="ab q kf kg kh ki kj kk kl km kn ko kp kq kr ks kt"><div class="lv k j i d"></div><div class="h k"><div><div class="bm" aria-hidden="false"><span><a class="af ag ah ai aj ak al am an ao ap aq ar as at" data-testid="headerBookmarkButton" rel="noopener follow" href="/m/signin?actionUrl=https%3A%2F%2Fmedium.com%2F_%2Fbookmark%2Fp%2F550d929126c8&operation=register&redirect=https%3A%2F%2Fmedium.com%2Fairbnb-engineering%2Fbetter-android-testing-at-airbnb-part-4-testing-viewmodels-550d929126c8&source=---header_actions--550d929126c8---------------------bookmark_footer-----------"><svg xmlns="http://www.w3.org/2000/svg" width="25" height="25" fill="none" viewBox="0 0 25 25" class="du lw" aria-label="Add to list bookmark button"><path fill="currentColor" d="M18 2.5a.5.5 0 0 1 1 0V5h2.5a.5.5 0 0 1 0 1H19v2.5a.5.5 0 1 1-1 0V6h-2.5a.5.5 0 0 1 0-1H18zM7 7a1 1 0 0 1 1-1h3.5a.5.5 0 0 0 0-1H8a2 2 0 0 0-2 2v14a.5.5 0 0 0 .805.396L12.5 17l5.695 4.396A.5.5 0 0 0 19 21v-8.5a.5.5 0 0 0-1 0v7.485l-5.195-4.012a.5.5 0 0 0-.61 0L7 19.985z"></path></svg></a></span></div></div></div><div class="fd lx cn"><div class="l ae"><div class="ab cb"><div class="ly lz ma mb mc md ci bh"><div class="ab"><div class="bm bh" aria-hidden="false"><div><div class="bm" aria-hidden="false"><button aria-label="Listen" data-testid="audioPlayButton" class="af fk ah ai aj ak al me an ao ap ex mf mg lt mh mi mj mk ml s mm mn mo mp mq mr ms u mt mu mv"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path fill="currentColor" fill-rule="evenodd" d="M3 12a9 9 0 1 1 18 0 9 9 0 0 1-18 0m9-10C6.477 2 2 6.477 2 12s4.477 10 10 10 10-4.477 10-10S17.523 2 12 2m3.376 10.416-4.599 3.066a.5.5 0 0 1-.777-.416V8.934a.5.5 0 0 1 .777-.416l4.599 3.066a.5.5 0 0 1 0 .832" clip-rule="evenodd"></path></svg><div class="j i d"><p class="bf b bg z du">Listen</p></div></button></div></div></div></div></div></div></div></div><div class="bm" aria-hidden="false" aria-describedby="postFooterSocialMenu" aria-labelledby="postFooterSocialMenu"><div><div class="bm" aria-hidden="false"><button aria-controls="postFooterSocialMenu" aria-expanded="false" aria-label="Share Post" data-testid="headerSocialShareButton" class="af fk ah ai aj ak al me an ao ap ex mf mg lt mh mi mj mk ml s mm mn mo mp mq mr ms u mt mu mv"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path fill="currentColor" fill-rule="evenodd" d="M15.218 4.931a.4.4 0 0 1-.118.132l.012.006a.45.45 0 0 1-.292.074.5.5 0 0 1-.3-.13l-2.02-2.02v7.07c0 .28-.23.5-.5.5s-.5-.22-.5-.5v-7.04l-2 2a.45.45 0 0 1-.57.04h-.02a.4.4 0 0 1-.16-.3.4.4 0 0 1 .1-.32l2.8-2.8a.5.5 0 0 1 .7 0l2.8 2.79a.42.42 0 0 1 .068.498m-.106.138.008.004v-.01zM16 7.063h1.5a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2h-11c-1.1 0-2-.9-2-2v-10a2 2 0 0 1 2-2H8a.5.5 0 0 1 .35.15.5.5 0 0 1 .15.35.5.5 0 0 1-.15.35.5.5 0 0 1-.35.15H6.4c-.5 0-.9.4-.9.9v10.2a.9.9 0 0 0 .9.9h11.2c.5 0 .9-.4.9-.9v-10.2c0-.5-.4-.9-.9-.9H16a.5.5 0 0 1 0-1" clip-rule="evenodd"></path></svg><div class="j i d"><p class="bf b bg z du">Share</p></div></button></div></div></div></div></div></div></div></div></div><p id="2b68" class="pw-post-body-paragraph mw mx gu my b mz na nb nc nd ne nf ng nh ni nj nk nl nm nn no np nq nr ns nt gn bk"><em class="nu">In the fourth installment of our series on Android Testing at Airbnb, we look at a framework for unit testing logic in ViewModels.</em></p></div></div><div class="nv bh"><figure class="nw nx ny nz oa nv bh paragraph-image"><picture><source srcSet="https://miro.medium.com/v2/resize:fit:640/format:webp/1*SzUfC_eUHmH-O9CZwUDMtQ.jpeg 640w, https://miro.medium.com/v2/resize:fit:720/format:webp/1*SzUfC_eUHmH-O9CZwUDMtQ.jpeg 720w, https://miro.medium.com/v2/resize:fit:750/format:webp/1*SzUfC_eUHmH-O9CZwUDMtQ.jpeg 750w, https://miro.medium.com/v2/resize:fit:786/format:webp/1*SzUfC_eUHmH-O9CZwUDMtQ.jpeg 786w, https://miro.medium.com/v2/resize:fit:828/format:webp/1*SzUfC_eUHmH-O9CZwUDMtQ.jpeg 828w, https://miro.medium.com/v2/resize:fit:1100/format:webp/1*SzUfC_eUHmH-O9CZwUDMtQ.jpeg 1100w, https://miro.medium.com/v2/resize:fit:4800/format:webp/1*SzUfC_eUHmH-O9CZwUDMtQ.jpeg 4800w" sizes="(min-resolution: 4dppx) and (max-width: 700px) 50vw, (-webkit-min-device-pixel-ratio: 4) and (max-width: 700px) 50vw, (min-resolution: 3dppx) and (max-width: 700px) 67vw, (-webkit-min-device-pixel-ratio: 3) and (max-width: 700px) 65vw, (min-resolution: 2.5dppx) and (max-width: 700px) 80vw, (-webkit-min-device-pixel-ratio: 2.5) and (max-width: 700px) 80vw, (min-resolution: 2dppx) and (max-width: 700px) 100vw, (-webkit-min-device-pixel-ratio: 2) and (max-width: 700px) 100vw, 100vw" type="image/webp"/><source data-testid="og" srcSet="https://miro.medium.com/v2/resize:fit:640/1*SzUfC_eUHmH-O9CZwUDMtQ.jpeg 640w, https://miro.medium.com/v2/resize:fit:720/1*SzUfC_eUHmH-O9CZwUDMtQ.jpeg 720w, https://miro.medium.com/v2/resize:fit:750/1*SzUfC_eUHmH-O9CZwUDMtQ.jpeg 750w, https://miro.medium.com/v2/resize:fit:786/1*SzUfC_eUHmH-O9CZwUDMtQ.jpeg 786w, https://miro.medium.com/v2/resize:fit:828/1*SzUfC_eUHmH-O9CZwUDMtQ.jpeg 828w, https://miro.medium.com/v2/resize:fit:1100/1*SzUfC_eUHmH-O9CZwUDMtQ.jpeg 1100w, https://miro.medium.com/v2/resize:fit:4800/1*SzUfC_eUHmH-O9CZwUDMtQ.jpeg 4800w" sizes="(min-resolution: 4dppx) and (max-width: 700px) 50vw, (-webkit-min-device-pixel-ratio: 4) and (max-width: 700px) 50vw, (min-resolution: 3dppx) and (max-width: 700px) 67vw, (-webkit-min-device-pixel-ratio: 3) and (max-width: 700px) 65vw, (min-resolution: 2.5dppx) and (max-width: 700px) 80vw, (-webkit-min-device-pixel-ratio: 2.5) and (max-width: 700px) 80vw, (min-resolution: 2dppx) and (max-width: 700px) 100vw, (-webkit-min-device-pixel-ratio: 2) and (max-width: 700px) 100vw, 100vw"/><img alt="" class="bh md ob c" width="2400" height="5792" loading="eager" role="presentation"/></picture></figure></div><div class="ab cb"><div class="ci bh fz ga gb gc"><p id="6cc5" class="pw-post-body-paragraph mw mx gu my b mz na nb nc nd ne nf ng nh ni nj nk nl nm nn no np nq nr ns nt gn bk"><a class="af oc" rel="noopener" href="/airbnb-engineering/better-android-testing-at-airbnb-1d1e91e489b4">In part three</a> of this series we saw how automated interaction testing can cover some of the code paths in our ViewModels by recording state changes. However, this can’t test all edge cases of our logic. ViewModel logic is crucial to the correct behavior of each screen, and thus is worth testing in more depth.</p><h1 id="b3f3" class="od oe gu bf of og oh oi oj ok ol om on oo op oq or os ot ou ov ow ox oy oz pa bk">Unit Testing ViewModels</h1><p id="0f85" class="pw-post-body-paragraph mw mx gu my b mz pb nb nc nd pc nf ng nh pd nj nk nl pe nn no np pf nr ns nt gn bk">ViewModels are a case where we support manually written tests, but as usual we take an approach to minimize the overhead of manual testing by providing our developers with a unit test framework. This includes a DSL to simplify test statements, integration with our network stack to make assertions on executed requests, and a tie in with our state mocking system to easily set ViewModel states for testing.</p><p id="0b4a" class="pw-post-body-paragraph mw mx gu my b mz na nb nc nd ne nf ng nh ni nj nk nl nm nn no np nq nr ns nt gn bk">We developed the unit test framework based on a few core tenets:</p><ul class=""><li id="060d" class="mw mx gu my b mz na nb nc nd ne nf ng nh ni nj nk nl nm nn no np nq nr ns nt pg ph pi bk">A ViewModel function should be independently testable. The design of the ViewModel should not rely on interactions between multiple function calls.</li><li id="e814" class="mw mx gu my b mz pj nb nc nd pk nf ng nh pl nj nk nl pm nn no np pn nr ns nt pg ph pi bk">The behavior of a function call should be purely defined by the ViewModel’s State when the function is invoked, and the parameters it is passed.</li><li id="1f98" class="mw mx gu my b mz pj nb nc nd pk nf ng nh pl nj nk nl pm nn no np pn nr ns nt pg ph pi bk">The output of the function should either be a new State set on the ViewModel, or a call out to a dependency.</li></ul><p id="148a" class="pw-post-body-paragraph mw mx gu my b mz na nb nc nd ne nf ng nh ni nj nk nl nm nn no np nq nr ns nt gn bk">Given these tenets, our framework takes the following approach:</p><ul class=""><li id="5e20" class="mw mx gu my b mz na nb nc nd ne nf ng nh ni nj nk nl nm nn no np nq nr ns nt pg ph pi bk">Each unit test invokes a single ViewModel function</li><li id="7576" class="mw mx gu my b mz pj nb nc nd pk nf ng nh pl nj nk nl pm nn no np pn nr ns nt pg ph pi bk">The input to the test is the initial state of the ViewModel, plus the parameters that are passed to the function</li><li id="6f56" class="mw mx gu my b mz pj nb nc nd pk nf ng nh pl nj nk nl pm nn no np pn nr ns nt pg ph pi bk">The output of the test is an assertion on what was changed in the state, and/or verification of an expected call to a dependency (via Mockito).</li></ul><h1 id="7366" class="od oe gu bf of og oh oi oj ok ol om on oo op oq or os ot ou ov ow ox oy oz pa bk">A Basic Example</h1><p id="c094" class="pw-post-body-paragraph mw mx gu my b mz pb nb nc nd pc nf ng nh pd nj nk nl pe nn no np pf nr ns nt gn bk">Let’s look at a basic ViewModel that stores an updatable String.</p><figure class="nw nx ny nz oa nv"><div class="po jf l fj"><div class="pp pq l"></div></div></figure><p id="0467" class="pw-post-body-paragraph mw mx gu my b mz na nb nc nd ne nf ng nh ni nj nk nl nm nn no np nq nr ns nt gn bk">The code to test the <strong class="my gv"><em class="nu">setText</em></strong> function looks like this:</p><figure class="nw nx ny nz oa nv"><div class="po jf l fj"><div class="pp pq l"></div></div></figure><p id="6566" class="pw-post-body-paragraph mw mx gu my b mz na nb nc nd ne nf ng nh ni nj nk nl nm nn no np nq nr ns nt gn bk">This specifies a reference to the function that we are testing, the parameter to invoke the function with, and what state change we should expect as a result. Here we test that calling <strong class="my gv"><em class="nu">setText(“hello”)</em></strong> results in the text state being updated to “hello”.</p><p id="8a32" class="pw-post-body-paragraph mw mx gu my b mz na nb nc nd ne nf ng nh ni nj nk nl nm nn no np nq nr ns nt gn bk">The <strong class="my gv"><em class="nu">expectState</em></strong> function takes the initial State as a receiver and returns the expected output State. This returned State must match the output exactly, otherwise the tests fails and the framework prints out which properties were not equal. Effectively, <strong class="my gv"><em class="nu">expectState</em></strong> defines which properties are expected to change and what the new values should be. This prevents side-effects from being missed.</p><h1 id="61b2" class="od oe gu bf of og oh oi oj ok ol om on oo op oq or os ot ou ov ow ox oy oz pa bk">Test Setup</h1><p id="91e1" class="pw-post-body-paragraph mw mx gu my b mz pb nb nc nd pc nf ng nh pd nj nk nl pe nn no np pf nr ns nt gn bk">The test framework takes care of initializing the ViewModel, collecting test statements, and checking assertions.</p><p id="2757" class="pw-post-body-paragraph mw mx gu my b mz na nb nc nd ne nf ng nh ni nj nk nl nm nn no np nq nr ns nt gn bk">Tests are run with a normal JUnit and Robolectric setup, and each test class corresponds to a single ViewModel. The class implements an interface which the test frameworks uses to initialize a new ViewModel for each test.</p><p id="7f56" class="pw-post-body-paragraph mw mx gu my b mz na nb nc nd ne nf ng nh ni nj nk nl nm nn no np nq nr ns nt gn bk">For example, the full test class for the above ViewModel would look like this:</p><figure class="nw nx ny nz oa nv"><div class="po jf l fj"><div class="pp pq l"></div></div></figure><p id="4b72" class="pw-post-body-paragraph mw mx gu my b mz na nb nc nd ne nf ng nh ni nj nk nl nm nn no np nq nr ns nt gn bk">The test framework uses the <strong class="my gv"><em class="nu">buildViewModel()</em></strong> function to create a new ViewModel for each test.</p><p id="3985" class="pw-post-body-paragraph mw mx gu my b mz na nb nc nd ne nf ng nh ni nj nk nl nm nn no np nq nr ns nt gn bk">The initial state of the ViewModel can be a mock state reused from an existing mock for the screen. This allows screenshot tests, interaction tests, and ViewModel unit tests to all share the same underlying mock instance. This greatly reduces the work involved to setup tests, and if the State data structure changes the mock only needs to be updated in one place.</p><h1 id="18c6" class="od oe gu bf of og oh oi oj ok ol om on oo op oq or os ot ou ov ow ox oy oz pa bk">Modifying State</h1><p id="cc91" class="pw-post-body-paragraph mw mx gu my b mz pb nb nc nd pc nf ng nh pd nj nk nl pe nn no np pf nr ns nt gn bk">If a test needs to use a modified version of the default state it can leverage our previously mentioned data class DSL to easily make changes to nested state.</p><p id="d5f7" class="pw-post-body-paragraph mw mx gu my b mz na nb nc nd ne nf ng nh ni nj nk nl nm nn no np nq nr ns nt gn bk">To demonstrate, let’s expand our example to be a bit more complicated. Now it has some additional state that allows us to track whether the text is bold.</p><figure class="nw nx ny nz oa nv"><div class="po jf l fj"><div class="pp pq l"></div></div></figure><p id="6bf1" class="pw-post-body-paragraph mw mx gu my b mz na nb nc nd ne nf ng nh ni nj nk nl nm nn no np nq nr ns nt gn bk">Our test syntax to check the <strong class="my gv"><em class="nu">setBold</em></strong> function looks like this:</p><figure class="nw nx ny nz oa nv"><div class="po jf l fj"><div class="pp pq l"></div></div></figure><p id="8cb8" class="pw-post-body-paragraph mw mx gu my b mz na nb nc nd ne nf ng nh ni nj nk nl nm nn no np nq nr ns nt gn bk">This does the following:</p><ol class=""><li id="8bfa" class="mw mx gu my b mz na nb nc nd ne nf ng nh ni nj nk nl nm nn no np nq nr ns nt pr ph pi bk">Initializes the nested <strong class="my gv"><em class="nu">bold</em></strong> boolean property to false</li><li id="13ad" class="mw mx gu my b mz pj nb nc nd pk nf ng nh pl nj nk nl pm nn no np pn nr ns nt pr ph pi bk">Invokes the <strong class="my gv"><em class="nu">setBold</em></strong> with the parameter value <strong class="my gv"><em class="nu">true</em></strong></li><li id="e45f" class="mw mx gu my b mz pj nb nc nd pk nf ng nh pl nj nk nl pm nn no np pn nr ns nt pr ph pi bk">Validates that the final state of the ViewModel has the <strong class="my gv"><em class="nu">bold</em></strong> property now set to true</li></ol><h1 id="e842" class="od oe gu bf of og oh oi oj ok ol om on oo op oq or os ot ou ov ow ox oy oz pa bk">Extensibility</h1><p id="6cd3" class="pw-post-body-paragraph mw mx gu my b mz pb nb nc nd pc nf ng nh pd nj nk nl pe nn no np pf nr ns nt gn bk">The DSL uses a pluggable system so that 3rd party extension functions can add custom statements and assertions. We use this ourselves to check that expected calls to our network stack are made.</p><p id="9eeb" class="pw-post-body-paragraph mw mx gu my b mz na nb nc nd ne nf ng nh ni nj nk nl nm nn no np nq nr ns nt gn bk">In our example ViewModel let’s add a function that loads text from a network request.</p><figure class="nw nx ny nz oa nv"><div class="po jf l fj"><div class="pp pq l"></div></div></figure><p id="01d7" class="pw-post-body-paragraph mw mx gu my b mz na nb nc nd ne nf ng nh ni nj nk nl nm nn no np nq nr ns nt gn bk">The function to test this would look like:</p><figure class="nw nx ny nz oa nv"><div class="po jf l fj"><div class="pp pq l"></div></div></figure><p id="20a1" class="pw-post-body-paragraph mw mx gu my b mz na nb nc nd ne nf ng nh ni nj nk nl nm nn no np nq nr ns nt gn bk">This test:</p><ol class=""><li id="ff39" class="mw mx gu my b mz na nb nc nd ne nf ng nh ni nj nk nl nm nn no np nq nr ns nt pr ph pi bk">Invokes the <strong class="my gv"><em class="nu">loadText</em></strong> function with the id argument “1”</li><li id="b1dd" class="mw mx gu my b mz pj nb nc nd pk nf ng nh pl nj nk nl pm nn no np pn nr ns nt pr ph pi bk">Makes an assertion that we expect the ViewModel to execute a network GET request to the given API path with the id as a query parameter</li><li id="53e6" class="mw mx gu my b mz pj nb nc nd pk nf ng nh pl nj nk nl pm nn no np pn nr ns nt pr ph pi bk">Specifies a mock response value of “server result”</li><li id="fe31" class="mw mx gu my b mz pj nb nc nd pk nf ng nh pl nj nk nl pm nn no np pn nr ns nt pr ph pi bk">Asserts that the final state value of the text matches our mocked response value “server result”</li></ol><p id="cac9" class="pw-post-body-paragraph mw mx gu my b mz na nb nc nd ne nf ng nh ni nj nk nl nm nn no np nq nr ns nt gn bk">This allows us to test the boundary of our ViewModel with the network layer, easily checking that the desired request is made and mocking the return value.</p><p id="4fc8" class="pw-post-body-paragraph mw mx gu my b mz na nb nc nd ne nf ng nh ni nj nk nl nm nn no np nq nr ns nt gn bk">The <strong class="my gv"><em class="nu">expectRequests</em></strong> function is an extension function to the unit testing framework. This allows us to open source the core library but still test our internal libraries at Airbnb.</p><p id="4293" class="pw-post-body-paragraph mw mx gu my b mz na nb nc nd ne nf ng nh ni nj nk nl nm nn no np nq nr ns nt gn bk">Also noteworthy is that in both unit and integration tests we never mock network requests at the JSON layer. We find maintaining JSON file mocks to be difficult and unnecessary. Instead, we are adopting GraphQL to give compile time guarantees on each request schema. This means we just need to assert that the proper query was made, and we can trust that the response will be in a valid, expected format.</p><p id="14e4" class="pw-post-body-paragraph mw mx gu my b mz na nb nc nd ne nf ng nh ni nj nk nl nm nn no np nq nr ns nt gn bk">This simplifies the scope of our tests, improves maintainability, and still offers us guarantees on the functionality of our network layer.</p><h1 id="6992" class="od oe gu bf of og oh oi oj ok ol om on oo op oq or os ot ou ov ow ox oy oz pa bk">Advanced Usage</h1><p id="dc2a" class="pw-post-body-paragraph mw mx gu my b mz pb nb nc nd pc nf ng nh pd nj nk nl pe nn no np pf nr ns nt gn bk">The unit test framework provides some other nice utilities for testing common ViewModel patterns.</p><h2 id="339d" class="ps oe gu bf of pt pu dy oj pv pw ea on nh px py pz nl qa qb qc np qd qe qf qg bk">Automatic Validations</h2><p id="fd54" class="pw-post-body-paragraph mw mx gu my b mz pb nb nc nd pc nf ng nh pd nj nk nl pe nn no np pf nr ns nt gn bk">One common case is a ViewModel function that updates a single property on the State, such as our example above that toggles the “bold” boolean. The framework offers special handling for this case to make it testable in just a few lines.</p><figure class="nw nx ny nz oa nv"><div class="po jf l fj"><div class="pp pq l"></div></div></figure><p id="8c12" class="pw-post-body-paragraph mw mx gu my b mz na nb nc nd ne nf ng nh ni nj nk nl nm nn no np nq nr ns nt gn bk">With this syntax, the test:</p><ol class=""><li id="b5a9" class="mw mx gu my b mz na nb nc nd ne nf ng nh ni nj nk nl nm nn no np nq nr ns nt pr ph pi bk">Indicates that the <strong class="my gv"><em class="nu">setBold</em></strong> function is being tested</li><li id="2561" class="mw mx gu my b mz pj nb nc nd pk nf ng nh pl nj nk nl pm nn no np pn nr ns nt pr ph pi bk">Specifies a reference to the nested state property that will be changed as a result</li><li id="bfe6" class="mw mx gu my b mz pj nb nc nd pk nf ng nh pl nj nk nl pm nn no np pn nr ns nt pr ph pi bk">Detects the parameter type of the <strong class="my gv"><em class="nu">setBold</em></strong> function (boolean, in this case)</li><li id="663e" class="mw mx gu my b mz pj nb nc nd pk nf ng nh pl nj nk nl pm nn no np pn nr ns nt pr ph pi bk">Generates test inputs based on the parameter type. For a boolean this will be true and false. If it is nullable it will also test a “<strong class="my gv"><em class="nu">null</em></strong>” input.</li><li id="60a3" class="mw mx gu my b mz pj nb nc nd pk nf ng nh pl nj nk nl pm nn no np pn nr ns nt pr ph pi bk">Invokes the <strong class="my gv"><em class="nu">setBold</em></strong> function with the generated input values, and after each invocation checks that the “bold” property in the state has been updated to the same value.</li></ol><p id="4b1d" class="pw-post-body-paragraph mw mx gu my b mz na nb nc nd ne nf ng nh ni nj nk nl nm nn no np nq nr ns nt gn bk">This works for any function with a single primitive type — the type is detected with reflection and the function is invoked with various test values. Then the property value on the state is checked to make sure it equals the expected test value.</p><p id="1be6" class="pw-post-body-paragraph mw mx gu my b mz na nb nc nd ne nf ng nh ni nj nk nl nm nn no np nq nr ns nt gn bk">In the more generic case, we also support multi parameter functions as well as cases where the state property type is different from the function parameter type.</p><p id="5034" class="pw-post-body-paragraph mw mx gu my b mz na nb nc nd ne nf ng nh ni nj nk nl nm nn no np nq nr ns nt gn bk">For example, the following tests a function that squares an input and sets the value on the “result” property of the state.</p><figure class="nw nx ny nz oa nv"><div class="po jf l fj"><div class="pp pq l"></div></div></figure><p id="8335" class="pw-post-body-paragraph mw mx gu my b mz na nb nc nd ne nf ng nh ni nj nk nl nm nn no np nq nr ns nt gn bk">This makes it easy to list a mapping of inputs to outputs; it automatically invokes the function with each input and checks for the corresponding output on the state.</p><h2 id="51b7" class="ps oe gu bf of pt pu dy oj pv pw ea on nh px py pz nl qa qb qc np qd qe qf qg bk">Testing Initialization</h2><p id="6e4f" class="pw-post-body-paragraph mw mx gu my b mz pb nb nc nd pc nf ng nh pd nj nk nl pe nn no np pf nr ns nt gn bk">It is also common for a ViewModel to execute network requests or other tasks during its initialization; that is, when a new instance is instantiated and the constructor is invoked.</p><p id="1950" class="pw-post-body-paragraph mw mx gu my b mz na nb nc nd ne nf ng nh ni nj nk nl nm nn no np nq nr ns nt gn bk">For example, imagine our TextViewModel from above was modified to load text from a network request when it is created. We can test that behavior with this syntax.</p><figure class="nw nx ny nz oa nv"><div class="po jf l fj"><div class="pp pq l"></div></div></figure><p id="fc50" class="pw-post-body-paragraph mw mx gu my b mz na nb nc nd ne nf ng nh ni nj nk nl nm nn no np nq nr ns nt gn bk">This asserts that when the ViewModel is instantiated it:</p><ol class=""><li id="b9b8" class="mw mx gu my b mz na nb nc nd ne nf ng nh ni nj nk nl nm nn no np nq nr ns nt pr ph pi bk">Makes a network GET request to an expected API path</li><li id="0541" class="mw mx gu my b mz pj nb nc nd pk nf ng nh pl nj nk nl pm nn no np pn nr ns nt pr ph pi bk">Sets the text property to a “Loading” state</li></ol><p id="ca5d" class="pw-post-body-paragraph mw mx gu my b mz na nb nc nd ne nf ng nh ni nj nk nl nm nn no np nq nr ns nt gn bk">This syntax is required compared to the normal function testing syntax because it must wrap the instantiation of the ViewModel and isolate the behavior there. On the other hand, when testing functions we exclude the behavior during instantiation in order to not conflate them.</p><h2 id="0b27" class="ps oe gu bf of pt pu dy oj pv pw ea on nh px py pz nl qa qb qc np qd qe qf qg bk">Generating Test Scaffolding</h2><p id="ee22" class="pw-post-body-paragraph mw mx gu my b mz pb nb nc nd pc nf ng nh pd nj nk nl pe nn no np pf nr ns nt gn bk">Finally, in a multi module world it can be tedious to set up a unit test environment for each new module that is created (we have hundreds of modules!). For each module we need:</p><ul class=""><li id="4156" class="mw mx gu my b mz na nb nc nd ne nf ng nh ni nj nk nl nm nn no np nq nr ns nt pg ph pi bk">A Robolectric test runner</li><li id="a719" class="mw mx gu my b mz pj nb nc nd pk nf ng nh pl nj nk nl pm nn no np pn nr ns nt pg ph pi bk">A base test for test classes to extend so the runner is applied</li><li id="ea28" class="mw mx gu my b mz pj nb nc nd pk nf ng nh pl nj nk nl pm nn no np pn nr ns nt pg ph pi bk">Scaffolding to support dagger test overrides (a new Dagger module plus a Test application to setup injection of the dagger module).</li><li id="96e4" class="mw mx gu my b mz pj nb nc nd pk nf ng nh pl nj nk nl pm nn no np pn nr ns nt pg ph pi bk">A mockito plugin to support mocking final classes and functions (for Kotlin usage)</li></ul><p id="660d" class="pw-post-body-paragraph mw mx gu my b mz na nb nc nd ne nf ng nh ni nj nk nl nm nn no np nq nr ns nt gn bk">We’ve created tooling that automatically generates all of this test scaffolding for a module, so a developer can instantly start writing unit tests without dealing with any of the tedium of configuration.</p><p id="b3f4" class="pw-post-body-paragraph mw mx gu my b mz na nb nc nd ne nf ng nh ni nj nk nl nm nn no np nq nr ns nt gn bk">Additionally, we’ve created an Intellij IDEA plugin that can generate new MvRx ViewModels for us. This allows us to automatically create a test file for each new ViewModel that we add.</p><h1 id="ee96" class="od oe gu bf of og oh oi oj ok ol om on oo op oq or os ot ou ov ow ox oy oz pa bk">Next: Our Automation Infrastructure</h1><p id="fb5c" class="pw-post-body-paragraph mw mx gu my b mz pb nb nc nd pc nf ng nh pd nj nk nl pe nn no np pf nr ns nt gn bk">Overall, our goal for this unit test framework was to:</p><ol class=""><li id="cc6d" class="mw mx gu my b mz na nb nc nd ne nf ng nh ni nj nk nl nm nn no np nq nr ns nt pr ph pi bk">Remove friction from testing ViewModel logic, while;</li><li id="a313" class="mw mx gu my b mz pj nb nc nd pk nf ng nh pl nj nk nl pm nn no np pn nr ns nt pr ph pi bk">Providing a simple, yet flexible, API that can cover all use cases of a ViewModel.</li></ol><p id="d695" class="pw-post-body-paragraph mw mx gu my b mz na nb nc nd ne nf ng nh ni nj nk nl nm nn no np nq nr ns nt gn bk">Additionally, we designed the library to be extensible so we can open source it, allowing teams to easily add their own assertions to the DSL.</p><p id="7538" class="pw-post-body-paragraph mw mx gu my b mz na nb nc nd ne nf ng nh ni nj nk nl nm nn no np nq nr ns nt gn bk">While this has been great for us, and was necessary for comprehensively testing business logic, the nicest tests are ones we can automatically generate! Next, in <a class="af oc" rel="noopener" href="/airbnb-engineering/better-android-testing-at-airbnb-661a554a8c8b">Part 5 of the series</a>, we’ll revisit our automated integration testing framework to see how it powers our interaction and screenshot tests.</p><h2 id="41bb" class="ps oe gu bf of pt pu dy oj pv pw ea on nh px py pz nl qa qb qc np qd qe qf qg bk">Series Index</h2><p id="18cd" class="pw-post-body-paragraph mw mx gu my b mz pb nb nc nd pc nf ng nh pd nj nk nl pe nn no np pf nr ns nt gn bk">This is a seven part article series on testing at Airbnb.</p><p id="c733" class="pw-post-body-paragraph mw mx gu my b mz na nb nc nd ne nf ng nh ni nj nk nl nm nn no np nq nr ns nt gn bk">Part 1 — <a class="af oc" rel="noopener" href="/airbnb-engineering/better-android-testing-at-airbnb-3f5b90b9c40a">Testing Philosophy and a Mocking System</a></p><p id="221c" class="pw-post-body-paragraph mw mx gu my b mz na nb nc nd ne nf ng nh ni nj nk nl nm nn no np nq nr ns nt gn bk">Part 2 — <a class="af oc" rel="noopener" href="/airbnb-engineering/better-android-testing-at-airbnb-a77ac9531cab">Screenshot Testing with MvRx and Happo</a></p><p id="3c1e" class="pw-post-body-paragraph mw mx gu my b mz na nb nc nd ne nf ng nh ni nj nk nl nm nn no np nq nr ns nt gn bk">Part 3 — <a class="af oc" rel="noopener" href="/airbnb-engineering/better-android-testing-at-airbnb-1d1e91e489b4">Automated Interaction Testing</a></p><p id="992a" class="pw-post-body-paragraph mw mx gu my b mz na nb nc nd ne nf ng nh ni nj nk nl nm nn no np nq nr ns nt gn bk"><strong class="my gv">Part 4 (This article)</strong> — <a class="af oc" rel="noopener" href="/airbnb-engineering/better-android-testing-at-airbnb-part-4-testing-viewmodels-550d929126c8">A Framework for Unit Testing ViewModels</a></p><p id="6103" class="pw-post-body-paragraph mw mx gu my b mz na nb nc nd ne nf ng nh ni nj nk nl nm nn no np nq nr ns nt gn bk">Part 5 — <a class="af oc" rel="noopener" href="/airbnb-engineering/better-android-testing-at-airbnb-661a554a8c8b">Architecture of our Automated Testing Framework</a></p><p id="c39d" class="pw-post-body-paragraph mw mx gu my b mz na nb nc nd ne nf ng nh ni nj nk nl nm nn no np nq nr ns nt gn bk">Part 6 — <a class="af oc" rel="noopener" href="/airbnb-engineering/better-android-testing-at-airbnb-a11f6832773f">Obstacles to Consistent Mocking</a></p><p id="461f" class="pw-post-body-paragraph mw mx gu my b mz na nb nc nd ne nf ng nh ni nj nk nl nm nn no np nq nr ns nt gn bk">Part 7 — <a class="af oc" rel="noopener" href="/airbnb-engineering/better-android-testing-at-airbnb-eacec3a8a72f">Test Generation and CI Configuration</a></p><h2 id="f73d" class="ps oe gu bf of pt pu dy oj pv pw ea on nh px py pz nl qa qb qc np qd qe qf qg bk">We’re Hiring!</h2><p id="e896" class="pw-post-body-paragraph mw mx gu my b mz pb nb nc nd pc nf ng nh pd nj nk nl pe nn no np pf nr ns nt gn bk">Want to work with us on these and other Android projects at scale? Airbnb is hiring for several Android engineer positions across the company! See <a class="af oc" href="https://careers.airbnb.com/" rel="noopener ugc nofollow" target="_blank">https://careers.airbnb.com</a> for current openings.</p></div></div></div></div></section></div></div></article></div><div class="ab cb"><div class="ci bh fz ga gb gc"><div class="qh qi ab ja"><div class="qj ab"><a class="qk ay am ao" rel="noopener follow" href="/tag/android?source=post_page-----550d929126c8--------------------------------"><div class="ql fj cx qm ge qn qo bf b bg z bk qp">Android</div></a></div><div class="qj ab"><a class="qk ay am ao" rel="noopener follow" href="/tag/testing?source=post_page-----550d929126c8--------------------------------"><div class="ql fj cx qm ge qn qo bf b bg z bk qp">Testing</div></a></div><div class="qj ab"><a class="qk ay am ao" rel="noopener follow" href="/tag/mobile?source=post_page-----550d929126c8--------------------------------"><div class="ql fj cx qm ge qn qo bf b bg z bk qp">Mobile</div></a></div><div class="qj ab"><a class="qk ay am ao" rel="noopener follow" href="/tag/native?source=post_page-----550d929126c8--------------------------------"><div class="ql fj cx qm ge qn qo bf b bg z bk qp">Native</div></a></div></div></div></div><div class="l"></div><footer class="qq qr qs qt qu ab q qv ik c"><div class="l ae"><div class="ab cb"><div class="ci bh fz ga gb gc"><div class="ab cp qw"><div class="ab q kv"><div class="qx l"><span class="l qy qz ra e d"><div class="ab q kv kw"><div class="pw-multi-vote-icon fj je kx ky kz"><span><a class="af ag ah ai aj ak al am an ao ap aq ar as at" data-testid="footerClapButton" rel="noopener follow" href="/m/signin?actionUrl=https%3A%2F%2Fmedium.com%2F_%2Fvote%2Fairbnb-engineering%2F550d929126c8&operation=register&redirect=https%3A%2F%2Fmedium.com%2Fairbnb-engineering%2Fbetter-android-testing-at-airbnb-part-4-testing-viewmodels-550d929126c8&user=Eli+Hart&userId=9f3427a69792&source=---footer_actions--550d929126c8---------------------clap_footer-----------"><div><div class="bm" aria-hidden="false"><div class="la ao lb lc ld le am lf lg lh kz"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" aria-label="clap"><path fill-rule="evenodd" d="M11.37.828 12 3.282l.63-2.454zM13.916 3.953l1.523-2.112-1.184-.39zM8.589 1.84l1.522 2.112-.337-2.501zM18.523 18.92c-.86.86-1.75 1.246-2.62 1.33a6 6 0 0 0 .407-.372c2.388-2.389 2.86-4.951 1.399-7.623l-.912-1.603-.79-1.672c-.26-.56-.194-.98.203-1.288a.7.7 0 0 1 .546-.132c.283.046.546.231.728.5l2.363 4.157c.976 1.624 1.141 4.237-1.324 6.702m-10.999-.438L3.37 14.328a.828.828 0 0 1 .585-1.408.83.83 0 0 1 .585.242l2.158 2.157a.365.365 0 0 0 .516-.516l-2.157-2.158-1.449-1.449a.826.826 0 0 1 1.167-1.17l3.438 3.44a.363.363 0 0 0 .516 0 .364.364 0 0 0 0-.516L5.293 9.513l-.97-.97a.826.826 0 0 1 0-1.166.84.84 0 0 1 1.167 0l.97.968 3.437 3.436a.36.36 0 0 0 .517 0 .366.366 0 0 0 0-.516L6.977 7.83a.82.82 0 0 1-.241-.584.82.82 0 0 1 .824-.826c.219 0 .43.087.584.242l5.787 5.787a.366.366 0 0 0 .587-.415l-1.117-2.363c-.26-.56-.194-.98.204-1.289a.7.7 0 0 1 .546-.132c.283.046.545.232.727.501l2.193 3.86c1.302 2.38.883 4.59-1.277 6.75-1.156 1.156-2.602 1.627-4.19 1.367-1.418-.236-2.866-1.033-4.079-2.246M10.75 5.971l2.12 2.12c-.41.502-.465 1.17-.128 1.89l.22.465-3.523-3.523a.8.8 0 0 1-.097-.368c0-.22.086-.428.241-.584a.847.847 0 0 1 1.167 0m7.355 1.705c-.31-.461-.746-.758-1.23-.837a1.44 1.44 0 0 0-1.11.275c-.312.24-.505.543-.59.881a1.74 1.74 0 0 0-.906-.465 1.47 1.47 0 0 0-.82.106l-2.182-2.182a1.56 1.56 0 0 0-2.2 0 1.54 1.54 0 0 0-.396.701 1.56 1.56 0 0 0-2.21-.01 1.55 1.55 0 0 0-.416.753c-.624-.624-1.649-.624-2.237-.037a1.557 1.557 0 0 0 0 2.2c-.239.1-.501.238-.715.453a1.56 1.56 0 0 0 0 2.2l.516.515a1.556 1.556 0 0 0-.753 2.615L7.01 19c1.32 1.319 2.909 2.189 4.475 2.449q.482.08.971.08c.85 0 1.653-.198 2.393-.579.231.033.46.054.686.054 1.266 0 2.457-.52 3.505-1.567 2.763-2.763 2.552-5.734 1.439-7.586z" clip-rule="evenodd"></path></svg></div></div></div></a></span></div><div class="pw-multi-vote-count l li lj lk ll lm ln lo"><p class="bf b dv z du"><span class="lp">--</span></p></div></div></span><span class="l h g f rb rc"><div class="ab q kv kw"><div class="pw-multi-vote-icon fj je kx ky kz"><span><a class="af ag ah ai aj ak al am an ao ap aq ar as at" data-testid="footerClapButton" rel="noopener follow" href="/m/signin?actionUrl=https%3A%2F%2Fmedium.com%2F_%2Fvote%2Fairbnb-engineering%2F550d929126c8&operation=register&redirect=https%3A%2F%2Fmedium.com%2Fairbnb-engineering%2Fbetter-android-testing-at-airbnb-part-4-testing-viewmodels-550d929126c8&user=Eli+Hart&userId=9f3427a69792&source=---footer_actions--550d929126c8---------------------clap_footer-----------"><div><div class="bm" aria-hidden="false"><div class="la ao lb lc ld le am lf lg lh kz"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" aria-label="clap"><path fill-rule="evenodd" d="M11.37.828 12 3.282l.63-2.454zM13.916 3.953l1.523-2.112-1.184-.39zM8.589 1.84l1.522 2.112-.337-2.501zM18.523 18.92c-.86.86-1.75 1.246-2.62 1.33a6 6 0 0 0 .407-.372c2.388-2.389 2.86-4.951 1.399-7.623l-.912-1.603-.79-1.672c-.26-.56-.194-.98.203-1.288a.7.7 0 0 1 .546-.132c.283.046.546.231.728.5l2.363 4.157c.976 1.624 1.141 4.237-1.324 6.702m-10.999-.438L3.37 14.328a.828.828 0 0 1 .585-1.408.83.83 0 0 1 .585.242l2.158 2.157a.365.365 0 0 0 .516-.516l-2.157-2.158-1.449-1.449a.826.826 0 0 1 1.167-1.17l3.438 3.44a.363.363 0 0 0 .516 0 .364.364 0 0 0 0-.516L5.293 9.513l-.97-.97a.826.826 0 0 1 0-1.166.84.84 0 0 1 1.167 0l.97.968 3.437 3.436a.36.36 0 0 0 .517 0 .366.366 0 0 0 0-.516L6.977 7.83a.82.82 0 0 1-.241-.584.82.82 0 0 1 .824-.826c.219 0 .43.087.584.242l5.787 5.787a.366.366 0 0 0 .587-.415l-1.117-2.363c-.26-.56-.194-.98.204-1.289a.7.7 0 0 1 .546-.132c.283.046.545.232.727.501l2.193 3.86c1.302 2.38.883 4.59-1.277 6.75-1.156 1.156-2.602 1.627-4.19 1.367-1.418-.236-2.866-1.033-4.079-2.246M10.75 5.971l2.12 2.12c-.41.502-.465 1.17-.128 1.89l.22.465-3.523-3.523a.8.8 0 0 1-.097-.368c0-.22.086-.428.241-.584a.847.847 0 0 1 1.167 0m7.355 1.705c-.31-.461-.746-.758-1.23-.837a1.44 1.44 0 0 0-1.11.275c-.312.24-.505.543-.59.881a1.74 1.74 0 0 0-.906-.465 1.47 1.47 0 0 0-.82.106l-2.182-2.182a1.56 1.56 0 0 0-2.2 0 1.54 1.54 0 0 0-.396.701 1.56 1.56 0 0 0-2.21-.01 1.55 1.55 0 0 0-.416.753c-.624-.624-1.649-.624-2.237-.037a1.557 1.557 0 0 0 0 2.2c-.239.1-.501.238-.715.453a1.56 1.56 0 0 0 0 2.2l.516.515a1.556 1.556 0 0 0-.753 2.615L7.01 19c1.32 1.319 2.909 2.189 4.475 2.449q.482.08.971.08c.85 0 1.653-.198 2.393-.579.231.033.46.054.686.054 1.266 0 2.457-.52 3.505-1.567 2.763-2.763 2.552-5.734 1.439-7.586z" clip-rule="evenodd"></path></svg></div></div></div></a></span></div><div class="pw-multi-vote-count l li lj lk ll lm ln lo"><p class="bf b dv z du"><span class="lp">--</span></p></div></div></span></div><div class="bq ab"><div><div class="bm" aria-hidden="false"><button class="ao la lq lr ab q fk ls lt" aria-label="responses"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" class="lu"><path d="M18.006 16.803c1.533-1.456 2.234-3.325 2.234-5.321C20.24 7.357 16.709 4 12.191 4S4 7.357 4 11.482c0 4.126 3.674 7.482 8.191 7.482.817 0 1.622-.111 2.393-.327.231.2.48.391.744.559 1.06.693 2.203 1.044 3.399 1.044.224-.008.4-.112.486-.287a.49.49 0 0 0-.042-.518c-.495-.67-.845-1.364-1.04-2.057a4 4 0 0 1-.125-.598zm-3.122 1.055-.067-.223-.315.096a8 8 0 0 1-2.311.338c-4.023 0-7.292-2.955-7.292-6.587 0-3.633 3.269-6.588 7.292-6.588 4.014 0 7.112 2.958 7.112 6.593 0 1.794-.608 3.469-2.027 4.72l-.195.168v.255c0 .056 0 .151.016.295.025.231.081.478.154.733.154.558.398 1.117.722 1.659a5.3 5.3 0 0 1-2.165-.845c-.276-.176-.714-.383-.941-.59z"></path></svg></button></div></div></div></div><div class="ab q"><div class="rd l ix"><div><div class="bm" aria-hidden="false"><span><a class="af ag ah ai aj ak al am an ao ap aq ar as at" data-testid="footerBookmarkButton" rel="noopener follow" href="/m/signin?actionUrl=https%3A%2F%2Fmedium.com%2F_%2Fbookmark%2Fp%2F550d929126c8&operation=register&redirect=https%3A%2F%2Fmedium.com%2Fairbnb-engineering%2Fbetter-android-testing-at-airbnb-part-4-testing-viewmodels-550d929126c8&source=---footer_actions--550d929126c8---------------------bookmark_footer-----------"><svg xmlns="http://www.w3.org/2000/svg" width="25" height="25" fill="none" viewBox="0 0 25 25" class="du lw" aria-label="Add to list bookmark button"><path fill="currentColor" d="M18 2.5a.5.5 0 0 1 1 0V5h2.5a.5.5 0 0 1 0 1H19v2.5a.5.5 0 1 1-1 0V6h-2.5a.5.5 0 0 1 0-1H18zM7 7a1 1 0 0 1 1-1h3.5a.5.5 0 0 0 0-1H8a2 2 0 0 0-2 2v14a.5.5 0 0 0 .805.396L12.5 17l5.695 4.396A.5.5 0 0 0 19 21v-8.5a.5.5 0 0 0-1 0v7.485l-5.195-4.012a.5.5 0 0 0-.61 0L7 19.985z"></path></svg></a></span></div></div></div><div class="rd l ix"><div class="bm" aria-hidden="false" aria-describedby="postFooterSocialMenu" aria-labelledby="postFooterSocialMenu"><div><div class="bm" aria-hidden="false"><button aria-controls="postFooterSocialMenu" aria-expanded="false" aria-label="Share Post" data-testid="footerSocialShareButton" class="af fk ah ai aj ak al me an ao ap ex mf mg lt mh"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24"><path fill="currentColor" fill-rule="evenodd" d="M15.218 4.931a.4.4 0 0 1-.118.132l.012.006a.45.45 0 0 1-.292.074.5.5 0 0 1-.3-.13l-2.02-2.02v7.07c0 .28-.23.5-.5.5s-.5-.22-.5-.5v-7.04l-2 2a.45.45 0 0 1-.57.04h-.02a.4.4 0 0 1-.16-.3.4.4 0 0 1 .1-.32l2.8-2.8a.5.5 0 0 1 .7 0l2.8 2.79a.42.42 0 0 1 .068.498m-.106.138.008.004v-.01zM16 7.063h1.5a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2h-11c-1.1 0-2-.9-2-2v-10a2 2 0 0 1 2-2H8a.5.5 0 0 1 .35.15.5.5 0 0 1 .15.35.5.5 0 0 1-.15.35.5.5 0 0 1-.35.15H6.4c-.5 0-.9.4-.9.9v10.2a.9.9 0 0 0 .9.9h11.2c.5 0 .9-.4.9-.9v-10.2c0-.5-.4-.9-.9-.9H16a.5.5 0 0 1 0-1" clip-rule="evenodd"></path></svg></button></div></div></div></div></div></div></div></div></div></footer><div class="re rf rg rh ri l"><div class="ab cb"><div class="ci bh fz ga gb gc"><div class="rj bh r rk"></div><div class="rl l"><div class="ab rm rn ro iz iy"><div class="rp rq rr rs rt ru rv rw rx ry ab cp"><div class="h k"><a href="https://medium.com/airbnb-engineering?source=post_page---post_publication_info--550d929126c8--------------------------------" rel="noopener follow"><div class="fj ab"><img alt="The Airbnb Tech Blog" class="rz ib ic cx" src="https://miro.medium.com/v2/resize:fill:96:96/1*MlNQKg-sieBGW5prWoe9HQ.jpeg" width="48" height="48" loading="lazy"/><div class="rz l ic ib fs n fr sa"></div></div></a></div><div class="j i d"><a href="https://medium.com/airbnb-engineering?source=post_page---post_publication_info--550d929126c8--------------------------------" rel="noopener follow"><div class="fj ab"><img alt="The Airbnb Tech Blog" class="rz sc sb cx" src="https://miro.medium.com/v2/resize:fill:128:128/1*MlNQKg-sieBGW5prWoe9HQ.jpeg" width="64" height="64" loading="lazy"/><div class="rz l sb sc fs n fr sa"></div></div></a></div><div class="j i d sd ix"><div class="ab"></div></div></div><div class="ab co se"><div class="sf sg sh si sj l"><a class="af ag ah aj ak al am an ao ap aq ar as at ab q" href="https://medium.com/airbnb-engineering?source=post_page---post_publication_info--550d929126c8--------------------------------" rel="noopener follow"><h2 class="pw-author-name bf sl sm sn so sp sq sr nh py pz nl qb qc np qe qf bk"><span class="gn sk">Published in <!-- -->The Airbnb Tech Blog</span></h2></a><div class="qj ab ia"><div class="l ix"><span class="pw-follower-count bf b bg z du"><a class="af ag ah ai aj ak al am an ao ap aq ar iq" rel="noopener follow" href="/airbnb-engineering/followers?source=post_page---post_publication_info--550d929126c8--------------------------------">148K Followers</a></span></div><div class="bf b bg z du ab jd"><span class="ir l" aria-hidden="true"><span class="bf b bg z du">·</span></span><a class="af ag ah ai aj ak al am an ao ap aq ar iq" rel="noopener follow" href="/airbnb-engineering/building-a-user-signals-platform-at-airbnb-b236078ec82b?source=post_page---post_publication_info--550d929126c8--------------------------------">Last published <!-- -->2 days ago</a></div></div><div class="ss l"><p class="bf b bg z bk">Creative engineers and data scientists building a world where you can belong anywhere. <a class="af ag ah ai aj ak al am an ao ap aq ar oc go" href="http://airbnb.io" rel="noopener ugc nofollow">http://airbnb.io</a></p></div></div></div><div class="h k"><div class="ab"></div></div></div></div><div class="ab rm rn ro iz iy"><div class="rp rq rr rs rt ru rv rw rx ry ab cp"><div class="h k"><a tabindex="0" rel="noopener follow" href="/@konakid?source=post_page---post_author_info--550d929126c8--------------------------------"><div class="l fj"><img alt="Eli Hart" class="l fd by ic ib cx" src="https://miro.medium.com/v2/resize:fill:96:96/2*qR91fuLzUz5PI59hjTTcRQ.jpeg" width="48" height="48" loading="lazy"/><div class="fr by l ic ib fs n ay sa"></div></div></a></div><div class="j i d"><a tabindex="0" rel="noopener follow" href="/@konakid?source=post_page---post_author_info--550d929126c8--------------------------------"><div class="l fj"><img alt="Eli Hart" class="l fd by sb sc cx" src="https://miro.medium.com/v2/resize:fill:128:128/2*qR91fuLzUz5PI59hjTTcRQ.jpeg" width="64" height="64" loading="lazy"/><div class="fr by l sb sc fs n ay sa"></div></div></a></div><div class="j i d sd ix"><div class="ab"><span><button class="bf b bg z st ql su sv sw sx sy ev ew sz ta tb fa fb fc fd bm fe ff">Follow</button></span></div></div></div><div class="ab co se"><div class="sf sg sh si sj l"><a class="af ag ah aj ak al am an ao ap aq ar as at ab q" rel="noopener follow" href="/@konakid?source=post_page---post_author_info--550d929126c8--------------------------------"><h2 class="pw-author-name bf sl sm sn so sp sq sr nh py pz nl qb qc np qe qf bk"><span class="gn sk">Written by <!-- -->Eli Hart</span></h2></a><div class="qj ab ia"><div class="l ix"><span class="pw-follower-count bf b bg z du"><a class="af ag ah ai aj ak al am an ao ap aq ar iq" rel="noopener follow" href="/@konakid/followers?source=post_page---post_author_info--550d929126c8--------------------------------">383 Followers</a></span></div><div class="bf b bg z du ab jd"><span class="ir l" aria-hidden="true"><span class="bf b bg z du">·</span></span><a class="af ag ah ai aj ak al am an ao ap aq ar iq" rel="noopener follow" href="/@konakid/following?source=post_page---post_author_info--550d929126c8--------------------------------">1 Following</a></div></div><div class="ss l"><p class="bf b bg z bk"><span class="gn">Android Engineer @ Airbnb</span></p></div></div></div><div class="h k"><div class="ab"><span><button class="bf b bg z st ql su sv sw sx sy ev ew sz ta tb fa fb fc fd bm fe ff">Follow</button></span></div></div></div></div></div></div><div class="tc l"><div class="rj bh r td te tf tg th"></div><div class="ab cb"><div class="ci bh fz ga gb gc"><div class="ab q cp"><h2 class="bf sl og oi oj ok om on oo oq or os ou ov ow oy oz bk">No responses yet</h2><div class="ab ti"><div><div class="bm" aria-hidden="false"><a class="tj tk" href="https://policy.medium.com/medium-rules-30e5502c4eb4?source=post_page---post_responses--550d929126c8--------------------------------" rel="noopener follow" target="_blank"><svg xmlns="http://www.w3.org/2000/svg" width="25" height="25" viewBox="0 0 25 25"><path fill-rule="evenodd" d="M11.987 5.036a.754.754 0 0 1 .914-.01c.972.721 1.767 1.218 2.6 1.543.828.322 1.719.485 2.887.505a.755.755 0 0 1 .741.757c-.018 3.623-.43 6.256-1.449 8.21-1.034 1.984-2.662 3.209-4.966 4.083a.75.75 0 0 1-.537-.003c-2.243-.874-3.858-2.095-4.897-4.074-1.024-1.951-1.457-4.583-1.476-8.216a.755.755 0 0 1 .741-.757c1.195-.02 2.1-.182 2.923-.503.827-.322 1.6-.815 2.519-1.535m.468.903c-.897.69-1.717 1.21-2.623 1.564-.898.35-1.856.527-3.026.565.037 3.45.469 5.817 1.36 7.515.884 1.684 2.25 2.762 4.284 3.571 2.092-.81 3.465-1.89 4.344-3.575.886-1.698 1.299-4.065 1.334-7.512-1.149-.039-2.091-.217-2.99-.567-.906-.353-1.745-.873-2.683-1.561m-.009 9.155a2.672 2.672 0 1 0 0-5.344 2.672 2.672 0 0 0 0 5.344m0 1a3.672 3.672 0 1 0 0-7.344 3.672 3.672 0 0 0 0 7.344m-1.813-3.777.525-.526.916.917 1.623-1.625.526.526-2.149 2.152z" clip-rule="evenodd"></path></svg></a></div></div></div></div></div></div></div><div class="tl tm tn to tp l bx"><div class="h k j"><div class="rj bh tq tr"></div><div class="ab cb"><div class="ci bh fz ga gb gc"><div class="ts ab kv ja"><div class="tt tu l"><a class="af ag ah ai aj ak al am an ao ap aq ar as at" href="https://help.medium.com/hc/en-us?source=post_page-----550d929126c8--------------------------------" rel="noopener follow"><p class="bf b dv z du">Help</p></a></div><div class="tt tu l"><a class="af ag ah ai aj ak al am an ao ap aq ar as at" href="https://medium.statuspage.io/?source=post_page-----550d929126c8--------------------------------" rel="noopener follow"><p class="bf b dv z du">Status</p></a></div><div class="tt tu l"><a class="af ag ah ai aj ak al am an ao ap aq ar as at" rel="noopener follow" href="/about?autoplay=1&source=post_page-----550d929126c8--------------------------------"><p class="bf b dv z du">About</p></a></div><div class="tt tu l"><a class="af ag ah ai aj ak al am an ao ap aq ar as at" rel="noopener follow" href="/jobs-at-medium/work-at-medium-959d1a85284e?source=post_page-----550d929126c8--------------------------------"><p class="bf b dv z du">Careers</p></a></div><div class="tt tu l"><a class="af ag ah ai aj ak al am an ao ap aq ar as at" href="pressinquiries@medium.com?source=post_page-----550d929126c8--------------------------------" rel="noopener follow"><p class="bf b dv z du">Press</p></a></div><div class="tt tu l"><a class="af ag ah ai aj ak al am an ao ap aq ar as at" href="https://blog.medium.com/?source=post_page-----550d929126c8--------------------------------" rel="noopener follow"><p class="bf b dv z du">Blog</p></a></div><div class="tt tu l"><a class="af ag ah ai aj ak al am an ao ap aq ar as at" href="https://policy.medium.com/medium-privacy-policy-f03bf92035c9?source=post_page-----550d929126c8--------------------------------" rel="noopener follow"><p class="bf b dv z du">Privacy</p></a></div><div class="tt tu l"><a class="af ag ah ai aj ak al am an ao ap aq ar as at" href="https://policy.medium.com/medium-terms-of-service-9db0094a1e0f?source=post_page-----550d929126c8--------------------------------" rel="noopener follow"><p class="bf b dv z du">Terms</p></a></div><div class="tt tu l"><a class="af ag ah ai aj ak al am an ao ap aq ar as at" href="https://speechify.com/medium?source=post_page-----550d929126c8--------------------------------" rel="noopener follow"><p class="bf b dv z du">Text to speech</p></a></div><div class="tt l"><a class="af ag ah ai aj ak al am an ao ap aq ar as at" rel="noopener follow" href="/business?source=post_page-----550d929126c8--------------------------------"><p class="bf b dv z du">Teams</p></a></div></div></div></div></div></div></div></div></div></div><script>window.__BUILD_ID__="main-20241122-185319-7bcdc08639"</script><script>window.__GRAPHQL_URI__ = "https://medium.com/_/graphql"</script><script>window.__PRELOADED_STATE__ = {"algolia":{"queries":{}},"cache":{"experimentGroupSet":true,"reason":"","group":"enabled","tags":["group-edgeCachePosts","post-550d929126c8","user-9f3427a69792","collection-53c7c27702d5"],"serverVariantState":"44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a","middlewareEnabled":true,"cacheStatus":"DYNAMIC","shouldUseCache":true,"vary":[],"lohpSummerUpsellEnabled":false,"publicationHierarchyEnabledWeb":false,"postBottomResponsesEnabled":false},"client":{"hydrated":false,"isUs":false,"isNativeMedium":false,"isSafariMobile":false,"isSafari":false,"isFirefox":false,"routingEntity":{"type":"DEFAULT","explicit":false},"viewerIsBot":false},"debug":{"requestId":"8cf3bbe5-9865-4eef-93eb-391803f76523","hybridDevServices":[],"originalSpanCarrier":{"traceparent":"00-91100a9fda35ddc685d98fca61fc772b-d56e190dd07a31a7-01"}},"multiVote":{"clapsPerPost":{}},"navigation":{"branch":{"show":null,"hasRendered":null,"blockedByCTA":false},"hideGoogleOneTap":false,"hasRenderedAlternateUserBanner":null,"currentLocation":"https:\u002F\u002Fmedium.com\u002Fairbnb-engineering\u002Fbetter-android-testing-at-airbnb-part-4-testing-viewmodels-550d929126c8","host":"medium.com","hostname":"medium.com","referrer":"","hasSetReferrer":false,"susiModal":{"step":null,"operation":"register"},"postRead":false,"partnerProgram":{"selectedCountryCode":null},"queryString":"","currentHash":""},"config":{"nodeEnv":"production","version":"main-20241122-185319-7bcdc08639","target":"production","productName":"Medium","publicUrl":"https:\u002F\u002Fcdn-client.medium.com\u002Flite","authDomain":"medium.com","authGoogleClientId":"216296035834-k1k6qe060s2tp2a2jam4ljdcms00sttg.apps.googleusercontent.com","favicon":"production","glyphUrl":"https:\u002F\u002Fglyph.medium.com","branchKey":"key_live_ofxXr2qTrrU9NqURK8ZwEhknBxiI6KBm","algolia":{"appId":"MQ57UUUQZ2","apiKeySearch":"394474ced050e3911ae2249ecc774921","indexPrefix":"medium_","host":"-dsn.algolia.net"},"recaptchaKey":"6Lfc37IUAAAAAKGGtC6rLS13R1Hrw_BqADfS1LRk","recaptcha3Key":"6Lf8R9wUAAAAABMI_85Wb8melS7Zj6ziuf99Yot5","recaptchaEnterpriseKeyId":"6Le-uGgpAAAAAPprRaokM8AKthQ9KNGdoxaGUvVp","datadog":{"applicationId":"6702d87d-a7e0-42fe-bbcb-95b469547ea0","clientToken":"pub853ea8d17ad6821d9f8f11861d23dfed","rumToken":"pubf9cc52896502b9413b68ba36fc0c7162","context":{"deployment":{"target":"production","tag":"main-20241122-185319-7bcdc08639","commit":"7bcdc08639c179dc5172558201a3fd3abc1b5db6"}},"datacenter":"us"},"googleAnalyticsCode":"G-7JY7T788PK","googlePay":{"apiVersion":"2","apiVersionMinor":"0","merchantId":"BCR2DN6TV7EMTGBM","merchantName":"Medium","instanceMerchantId":"13685562959212738550"},"applePay":{"version":3},"signInWallCustomDomainCollectionIds":["3a8144eabfe3","336d898217ee","61061eb0c96b","138adf9c44c","819cc2aaeee0"],"mediumMastodonDomainName":"me.dm","mediumOwnedAndOperatedCollectionIds":["8a9336e5bb4","b7e45b22fec3","193b68bd4fba","8d6b8a439e32","54c98c43354d","3f6ecf56618","d944778ce714","92d2092dc598","ae2a65f35510","1285ba81cada","544c7006046e","fc8964313712","40187e704f1c","88d9857e584e","7b6769f2748b","bcc38c8f6edf","cef6983b292","cb8577c9149e","444d13b52878","713d7dbc99b0","ef8e90590e66","191186aaafa0","55760f21cdc5","9dc80918cc93","bdc4052bbdba","8ccfed20cbb2"],"tierOneDomains":["medium.com","thebolditalic.com","arcdigital.media","towardsdatascience.com","uxdesign.cc","codeburst.io","psiloveyou.xyz","writingcooperative.com","entrepreneurshandbook.co","prototypr.io","betterhumans.coach.me","theascent.pub"],"topicsToFollow":["d61cf867d93f","8a146bc21b28","1eca0103fff3","4d562ee63426","aef1078a3ef5","e15e46793f8d","6158eb913466","55f1c20aba7a","3d18b94f6858","4861fee224fd","63c6f1f93ee","1d98b3a9a871","decb52b64abf","ae5d4995e225","830cded25262"],"topicToTagMappings":{"accessibility":"accessibility","addiction":"addiction","android-development":"android-development","art":"art","artificial-intelligence":"artificial-intelligence","astrology":"astrology","basic-income":"basic-income","beauty":"beauty","biotech":"biotech","blockchain":"blockchain","books":"books","business":"business","cannabis":"cannabis","cities":"cities","climate-change":"climate-change","comics":"comics","coronavirus":"coronavirus","creativity":"creativity","cryptocurrency":"cryptocurrency","culture":"culture","cybersecurity":"cybersecurity","data-science":"data-science","design":"design","digital-life":"digital-life","disability":"disability","economy":"economy","education":"education","equality":"equality","family":"family","feminism":"feminism","fiction":"fiction","film":"film","fitness":"fitness","food":"food","freelancing":"freelancing","future":"future","gadgets":"gadgets","gaming":"gaming","gun-control":"gun-control","health":"health","history":"history","humor":"humor","immigration":"immigration","ios-development":"ios-development","javascript":"javascript","justice":"justice","language":"language","leadership":"leadership","lgbtqia":"lgbtqia","lifestyle":"lifestyle","machine-learning":"machine-learning","makers":"makers","marketing":"marketing","math":"math","media":"media","mental-health":"mental-health","mindfulness":"mindfulness","money":"money","music":"music","neuroscience":"neuroscience","nonfiction":"nonfiction","outdoors":"outdoors","parenting":"parenting","pets":"pets","philosophy":"philosophy","photography":"photography","podcasts":"podcast","poetry":"poetry","politics":"politics","privacy":"privacy","product-management":"product-management","productivity":"productivity","programming":"programming","psychedelics":"psychedelics","psychology":"psychology","race":"race","relationships":"relationships","religion":"religion","remote-work":"remote-work","san-francisco":"san-francisco","science":"science","self":"self","self-driving-cars":"self-driving-cars","sexuality":"sexuality","social-media":"social-media","society":"society","software-engineering":"software-engineering","space":"space","spirituality":"spirituality","sports":"sports","startups":"startup","style":"style","technology":"technology","transportation":"transportation","travel":"travel","true-crime":"true-crime","tv":"tv","ux":"ux","venture-capital":"venture-capital","visual-design":"visual-design","work":"work","world":"world","writing":"writing"},"defaultImages":{"avatar":{"imageId":"1*dmbNkD5D-u45r44go_cf0g.png","height":150,"width":150},"orgLogo":{"imageId":"7*V1_7XP4snlmqrc_0Njontw.png","height":110,"width":500},"postLogo":{"imageId":"bd978bb536350a710e8efb012513429cabdc4c28700604261aeda246d0f980b7","height":810,"width":1440},"postPreviewImage":{"imageId":"1*hn4v1tCaJy7cWMyb0bpNpQ.png","height":386,"width":579}},"collectionStructuredData":{"8d6b8a439e32":{"name":"Elemental","data":{"@type":"NewsMediaOrganization","ethicsPolicy":"https:\u002F\u002Fhelp.medium.com\u002Fhc\u002Fen-us\u002Farticles\u002F360043290473","logo":{"@type":"ImageObject","url":"https:\u002F\u002Fcdn-images-1.medium.com\u002Fmax\u002F980\u002F1*9ygdqoKprhwuTVKUM0DLPA@2x.png","width":980,"height":159}}},"3f6ecf56618":{"name":"Forge","data":{"@type":"NewsMediaOrganization","ethicsPolicy":"https:\u002F\u002Fhelp.medium.com\u002Fhc\u002Fen-us\u002Farticles\u002F360043290473","logo":{"@type":"ImageObject","url":"https:\u002F\u002Fcdn-images-1.medium.com\u002Fmax\u002F596\u002F1*uULpIlImcO5TDuBZ6lm7Lg@2x.png","width":596,"height":183}}},"ae2a65f35510":{"name":"GEN","data":{"@type":"NewsMediaOrganization","ethicsPolicy":"https:\u002F\u002Fhelp.medium.com\u002Fhc\u002Fen-us\u002Farticles\u002F360043290473","logo":{"@type":"ImageObject","url":"https:\u002F\u002Fmiro.medium.com\u002Fmax\u002F264\u002F1*RdVZMdvfV3YiZTw6mX7yWA.png","width":264,"height":140}}},"88d9857e584e":{"name":"LEVEL","data":{"@type":"NewsMediaOrganization","ethicsPolicy":"https:\u002F\u002Fhelp.medium.com\u002Fhc\u002Fen-us\u002Farticles\u002F360043290473","logo":{"@type":"ImageObject","url":"https:\u002F\u002Fmiro.medium.com\u002Fmax\u002F540\u002F1*JqYMhNX6KNNb2UlqGqO2WQ.png","width":540,"height":108}}},"7b6769f2748b":{"name":"Marker","data":{"@type":"NewsMediaOrganization","ethicsPolicy":"https:\u002F\u002Fhelp.medium.com\u002Fhc\u002Fen-us\u002Farticles\u002F360043290473","logo":{"@type":"ImageObject","url":"https:\u002F\u002Fcdn-images-1.medium.com\u002Fmax\u002F383\u002F1*haCUs0wF6TgOOvfoY-jEoQ@2x.png","width":383,"height":92}}},"444d13b52878":{"name":"OneZero","data":{"@type":"NewsMediaOrganization","ethicsPolicy":"https:\u002F\u002Fhelp.medium.com\u002Fhc\u002Fen-us\u002Farticles\u002F360043290473","logo":{"@type":"ImageObject","url":"https:\u002F\u002Fmiro.medium.com\u002Fmax\u002F540\u002F1*cw32fIqCbRWzwJaoQw6BUg.png","width":540,"height":123}}},"8ccfed20cbb2":{"name":"Zora","data":{"@type":"NewsMediaOrganization","ethicsPolicy":"https:\u002F\u002Fhelp.medium.com\u002Fhc\u002Fen-us\u002Farticles\u002F360043290473","logo":{"@type":"ImageObject","url":"https:\u002F\u002Fmiro.medium.com\u002Fmax\u002F540\u002F1*tZUQqRcCCZDXjjiZ4bDvgQ.png","width":540,"height":106}}}},"embeddedPostIds":{"coronavirus":"cd3010f9d81f"},"sharedCdcMessaging":{"COVID_APPLICABLE_TAG_SLUGS":[],"COVID_APPLICABLE_TOPIC_NAMES":[],"COVID_APPLICABLE_TOPIC_NAMES_FOR_TOPIC_PAGE":[],"COVID_MESSAGES":{"tierA":{"text":"For more information on the novel coronavirus and Covid-19, visit cdc.gov.","markups":[{"start":66,"end":73,"href":"https:\u002F\u002Fwww.cdc.gov\u002Fcoronavirus\u002F2019-nCoV"}]},"tierB":{"text":"Anyone can publish on Medium per our Policies, but we don’t fact-check every story. For more info about the coronavirus, see cdc.gov.","markups":[{"start":37,"end":45,"href":"https:\u002F\u002Fhelp.medium.com\u002Fhc\u002Fen-us\u002Fcategories\u002F201931128-Policies-Safety"},{"start":125,"end":132,"href":"https:\u002F\u002Fwww.cdc.gov\u002Fcoronavirus\u002F2019-nCoV"}]},"paywall":{"text":"This article has been made free for everyone, thanks to Medium Members. For more information on the novel coronavirus and Covid-19, visit cdc.gov.","markups":[{"start":56,"end":70,"href":"https:\u002F\u002Fmedium.com\u002Fmembership"},{"start":138,"end":145,"href":"https:\u002F\u002Fwww.cdc.gov\u002Fcoronavirus\u002F2019-nCoV"}]},"unbound":{"text":"This article is free for everyone, thanks to Medium Members. For more information on the novel coronavirus and Covid-19, visit cdc.gov.","markups":[{"start":45,"end":59,"href":"https:\u002F\u002Fmedium.com\u002Fmembership"},{"start":127,"end":134,"href":"https:\u002F\u002Fwww.cdc.gov\u002Fcoronavirus\u002F2019-nCoV"}]}},"COVID_BANNER_POST_ID_OVERRIDE_WHITELIST":["3b31a67bff4a"]},"sharedVoteMessaging":{"TAGS":["politics","election-2020","government","us-politics","election","2020-presidential-race","trump","donald-trump","democrats","republicans","congress","republican-party","democratic-party","biden","joe-biden","maga"],"TOPICS":["politics","election"],"MESSAGE":{"text":"Find out more about the U.S. election results here.","markups":[{"start":46,"end":50,"href":"https:\u002F\u002Fcookpolitical.com\u002F2020-national-popular-vote-tracker"}]},"EXCLUDE_POSTS":["397ef29e3ca5"]},"embedPostRules":[],"recircOptions":{"v1":{"limit":3},"v2":{"limit":8}},"braintreeClientKey":"production_zjkj96jm_m56f8fqpf7ngnrd4","braintree":{"enabled":true,"merchantId":"m56f8fqpf7ngnrd4","merchantAccountId":{"usd":"AMediumCorporation_instant","eur":"amediumcorporation_EUR","cad":"amediumcorporation_CAD"},"publicKey":"ds2nn34bg2z7j5gd","braintreeEnvironment":"production","dashboardUrl":"https:\u002F\u002Fwww.braintreegateway.com\u002Fmerchants","gracePeriodDurationInDays":14,"mediumMembershipPlanId":{"monthly":"ce105f8c57a3","monthlyV2":"e8a5e126-792b-4ee6-8fba-d574c1b02fc5","monthlyWithTrial":"d5ee3dbe3db8","monthlyPremium":"fa741a9b47a2","yearly":"a40ad4a43185","yearlyV2":"3815d7d6-b8ca-4224-9b8c-182f9047866e","yearlyStaff":"d74fb811198a","yearlyWithTrial":"b3bc7350e5c7","yearlyPremium":"e21bd2c12166","monthlyOneYearFree":"e6c0637a-2bad-4171-ab4f-3c268633d83c","monthly25PercentOffFirstYear":"235ecc62-0cdb-49ae-9378-726cd21c504b","monthly20PercentOffFirstYear":"ba518864-9c13-4a99-91ca-411bf0cac756","monthly15PercentOffFirstYear":"594c029b-9f89-43d5-88f8-8173af4e070e","monthly10PercentOffFirstYear":"c6c7bc9a-40f2-4b51-8126-e28511d5bdb0","monthlyForStudents":"629ebe51-da7d-41fd-8293-34cd2f2030a8","yearlyOneYearFree":"78ba7be9-0d9f-4ece-aa3e-b54b826f2bf1","yearly25PercentOffFirstYear":"2dbb010d-bb8f-4eeb-ad5c-a08509f42d34","yearly20PercentOffFirstYear":"47565488-435b-47f8-bf93-40d5fbe0ebc8","yearly15PercentOffFirstYear":"8259809b-0881-47d9-acf7-6c001c7f720f","yearly10PercentOffFirstYear":"9dd694fb-96e1-472c-8d9e-3c868d5c1506","yearlyForStudents":"e29345ef-ab1c-4234-95c5-70e50fe6bc23","monthlyCad":"p52orjkaceei","yearlyCad":"h4q9g2up9ktt"},"braintreeDiscountId":{"oneMonthFree":"MONTHS_FREE_01","threeMonthsFree":"MONTHS_FREE_03","sixMonthsFree":"MONTHS_FREE_06","fiftyPercentOffOneYear":"FIFTY_PERCENT_OFF_ONE_YEAR"},"3DSecureVersion":"2","defaultCurrency":"usd","providerPlanIdCurrency":{"4ycw":"usd","rz3b":"usd","3kqm":"usd","jzw6":"usd","c2q2":"usd","nnsw":"usd","q8qw":"usd","d9y6":"usd","fx7w":"cad","nwf2":"cad"}},"paypalClientId":"AXj1G4fotC2GE8KzWX9mSxCH1wmPE3nJglf4Z2ig_amnhvlMVX87otaq58niAg9iuLktVNF_1WCMnN7v","paypal":{"host":"https:\u002F\u002Fapi.paypal.com:443","clientMode":"production","serverMode":"live","webhookId":"4G466076A0294510S","monthlyPlan":{"planId":"P-9WR0658853113943TMU5FDQA","name":"Medium Membership (Monthly) with setup fee","description":"Unlimited access to the best and brightest stories on Medium. Membership billed monthly."},"yearlyPlan":{"planId":"P-7N8963881P8875835MU5JOPQ","name":"Medium Membership (Annual) with setup fee","description":"Unlimited access to the best and brightest stories on Medium. Membership billed annually."},"oneYearGift":{"name":"Medium Membership (1 Year, Digital Gift Code)","description":"Unlimited access to the best and brightest stories on Medium. Gift codes can be redeemed at medium.com\u002Fredeem.","price":"50.00","currency":"USD","sku":"membership-gift-1-yr"},"oldMonthlyPlan":{"planId":"P-96U02458LM656772MJZUVH2Y","name":"Medium Membership (Monthly)","description":"Unlimited access to the best and brightest stories on Medium. Membership billed monthly."},"oldYearlyPlan":{"planId":"P-59P80963JF186412JJZU3SMI","name":"Medium Membership (Annual)","description":"Unlimited access to the best and brightest stories on Medium. Membership billed annually."},"monthlyPlanWithTrial":{"planId":"P-66C21969LR178604GJPVKUKY","name":"Medium Membership (Monthly) with setup fee","description":"Unlimited access to the best and brightest stories on Medium. Membership billed monthly."},"yearlyPlanWithTrial":{"planId":"P-6XW32684EX226940VKCT2MFA","name":"Medium Membership (Annual) with setup fee","description":"Unlimited access to the best and brightest stories on Medium. Membership billed annually."},"oldMonthlyPlanNoSetupFee":{"planId":"P-4N046520HR188054PCJC7LJI","name":"Medium Membership (Monthly)","description":"Unlimited access to the best and brightest stories on Medium. Membership billed monthly."},"oldYearlyPlanNoSetupFee":{"planId":"P-7A4913502Y5181304CJEJMXQ","name":"Medium Membership (Annual)","description":"Unlimited access to the best and brightest stories on Medium. Membership billed annually."},"sdkUrl":"https:\u002F\u002Fwww.paypal.com\u002Fsdk\u002Fjs"},"stripePublishableKey":"pk_live_7FReX44VnNIInZwrIIx6ghjl","log":{"json":true,"level":"info"},"imageUploadMaxSizeMb":25,"staffPicks":{"title":"Staff Picks","catalogId":"c7bc6e1ee00f"}},"session":{"xsrf":""}}</script><script>window.__APOLLO_STATE__ = {"ROOT_QUERY":{"__typename":"Query","viewer":null,"collectionByDomainOrSlug({\"domainOrSlug\":\"airbnb-engineering\"})":{"__ref":"Collection:53c7c27702d5"},"postResult({\"id\":\"550d929126c8\"})":{"__ref":"Post:550d929126c8"}},"Collection:53c7c27702d5":{"__typename":"Collection","id":"53c7c27702d5","customStyleSheet":null,"colorPalette":{"__typename":"ColorPalette","highlightSpectrum":{"__typename":"ColorSpectrum","backgroundColor":"#FFFFFFFF","colorPoints":[{"__typename":"ColorPoint","color":"#FFE4F7F8","point":0},{"__typename":"ColorPoint","color":"#FFDFF6F7","point":0.1},{"__typename":"ColorPoint","color":"#FFDAF6F6","point":0.2},{"__typename":"ColorPoint","color":"#FFD5F5F6","point":0.3},{"__typename":"ColorPoint","color":"#FFCFF4F5","point":0.4},{"__typename":"ColorPoint","color":"#FFC9F3F4","point":0.5},{"__typename":"ColorPoint","color":"#FFC3F2F4","point":0.6},{"__typename":"ColorPoint","color":"#FFBDF1F3","point":0.7},{"__typename":"ColorPoint","color":"#FFB7F0F3","point":0.8},{"__typename":"ColorPoint","color":"#FFB1F0F2","point":0.9},{"__typename":"ColorPoint","color":"#FFAAEFF1","point":1}]},"defaultBackgroundSpectrum":{"__typename":"ColorSpectrum","backgroundColor":"#FFFFFFFF","colorPoints":[{"__typename":"ColorPoint","color":"#FF30969A","point":0},{"__typename":"ColorPoint","color":"#FF338B8E","point":0.1},{"__typename":"ColorPoint","color":"#FF338083","point":0.2},{"__typename":"ColorPoint","color":"#FF337477","point":0.3},{"__typename":"ColorPoint","color":"#FF31696B","point":0.4},{"__typename":"ColorPoint","color":"#FF2F5D5F","point":0.5},{"__typename":"ColorPoint","color":"#FF2B5153","point":0.6},{"__typename":"ColorPoint","color":"#FF264546","point":0.7},{"__typename":"ColorPoint","color":"#FF203839","point":0.8},{"__typename":"ColorPoint","color":"#FF192B2C","point":0.9},{"__typename":"ColorPoint","color":"#FF101D1D","point":1}]},"tintBackgroundSpectrum":{"__typename":"ColorSpectrum","backgroundColor":"#FF007E82","colorPoints":[{"__typename":"ColorPoint","color":"#FF007E82","point":0},{"__typename":"ColorPoint","color":"#FF368D91","point":0.1},{"__typename":"ColorPoint","color":"#FF529C9F","point":0.2},{"__typename":"ColorPoint","color":"#FF6AAAAD","point":0.3},{"__typename":"ColorPoint","color":"#FF80B8BA","point":0.4},{"__typename":"ColorPoint","color":"#FF95C5C7","point":0.5},{"__typename":"ColorPoint","color":"#FFA9D2D4","point":0.6},{"__typename":"ColorPoint","color":"#FFBDDFE0","point":0.7},{"__typename":"ColorPoint","color":"#FFCFEBEC","point":0.8},{"__typename":"ColorPoint","color":"#FFE2F7F7","point":0.9},{"__typename":"ColorPoint","color":"#FFF4FFFF","point":1}]}},"favicon":{"__ref":"ImageMetadata:"},"domain":null,"slug":"airbnb-engineering","googleAnalyticsId":null,"editors":[{"__typename":"CollectionMastheadUserItem","user":{"__ref":"User:ebe93072cafd"}},{"__typename":"CollectionMastheadUserItem","user":{"__ref":"User:715069fb9693"}},{"__typename":"CollectionMastheadUserItem","user":{"__ref":"User:2c64fccbad80"}},{"__typename":"CollectionMastheadUserItem","user":{"__ref":"User:f4c9b6905436"}},{"__typename":"CollectionMastheadUserItem","user":{"__ref":"User:d06d84a4dee7"}},{"__typename":"CollectionMastheadUserItem","user":{"__ref":"User:ae9de0d76057"}},{"__typename":"CollectionMastheadUserItem","user":{"__ref":"User:8a8dba98ccda"}},{"__typename":"CollectionMastheadUserItem","user":{"__ref":"User:ba68c6ee8dae"}},{"__typename":"CollectionMastheadUserItem","user":{"__ref":"User:5315ce63140f"}},{"__typename":"CollectionMastheadUserItem","user":{"__ref":"User:23561a2a5df3"}}],"name":"The Airbnb Tech Blog","avatar":{"__ref":"ImageMetadata:1*MlNQKg-sieBGW5prWoe9HQ.jpeg"},"description":"Creative engineers and data scientists building a world where you can belong anywhere. http:\u002F\u002Fairbnb.io","subscriberCount":148662,"latestPostsConnection({\"paging\":{\"limit\":1}})":{"__typename":"PostConnection","posts":[{"__ref":"Post:b236078ec82b"}]},"viewerEdge":{"__ref":"CollectionViewerEdge:collectionId:53c7c27702d5-viewerId:lo_caae6e9abf64"},"twitterUsername":"AirbnbEng","facebookPageId":null,"logo":{"__ref":"ImageMetadata:1*JZl-TXoSiG0VmYn3qWLdTA.png"}},"ImageMetadata:":{"__typename":"ImageMetadata","id":""},"User:ebe93072cafd":{"__typename":"User","id":"ebe93072cafd"},"User:715069fb9693":{"__typename":"User","id":"715069fb9693"},"User:2c64fccbad80":{"__typename":"User","id":"2c64fccbad80"},"User:f4c9b6905436":{"__typename":"User","id":"f4c9b6905436"},"User:d06d84a4dee7":{"__typename":"User","id":"d06d84a4dee7"},"User:ae9de0d76057":{"__typename":"User","id":"ae9de0d76057"},"User:8a8dba98ccda":{"__typename":"User","id":"8a8dba98ccda"},"User:ba68c6ee8dae":{"__typename":"User","id":"ba68c6ee8dae"},"User:5315ce63140f":{"__typename":"User","id":"5315ce63140f"},"User:23561a2a5df3":{"__typename":"User","id":"23561a2a5df3"},"ImageMetadata:1*MlNQKg-sieBGW5prWoe9HQ.jpeg":{"__typename":"ImageMetadata","id":"1*MlNQKg-sieBGW5prWoe9HQ.jpeg"},"User:ffe99664019a":{"__typename":"User","id":"ffe99664019a","customDomainState":null,"hasSubdomain":false,"username":"kidaikwon36"},"Post:b236078ec82b":{"__typename":"Post","id":"b236078ec82b","firstPublishedAt":1732130847368,"creator":{"__ref":"User:ffe99664019a"},"collection":{"__ref":"Collection:53c7c27702d5"},"isSeries":false,"mediumUrl":"https:\u002F\u002Fmedium.com\u002Fairbnb-engineering\u002Fbuilding-a-user-signals-platform-at-airbnb-b236078ec82b","sequence":null,"uniqueSlug":"building-a-user-signals-platform-at-airbnb-b236078ec82b"},"LinkedAccounts:9f3427a69792":{"__typename":"LinkedAccounts","mastodon":null,"id":"9f3427a69792"},"UserViewerEdge:userId:9f3427a69792-viewerId:lo_caae6e9abf64":{"__typename":"UserViewerEdge","id":"userId:9f3427a69792-viewerId:lo_caae6e9abf64","isFollowing":false,"isUser":false,"isMuting":false},"NewsletterV3:ce325a1bb786":{"__typename":"NewsletterV3","id":"ce325a1bb786","type":"NEWSLETTER_TYPE_AUTHOR","slug":"9f3427a69792","name":"9f3427a69792","collection":null,"user":{"__ref":"User:9f3427a69792"}},"User:9f3427a69792":{"__typename":"User","id":"9f3427a69792","name":"Eli Hart","username":"konakid","newsletterV3":{"__ref":"NewsletterV3:ce325a1bb786"},"linkedAccounts":{"__ref":"LinkedAccounts:9f3427a69792"},"isSuspended":false,"imageId":"2*qR91fuLzUz5PI59hjTTcRQ.jpeg","mediumMemberAt":0,"verifications":{"__typename":"VerifiedInfo","isBookAuthor":false},"socialStats":{"__typename":"SocialStats","followerCount":383,"followingCount":1,"collectionFollowingCount":0},"customDomainState":null,"hasSubdomain":false,"bio":"Android Engineer @ Airbnb","isPartnerProgramEnrolled":false,"viewerEdge":{"__ref":"UserViewerEdge:userId:9f3427a69792-viewerId:lo_caae6e9abf64"},"viewerIsUser":false,"postSubscribeMembershipUpsellShownAt":0,"membership":null,"allowNotes":true,"twitterScreenName":""},"Topic:decb52b64abf":{"__typename":"Topic","slug":"programming","id":"decb52b64abf","name":"Programming"},"Paragraph:79f5c4786e38_0":{"__typename":"Paragraph","id":"79f5c4786e38_0","name":"1fc1","type":"H3","href":null,"layout":null,"metadata":null,"text":"Better Android Testing at Airbnb — Part 4: Testing ViewModels","hasDropCap":null,"dropCapImage":null,"markups":[],"codeBlockMetadata":null,"iframe":null,"mixtapeMetadata":null},"Paragraph:79f5c4786e38_1":{"__typename":"Paragraph","id":"79f5c4786e38_1","name":"2b68","type":"P","href":null,"layout":null,"metadata":null,"text":"In the fourth installment of our series on Android Testing at Airbnb, we look at a framework for unit testing logic in ViewModels.","hasDropCap":null,"dropCapImage":null,"markups":[{"__typename":"Markup","type":"EM","start":0,"end":130,"href":null,"anchorType":null,"userId":null,"linkMetadata":null}],"codeBlockMetadata":null,"iframe":null,"mixtapeMetadata":null},"ImageMetadata:1*SzUfC_eUHmH-O9CZwUDMtQ.jpeg":{"__typename":"ImageMetadata","id":"1*SzUfC_eUHmH-O9CZwUDMtQ.jpeg","originalHeight":5792,"originalWidth":8688,"focusPercentX":null,"focusPercentY":null,"alt":null},"Paragraph:79f5c4786e38_2":{"__typename":"Paragraph","id":"79f5c4786e38_2","name":"2b5a","type":"IMG","href":null,"layout":"FULL_WIDTH","metadata":{"__ref":"ImageMetadata:1*SzUfC_eUHmH-O9CZwUDMtQ.jpeg"},"text":"","hasDropCap":null,"dropCapImage":null,"markups":[],"codeBlockMetadata":null,"iframe":null,"mixtapeMetadata":null},"Paragraph:79f5c4786e38_3":{"__typename":"Paragraph","id":"79f5c4786e38_3","name":"6cc5","type":"P","href":null,"layout":null,"metadata":null,"text":"In part three of this series we saw how automated interaction testing can cover some of the code paths in our ViewModels by recording state changes. However, this can’t test all edge cases of our logic. ViewModel logic is crucial to the correct behavior of each screen, and thus is worth testing in more depth.","hasDropCap":null,"dropCapImage":null,"markups":[{"__typename":"Markup","type":"A","start":0,"end":13,"href":"https:\u002F\u002Fmedium.com\u002Fairbnb-engineering\u002Fbetter-android-testing-at-airbnb-1d1e91e489b4","anchorType":"LINK","userId":null,"linkMetadata":null}],"codeBlockMetadata":null,"iframe":null,"mixtapeMetadata":null},"Paragraph:79f5c4786e38_4":{"__typename":"Paragraph","id":"79f5c4786e38_4","name":"b3f3","type":"H3","href":null,"layout":null,"metadata":null,"text":"Unit Testing ViewModels","hasDropCap":null,"dropCapImage":null,"markups":[],"codeBlockMetadata":null,"iframe":null,"mixtapeMetadata":null},"Paragraph:79f5c4786e38_5":{"__typename":"Paragraph","id":"79f5c4786e38_5","name":"0f85","type":"P","href":null,"layout":null,"metadata":null,"text":"ViewModels are a case where we support manually written tests, but as usual we take an approach to minimize the overhead of manual testing by providing our developers with a unit test framework. This includes a DSL to simplify test statements, integration with our network stack to make assertions on executed requests, and a tie in with our state mocking system to easily set ViewModel states for testing.","hasDropCap":null,"dropCapImage":null,"markups":[],"codeBlockMetadata":null,"iframe":null,"mixtapeMetadata":null},"Paragraph:79f5c4786e38_6":{"__typename":"Paragraph","id":"79f5c4786e38_6","name":"0b4a","type":"P","href":null,"layout":null,"metadata":null,"text":"We developed the unit test framework based on a few core tenets:","hasDropCap":null,"dropCapImage":null,"markups":[],"codeBlockMetadata":null,"iframe":null,"mixtapeMetadata":null},"Paragraph:79f5c4786e38_7":{"__typename":"Paragraph","id":"79f5c4786e38_7","name":"060d","type":"ULI","href":null,"layout":null,"metadata":null,"text":"A ViewModel function should be independently testable. The design of the ViewModel should not rely on interactions between multiple function calls.","hasDropCap":null,"dropCapImage":null,"markups":[],"codeBlockMetadata":null,"iframe":null,"mixtapeMetadata":null},"Paragraph:79f5c4786e38_8":{"__typename":"Paragraph","id":"79f5c4786e38_8","name":"e814","type":"ULI","href":null,"layout":null,"metadata":null,"text":"The behavior of a function call should be purely defined by the ViewModel’s State when the function is invoked, and the parameters it is passed.","hasDropCap":null,"dropCapImage":null,"markups":[],"codeBlockMetadata":null,"iframe":null,"mixtapeMetadata":null},"Paragraph:79f5c4786e38_9":{"__typename":"Paragraph","id":"79f5c4786e38_9","name":"1f98","type":"ULI","href":null,"layout":null,"metadata":null,"text":"The output of the function should either be a new State set on the ViewModel, or a call out to a dependency.","hasDropCap":null,"dropCapImage":null,"markups":[],"codeBlockMetadata":null,"iframe":null,"mixtapeMetadata":null},"Paragraph:79f5c4786e38_10":{"__typename":"Paragraph","id":"79f5c4786e38_10","name":"148a","type":"P","href":null,"layout":null,"metadata":null,"text":"Given these tenets, our framework takes the following approach:","hasDropCap":null,"dropCapImage":null,"markups":[],"codeBlockMetadata":null,"iframe":null,"mixtapeMetadata":null},"Paragraph:79f5c4786e38_11":{"__typename":"Paragraph","id":"79f5c4786e38_11","name":"5e20","type":"ULI","href":null,"layout":null,"metadata":null,"text":"Each unit test invokes a single ViewModel function","hasDropCap":null,"dropCapImage":null,"markups":[],"codeBlockMetadata":null,"iframe":null,"mixtapeMetadata":null},"Paragraph:79f5c4786e38_12":{"__typename":"Paragraph","id":"79f5c4786e38_12","name":"7576","type":"ULI","href":null,"layout":null,"metadata":null,"text":"The input to the test is the initial state of the ViewModel, plus the parameters that are passed to the function","hasDropCap":null,"dropCapImage":null,"markups":[],"codeBlockMetadata":null,"iframe":null,"mixtapeMetadata":null},"Paragraph:79f5c4786e38_13":{"__typename":"Paragraph","id":"79f5c4786e38_13","name":"6f56","type":"ULI","href":null,"layout":null,"metadata":null,"text":"The output of the test is an assertion on what was changed in the state, and\u002For verification of an expected call to a dependency (via Mockito).","hasDropCap":null,"dropCapImage":null,"markups":[],"codeBlockMetadata":null,"iframe":null,"mixtapeMetadata":null},"Paragraph:79f5c4786e38_14":{"__typename":"Paragraph","id":"79f5c4786e38_14","name":"7366","type":"H3","href":null,"layout":null,"metadata":null,"text":"A Basic Example","hasDropCap":null,"dropCapImage":null,"markups":[],"codeBlockMetadata":null,"iframe":null,"mixtapeMetadata":null},"Paragraph:79f5c4786e38_15":{"__typename":"Paragraph","id":"79f5c4786e38_15","name":"c094","type":"P","href":null,"layout":null,"metadata":null,"text":"Let’s look at a basic ViewModel that stores an updatable String.","hasDropCap":null,"dropCapImage":null,"markups":[],"codeBlockMetadata":null,"iframe":null,"mixtapeMetadata":null},"MediaResource:c7f19c38b386a096cee2bbf8e76720a3":{"__typename":"MediaResource","id":"c7f19c38b386a096cee2bbf8e76720a3","iframeSrc":"","iframeHeight":0,"iframeWidth":0,"title":"MvRxViewModelExample"},"Paragraph:79f5c4786e38_16":{"__typename":"Paragraph","id":"79f5c4786e38_16","name":"3f3c","type":"IFRAME","href":null,"layout":"INSET_CENTER","metadata":null,"text":"","hasDropCap":null,"dropCapImage":null,"markups":[],"codeBlockMetadata":null,"iframe":{"__typename":"Iframe","mediaResource":{"__ref":"MediaResource:c7f19c38b386a096cee2bbf8e76720a3"}},"mixtapeMetadata":null},"Paragraph:79f5c4786e38_17":{"__typename":"Paragraph","id":"79f5c4786e38_17","name":"0467","type":"P","href":null,"layout":null,"metadata":null,"text":"The code to test the setText function looks like this:","hasDropCap":null,"dropCapImage":null,"markups":[{"__typename":"Markup","type":"STRONG","start":21,"end":28,"href":null,"anchorType":null,"userId":null,"linkMetadata":null},{"__typename":"Markup","type":"EM","start":21,"end":28,"href":null,"anchorType":null,"userId":null,"linkMetadata":null}],"codeBlockMetadata":null,"iframe":null,"mixtapeMetadata":null},"MediaResource:655b82c9e9e586599fee0e624a0d8093":{"__typename":"MediaResource","id":"655b82c9e9e586599fee0e624a0d8093","iframeSrc":"","iframeHeight":0,"iframeWidth":0,"title":"MvRxViewModelUnitTestSimpleExample"},"Paragraph:79f5c4786e38_18":{"__typename":"Paragraph","id":"79f5c4786e38_18","name":"42a6","type":"IFRAME","href":null,"layout":"INSET_CENTER","metadata":null,"text":"","hasDropCap":null,"dropCapImage":null,"markups":[],"codeBlockMetadata":null,"iframe":{"__typename":"Iframe","mediaResource":{"__ref":"MediaResource:655b82c9e9e586599fee0e624a0d8093"}},"mixtapeMetadata":null},"Paragraph:79f5c4786e38_19":{"__typename":"Paragraph","id":"79f5c4786e38_19","name":"6566","type":"P","href":null,"layout":null,"metadata":null,"text":"This specifies a reference to the function that we are testing, the parameter to invoke the function with, and what state change we should expect as a result. Here we test that calling setText(“hello”) results in the text state being updated to “hello”.","hasDropCap":null,"dropCapImage":null,"markups":[{"__typename":"Markup","type":"STRONG","start":185,"end":201,"href":null,"anchorType":null,"userId":null,"linkMetadata":null},{"__typename":"Markup","type":"EM","start":185,"end":201,"href":null,"anchorType":null,"userId":null,"linkMetadata":null}],"codeBlockMetadata":null,"iframe":null,"mixtapeMetadata":null},"Paragraph:79f5c4786e38_20":{"__typename":"Paragraph","id":"79f5c4786e38_20","name":"8a32","type":"P","href":null,"layout":null,"metadata":null,"text":"The expectState function takes the initial State as a receiver and returns the expected output State. This returned State must match the output exactly, otherwise the tests fails and the framework prints out which properties were not equal. Effectively, expectState defines which properties are expected to change and what the new values should be. This prevents side-effects from being missed.","hasDropCap":null,"dropCapImage":null,"markups":[{"__typename":"Markup","type":"STRONG","start":4,"end":15,"href":null,"anchorType":null,"userId":null,"linkMetadata":null},{"__typename":"Markup","type":"STRONG","start":254,"end":265,"href":null,"anchorType":null,"userId":null,"linkMetadata":null},{"__typename":"Markup","type":"EM","start":4,"end":15,"href":null,"anchorType":null,"userId":null,"linkMetadata":null},{"__typename":"Markup","type":"EM","start":254,"end":265,"href":null,"anchorType":null,"userId":null,"linkMetadata":null}],"codeBlockMetadata":null,"iframe":null,"mixtapeMetadata":null},"Paragraph:79f5c4786e38_21":{"__typename":"Paragraph","id":"79f5c4786e38_21","name":"61b2","type":"H3","href":null,"layout":null,"metadata":null,"text":"Test Setup","hasDropCap":null,"dropCapImage":null,"markups":[],"codeBlockMetadata":null,"iframe":null,"mixtapeMetadata":null},"Paragraph:79f5c4786e38_22":{"__typename":"Paragraph","id":"79f5c4786e38_22","name":"91e1","type":"P","href":null,"layout":null,"metadata":null,"text":"The test framework takes care of initializing the ViewModel, collecting test statements, and checking assertions.","hasDropCap":null,"dropCapImage":null,"markups":[],"codeBlockMetadata":null,"iframe":null,"mixtapeMetadata":null},"Paragraph:79f5c4786e38_23":{"__typename":"Paragraph","id":"79f5c4786e38_23","name":"2757","type":"P","href":null,"layout":null,"metadata":null,"text":"Tests are run with a normal JUnit and Robolectric setup, and each test class corresponds to a single ViewModel. The class implements an interface which the test frameworks uses to initialize a new ViewModel for each test.","hasDropCap":null,"dropCapImage":null,"markups":[],"codeBlockMetadata":null,"iframe":null,"mixtapeMetadata":null},"Paragraph:79f5c4786e38_24":{"__typename":"Paragraph","id":"79f5c4786e38_24","name":"7f56","type":"P","href":null,"layout":null,"metadata":null,"text":"For example, the full test class for the above ViewModel would look like this:","hasDropCap":null,"dropCapImage":null,"markups":[],"codeBlockMetadata":null,"iframe":null,"mixtapeMetadata":null},"MediaResource:d8fed47860a51e1a4e5b17c1bd969c11":{"__typename":"MediaResource","id":"d8fed47860a51e1a4e5b17c1bd969c11","iframeSrc":"","iframeHeight":0,"iframeWidth":0,"title":"MvRxViewModelTestClass"},"Paragraph:79f5c4786e38_25":{"__typename":"Paragraph","id":"79f5c4786e38_25","name":"f9f6","type":"IFRAME","href":null,"layout":"INSET_CENTER","metadata":null,"text":"","hasDropCap":null,"dropCapImage":null,"markups":[],"codeBlockMetadata":null,"iframe":{"__typename":"Iframe","mediaResource":{"__ref":"MediaResource:d8fed47860a51e1a4e5b17c1bd969c11"}},"mixtapeMetadata":null},"Paragraph:79f5c4786e38_26":{"__typename":"Paragraph","id":"79f5c4786e38_26","name":"4b72","type":"P","href":null,"layout":null,"metadata":null,"text":"The test framework uses the buildViewModel() function to create a new ViewModel for each test.","hasDropCap":null,"dropCapImage":null,"markups":[{"__typename":"Markup","type":"STRONG","start":28,"end":44,"href":null,"anchorType":null,"userId":null,"linkMetadata":null},{"__typename":"Markup","type":"EM","start":28,"end":44,"href":null,"anchorType":null,"userId":null,"linkMetadata":null}],"codeBlockMetadata":null,"iframe":null,"mixtapeMetadata":null},"Paragraph:79f5c4786e38_27":{"__typename":"Paragraph","id":"79f5c4786e38_27","name":"3985","type":"P","href":null,"layout":null,"metadata":null,"text":"The initial state of the ViewModel can be a mock state reused from an existing mock for the screen. This allows screenshot tests, interaction tests, and ViewModel unit tests to all share the same underlying mock instance. This greatly reduces the work involved to setup tests, and if the State data structure changes the mock only needs to be updated in one place.","hasDropCap":null,"dropCapImage":null,"markups":[],"codeBlockMetadata":null,"iframe":null,"mixtapeMetadata":null},"Paragraph:79f5c4786e38_28":{"__typename":"Paragraph","id":"79f5c4786e38_28","name":"18c6","type":"H3","href":null,"layout":null,"metadata":null,"text":"Modifying State","hasDropCap":null,"dropCapImage":null,"markups":[],"codeBlockMetadata":null,"iframe":null,"mixtapeMetadata":null},"Paragraph:79f5c4786e38_29":{"__typename":"Paragraph","id":"79f5c4786e38_29","name":"cc91","type":"P","href":null,"layout":null,"metadata":null,"text":"If a test needs to use a modified version of the default state it can leverage our previously mentioned data class DSL to easily make changes to nested state.","hasDropCap":null,"dropCapImage":null,"markups":[],"codeBlockMetadata":null,"iframe":null,"mixtapeMetadata":null},"Paragraph:79f5c4786e38_30":{"__typename":"Paragraph","id":"79f5c4786e38_30","name":"d5f7","type":"P","href":null,"layout":null,"metadata":null,"text":"To demonstrate, let’s expand our example to be a bit more complicated. Now it has some additional state that allows us to track whether the text is bold.","hasDropCap":null,"dropCapImage":null,"markups":[],"codeBlockMetadata":null,"iframe":null,"mixtapeMetadata":null},"MediaResource:bc88f5b64a2d859a803ae99d2c73210b":{"__typename":"MediaResource","id":"bc88f5b64a2d859a803ae99d2c73210b","iframeSrc":"","iframeHeight":0,"iframeWidth":0,"title":"MvRxViewModelWithBoldOption"},"Paragraph:79f5c4786e38_31":{"__typename":"Paragraph","id":"79f5c4786e38_31","name":"1279","type":"IFRAME","href":null,"layout":"INSET_CENTER","metadata":null,"text":"","hasDropCap":null,"dropCapImage":null,"markups":[],"codeBlockMetadata":null,"iframe":{"__typename":"Iframe","mediaResource":{"__ref":"MediaResource:bc88f5b64a2d859a803ae99d2c73210b"}},"mixtapeMetadata":null},"Paragraph:79f5c4786e38_32":{"__typename":"Paragraph","id":"79f5c4786e38_32","name":"6bf1","type":"P","href":null,"layout":null,"metadata":null,"text":"Our test syntax to check the setBold function looks like this:","hasDropCap":null,"dropCapImage":null,"markups":[{"__typename":"Markup","type":"STRONG","start":29,"end":36,"href":null,"anchorType":null,"userId":null,"linkMetadata":null},{"__typename":"Markup","type":"EM","start":29,"end":36,"href":null,"anchorType":null,"userId":null,"linkMetadata":null}],"codeBlockMetadata":null,"iframe":null,"mixtapeMetadata":null},"MediaResource:ea9ac7395e2381a039cff870f04f1203":{"__typename":"MediaResource","id":"ea9ac7395e2381a039cff870f04f1203","iframeSrc":"","iframeHeight":0,"iframeWidth":0,"title":"MvRxViewModelBoldTest"},"Paragraph:79f5c4786e38_33":{"__typename":"Paragraph","id":"79f5c4786e38_33","name":"8dd6","type":"IFRAME","href":null,"layout":"INSET_CENTER","metadata":null,"text":"","hasDropCap":null,"dropCapImage":null,"markups":[],"codeBlockMetadata":null,"iframe":{"__typename":"Iframe","mediaResource":{"__ref":"MediaResource:ea9ac7395e2381a039cff870f04f1203"}},"mixtapeMetadata":null},"Paragraph:79f5c4786e38_34":{"__typename":"Paragraph","id":"79f5c4786e38_34","name":"8cb8","type":"P","href":null,"layout":null,"metadata":null,"text":"This does the following:","hasDropCap":null,"dropCapImage":null,"markups":[],"codeBlockMetadata":null,"iframe":null,"mixtapeMetadata":null},"Paragraph:79f5c4786e38_35":{"__typename":"Paragraph","id":"79f5c4786e38_35","name":"8bfa","type":"OLI","href":null,"layout":null,"metadata":null,"text":"Initializes the nested bold boolean property to false","hasDropCap":null,"dropCapImage":null,"markups":[{"__typename":"Markup","type":"STRONG","start":23,"end":27,"href":null,"anchorType":null,"userId":null,"linkMetadata":null},{"__typename":"Markup","type":"EM","start":23,"end":27,"href":null,"anchorType":null,"userId":null,"linkMetadata":null}],"codeBlockMetadata":null,"iframe":null,"mixtapeMetadata":null},"Paragraph:79f5c4786e38_36":{"__typename":"Paragraph","id":"79f5c4786e38_36","name":"13ad","type":"OLI","href":null,"layout":null,"metadata":null,"text":"Invokes the setBold with the parameter value true","hasDropCap":null,"dropCapImage":null,"markups":[{"__typename":"Markup","type":"STRONG","start":12,"end":19,"href":null,"anchorType":null,"userId":null,"linkMetadata":null},{"__typename":"Markup","type":"STRONG","start":45,"end":49,"href":null,"anchorType":null,"userId":null,"linkMetadata":null},{"__typename":"Markup","type":"EM","start":12,"end":19,"href":null,"anchorType":null,"userId":null,"linkMetadata":null},{"__typename":"Markup","type":"EM","start":45,"end":49,"href":null,"anchorType":null,"userId":null,"linkMetadata":null}],"codeBlockMetadata":null,"iframe":null,"mixtapeMetadata":null},"Paragraph:79f5c4786e38_37":{"__typename":"Paragraph","id":"79f5c4786e38_37","name":"e45f","type":"OLI","href":null,"layout":null,"metadata":null,"text":"Validates that the final state of the ViewModel has the bold property now set to true","hasDropCap":null,"dropCapImage":null,"markups":[{"__typename":"Markup","type":"STRONG","start":56,"end":60,"href":null,"anchorType":null,"userId":null,"linkMetadata":null},{"__typename":"Markup","type":"EM","start":56,"end":60,"href":null,"anchorType":null,"userId":null,"linkMetadata":null}],"codeBlockMetadata":null,"iframe":null,"mixtapeMetadata":null},"Paragraph:79f5c4786e38_38":{"__typename":"Paragraph","id":"79f5c4786e38_38","name":"e842","type":"H3","href":null,"layout":null,"metadata":null,"text":"Extensibility","hasDropCap":null,"dropCapImage":null,"markups":[],"codeBlockMetadata":null,"iframe":null,"mixtapeMetadata":null},"Paragraph:79f5c4786e38_39":{"__typename":"Paragraph","id":"79f5c4786e38_39","name":"6cd3","type":"P","href":null,"layout":null,"metadata":null,"text":"The DSL uses a pluggable system so that 3rd party extension functions can add custom statements and assertions. We use this ourselves to check that expected calls to our network stack are made.","hasDropCap":null,"dropCapImage":null,"markups":[],"codeBlockMetadata":null,"iframe":null,"mixtapeMetadata":null},"Paragraph:79f5c4786e38_40":{"__typename":"Paragraph","id":"79f5c4786e38_40","name":"9eeb","type":"P","href":null,"layout":null,"metadata":null,"text":"In our example ViewModel let’s add a function that loads text from a network request.","hasDropCap":null,"dropCapImage":null,"markups":[],"codeBlockMetadata":null,"iframe":null,"mixtapeMetadata":null},"MediaResource:6f0eb77e44eb1474fe7f47d6b49227f2":{"__typename":"MediaResource","id":"6f0eb77e44eb1474fe7f47d6b49227f2","iframeSrc":"","iframeHeight":0,"iframeWidth":0,"title":"MvRxViewModelTextLoadExample"},"Paragraph:79f5c4786e38_41":{"__typename":"Paragraph","id":"79f5c4786e38_41","name":"5638","type":"IFRAME","href":null,"layout":"INSET_CENTER","metadata":null,"text":"","hasDropCap":null,"dropCapImage":null,"markups":[],"codeBlockMetadata":null,"iframe":{"__typename":"Iframe","mediaResource":{"__ref":"MediaResource:6f0eb77e44eb1474fe7f47d6b49227f2"}},"mixtapeMetadata":null},"Paragraph:79f5c4786e38_42":{"__typename":"Paragraph","id":"79f5c4786e38_42","name":"01d7","type":"P","href":null,"layout":null,"metadata":null,"text":"The function to test this would look like:","hasDropCap":null,"dropCapImage":null,"markups":[],"codeBlockMetadata":null,"iframe":null,"mixtapeMetadata":null},"MediaResource:a0724595db2d78e65adc8d909ce7f7c2":{"__typename":"MediaResource","id":"a0724595db2d78e65adc8d909ce7f7c2","iframeSrc":"","iframeHeight":0,"iframeWidth":0,"title":"MvRxViewModelTextLoadingTest"},"Paragraph:79f5c4786e38_43":{"__typename":"Paragraph","id":"79f5c4786e38_43","name":"4982","type":"IFRAME","href":null,"layout":"INSET_CENTER","metadata":null,"text":"","hasDropCap":null,"dropCapImage":null,"markups":[],"codeBlockMetadata":null,"iframe":{"__typename":"Iframe","mediaResource":{"__ref":"MediaResource:a0724595db2d78e65adc8d909ce7f7c2"}},"mixtapeMetadata":null},"Paragraph:79f5c4786e38_44":{"__typename":"Paragraph","id":"79f5c4786e38_44","name":"20a1","type":"P","href":null,"layout":null,"metadata":null,"text":"This test:","hasDropCap":null,"dropCapImage":null,"markups":[],"codeBlockMetadata":null,"iframe":null,"mixtapeMetadata":null},"Paragraph:79f5c4786e38_45":{"__typename":"Paragraph","id":"79f5c4786e38_45","name":"ff39","type":"OLI","href":null,"layout":null,"metadata":null,"text":"Invokes the loadText function with the id argument “1”","hasDropCap":null,"dropCapImage":null,"markups":[{"__typename":"Markup","type":"STRONG","start":12,"end":20,"href":null,"anchorType":null,"userId":null,"linkMetadata":null},{"__typename":"Markup","type":"EM","start":12,"end":20,"href":null,"anchorType":null,"userId":null,"linkMetadata":null}],"codeBlockMetadata":null,"iframe":null,"mixtapeMetadata":null},"Paragraph:79f5c4786e38_46":{"__typename":"Paragraph","id":"79f5c4786e38_46","name":"b1dd","type":"OLI","href":null,"layout":null,"metadata":null,"text":"Makes an assertion that we expect the ViewModel to execute a network GET request to the given API path with the id as a query parameter","hasDropCap":null,"dropCapImage":null,"markups":[],"codeBlockMetadata":null,"iframe":null,"mixtapeMetadata":null},"Paragraph:79f5c4786e38_47":{"__typename":"Paragraph","id":"79f5c4786e38_47","name":"53e6","type":"OLI","href":null,"layout":null,"metadata":null,"text":"Specifies a mock response value of “server result”","hasDropCap":null,"dropCapImage":null,"markups":[],"codeBlockMetadata":null,"iframe":null,"mixtapeMetadata":null},"Paragraph:79f5c4786e38_48":{"__typename":"Paragraph","id":"79f5c4786e38_48","name":"fe31","type":"OLI","href":null,"layout":null,"metadata":null,"text":"Asserts that the final state value of the text matches our mocked response value “server result”","hasDropCap":null,"dropCapImage":null,"markups":[],"codeBlockMetadata":null,"iframe":null,"mixtapeMetadata":null},"Paragraph:79f5c4786e38_49":{"__typename":"Paragraph","id":"79f5c4786e38_49","name":"cac9","type":"P","href":null,"layout":null,"metadata":null,"text":"This allows us to test the boundary of our ViewModel with the network layer, easily checking that the desired request is made and mocking the return value.","hasDropCap":null,"dropCapImage":null,"markups":[],"codeBlockMetadata":null,"iframe":null,"mixtapeMetadata":null},"Paragraph:79f5c4786e38_50":{"__typename":"Paragraph","id":"79f5c4786e38_50","name":"4fc8","type":"P","href":null,"layout":null,"metadata":null,"text":"The expectRequests function is an extension function to the unit testing framework. This allows us to open source the core library but still test our internal libraries at Airbnb.","hasDropCap":null,"dropCapImage":null,"markups":[{"__typename":"Markup","type":"STRONG","start":4,"end":18,"href":null,"anchorType":null,"userId":null,"linkMetadata":null},{"__typename":"Markup","type":"EM","start":4,"end":18,"href":null,"anchorType":null,"userId":null,"linkMetadata":null}],"codeBlockMetadata":null,"iframe":null,"mixtapeMetadata":null},"Paragraph:79f5c4786e38_51":{"__typename":"Paragraph","id":"79f5c4786e38_51","name":"4293","type":"P","href":null,"layout":null,"metadata":null,"text":"Also noteworthy is that in both unit and integration tests we never mock network requests at the JSON layer. We find maintaining JSON file mocks to be difficult and unnecessary. Instead, we are adopting GraphQL to give compile time guarantees on each request schema. This means we just need to assert that the proper query was made, and we can trust that the response will be in a valid, expected format.","hasDropCap":null,"dropCapImage":null,"markups":[],"codeBlockMetadata":null,"iframe":null,"mixtapeMetadata":null},"Paragraph:79f5c4786e38_52":{"__typename":"Paragraph","id":"79f5c4786e38_52","name":"14e4","type":"P","href":null,"layout":null,"metadata":null,"text":"This simplifies the scope of our tests, improves maintainability, and still offers us guarantees on the functionality of our network layer.","hasDropCap":null,"dropCapImage":null,"markups":[],"codeBlockMetadata":null,"iframe":null,"mixtapeMetadata":null},"Paragraph:79f5c4786e38_53":{"__typename":"Paragraph","id":"79f5c4786e38_53","name":"6992","type":"H3","href":null,"layout":null,"metadata":null,"text":"Advanced Usage","hasDropCap":null,"dropCapImage":null,"markups":[],"codeBlockMetadata":null,"iframe":null,"mixtapeMetadata":null},"Paragraph:79f5c4786e38_54":{"__typename":"Paragraph","id":"79f5c4786e38_54","name":"dc2a","type":"P","href":null,"layout":null,"metadata":null,"text":"The unit test framework provides some other nice utilities for testing common ViewModel patterns.","hasDropCap":null,"dropCapImage":null,"markups":[],"codeBlockMetadata":null,"iframe":null,"mixtapeMetadata":null},"Paragraph:79f5c4786e38_55":{"__typename":"Paragraph","id":"79f5c4786e38_55","name":"339d","type":"H4","href":null,"layout":null,"metadata":null,"text":"Automatic Validations","hasDropCap":null,"dropCapImage":null,"markups":[],"codeBlockMetadata":null,"iframe":null,"mixtapeMetadata":null},"Paragraph:79f5c4786e38_56":{"__typename":"Paragraph","id":"79f5c4786e38_56","name":"fd54","type":"P","href":null,"layout":null,"metadata":null,"text":"One common case is a ViewModel function that updates a single property on the State, such as our example above that toggles the “bold” boolean. The framework offers special handling for this case to make it testable in just a few lines.","hasDropCap":null,"dropCapImage":null,"markups":[],"codeBlockMetadata":null,"iframe":null,"mixtapeMetadata":null},"MediaResource:9f4e1f496869452c8fb2ef47725592f9":{"__typename":"MediaResource","id":"9f4e1f496869452c8fb2ef47725592f9","iframeSrc":"","iframeHeight":0,"iframeWidth":0,"title":"MvRxViewModelPropertyTestExample"},"Paragraph:79f5c4786e38_57":{"__typename":"Paragraph","id":"79f5c4786e38_57","name":"4641","type":"IFRAME","href":null,"layout":"INSET_CENTER","metadata":null,"text":"","hasDropCap":null,"dropCapImage":null,"markups":[],"codeBlockMetadata":null,"iframe":{"__typename":"Iframe","mediaResource":{"__ref":"MediaResource:9f4e1f496869452c8fb2ef47725592f9"}},"mixtapeMetadata":null},"Paragraph:79f5c4786e38_58":{"__typename":"Paragraph","id":"79f5c4786e38_58","name":"8c12","type":"P","href":null,"layout":null,"metadata":null,"text":"With this syntax, the test:","hasDropCap":null,"dropCapImage":null,"markups":[],"codeBlockMetadata":null,"iframe":null,"mixtapeMetadata":null},"Paragraph:79f5c4786e38_59":{"__typename":"Paragraph","id":"79f5c4786e38_59","name":"b5a9","type":"OLI","href":null,"layout":null,"metadata":null,"text":"Indicates that the setBold function is being tested","hasDropCap":null,"dropCapImage":null,"markups":[{"__typename":"Markup","type":"STRONG","start":19,"end":26,"href":null,"anchorType":null,"userId":null,"linkMetadata":null},{"__typename":"Markup","type":"EM","start":19,"end":26,"href":null,"anchorType":null,"userId":null,"linkMetadata":null}],"codeBlockMetadata":null,"iframe":null,"mixtapeMetadata":null},"Paragraph:79f5c4786e38_60":{"__typename":"Paragraph","id":"79f5c4786e38_60","name":"2561","type":"OLI","href":null,"layout":null,"metadata":null,"text":"Specifies a reference to the nested state property that will be changed as a result","hasDropCap":null,"dropCapImage":null,"markups":[],"codeBlockMetadata":null,"iframe":null,"mixtapeMetadata":null},"Paragraph:79f5c4786e38_61":{"__typename":"Paragraph","id":"79f5c4786e38_61","name":"bfe6","type":"OLI","href":null,"layout":null,"metadata":null,"text":"Detects the parameter type of the setBold function (boolean, in this case)","hasDropCap":null,"dropCapImage":null,"markups":[{"__typename":"Markup","type":"STRONG","start":34,"end":41,"href":null,"anchorType":null,"userId":null,"linkMetadata":null},{"__typename":"Markup","type":"EM","start":34,"end":41,"href":null,"anchorType":null,"userId":null,"linkMetadata":null}],"codeBlockMetadata":null,"iframe":null,"mixtapeMetadata":null},"Paragraph:79f5c4786e38_62":{"__typename":"Paragraph","id":"79f5c4786e38_62","name":"663e","type":"OLI","href":null,"layout":null,"metadata":null,"text":"Generates test inputs based on the parameter type. For a boolean this will be true and false. If it is nullable it will also test a “null” input.","hasDropCap":null,"dropCapImage":null,"markups":[{"__typename":"Markup","type":"STRONG","start":133,"end":137,"href":null,"anchorType":null,"userId":null,"linkMetadata":null},{"__typename":"Markup","type":"EM","start":133,"end":137,"href":null,"anchorType":null,"userId":null,"linkMetadata":null}],"codeBlockMetadata":null,"iframe":null,"mixtapeMetadata":null},"Paragraph:79f5c4786e38_63":{"__typename":"Paragraph","id":"79f5c4786e38_63","name":"60a3","type":"OLI","href":null,"layout":null,"metadata":null,"text":"Invokes the setBold function with the generated input values, and after each invocation checks that the “bold” property in the state has been updated to the same value.","hasDropCap":null,"dropCapImage":null,"markups":[{"__typename":"Markup","type":"STRONG","start":12,"end":19,"href":null,"anchorType":null,"userId":null,"linkMetadata":null},{"__typename":"Markup","type":"EM","start":12,"end":19,"href":null,"anchorType":null,"userId":null,"linkMetadata":null}],"codeBlockMetadata":null,"iframe":null,"mixtapeMetadata":null},"Paragraph:79f5c4786e38_64":{"__typename":"Paragraph","id":"79f5c4786e38_64","name":"4b1d","type":"P","href":null,"layout":null,"metadata":null,"text":"This works for any function with a single primitive type — the type is detected with reflection and the function is invoked with various test values. Then the property value on the state is checked to make sure it equals the expected test value.","hasDropCap":null,"dropCapImage":null,"markups":[],"codeBlockMetadata":null,"iframe":null,"mixtapeMetadata":null},"Paragraph:79f5c4786e38_65":{"__typename":"Paragraph","id":"79f5c4786e38_65","name":"1be6","type":"P","href":null,"layout":null,"metadata":null,"text":"In the more generic case, we also support multi parameter functions as well as cases where the state property type is different from the function parameter type.","hasDropCap":null,"dropCapImage":null,"markups":[],"codeBlockMetadata":null,"iframe":null,"mixtapeMetadata":null},"Paragraph:79f5c4786e38_66":{"__typename":"Paragraph","id":"79f5c4786e38_66","name":"5034","type":"P","href":null,"layout":null,"metadata":null,"text":"For example, the following tests a function that squares an input and sets the value on the “result” property of the state.","hasDropCap":null,"dropCapImage":null,"markups":[],"codeBlockMetadata":null,"iframe":null,"mixtapeMetadata":null},"MediaResource:2386f1f970e37eeae81f26e782b7d654":{"__typename":"MediaResource","id":"2386f1f970e37eeae81f26e782b7d654","iframeSrc":"","iframeHeight":0,"iframeWidth":0,"title":"MvRxViewModelTestSetsMappedExample"},"Paragraph:79f5c4786e38_67":{"__typename":"Paragraph","id":"79f5c4786e38_67","name":"68fd","type":"IFRAME","href":null,"layout":"INSET_CENTER","metadata":null,"text":"","hasDropCap":null,"dropCapImage":null,"markups":[],"codeBlockMetadata":null,"iframe":{"__typename":"Iframe","mediaResource":{"__ref":"MediaResource:2386f1f970e37eeae81f26e782b7d654"}},"mixtapeMetadata":null},"Paragraph:79f5c4786e38_68":{"__typename":"Paragraph","id":"79f5c4786e38_68","name":"8335","type":"P","href":null,"layout":null,"metadata":null,"text":"This makes it easy to list a mapping of inputs to outputs; it automatically invokes the function with each input and checks for the corresponding output on the state.","hasDropCap":null,"dropCapImage":null,"markups":[],"codeBlockMetadata":null,"iframe":null,"mixtapeMetadata":null},"Paragraph:79f5c4786e38_69":{"__typename":"Paragraph","id":"79f5c4786e38_69","name":"51b7","type":"H4","href":null,"layout":null,"metadata":null,"text":"Testing Initialization","hasDropCap":null,"dropCapImage":null,"markups":[],"codeBlockMetadata":null,"iframe":null,"mixtapeMetadata":null},"Paragraph:79f5c4786e38_70":{"__typename":"Paragraph","id":"79f5c4786e38_70","name":"6e4f","type":"P","href":null,"layout":null,"metadata":null,"text":"It is also common for a ViewModel to execute network requests or other tasks during its initialization; that is, when a new instance is instantiated and the constructor is invoked.","hasDropCap":null,"dropCapImage":null,"markups":[],"codeBlockMetadata":null,"iframe":null,"mixtapeMetadata":null},"Paragraph:79f5c4786e38_71":{"__typename":"Paragraph","id":"79f5c4786e38_71","name":"1950","type":"P","href":null,"layout":null,"metadata":null,"text":"For example, imagine our TextViewModel from above was modified to load text from a network request when it is created. We can test that behavior with this syntax.","hasDropCap":null,"dropCapImage":null,"markups":[],"codeBlockMetadata":null,"iframe":null,"mixtapeMetadata":null},"MediaResource:673095c59ef1132316180b826be8419e":{"__typename":"MediaResource","id":"673095c59ef1132316180b826be8419e","iframeSrc":"","iframeHeight":0,"iframeWidth":0,"title":"MvRxViewModelInitializationTestExample"},"Paragraph:79f5c4786e38_72":{"__typename":"Paragraph","id":"79f5c4786e38_72","name":"6d00","type":"IFRAME","href":null,"layout":"INSET_CENTER","metadata":null,"text":"","hasDropCap":null,"dropCapImage":null,"markups":[],"codeBlockMetadata":null,"iframe":{"__typename":"Iframe","mediaResource":{"__ref":"MediaResource:673095c59ef1132316180b826be8419e"}},"mixtapeMetadata":null},"Paragraph:79f5c4786e38_73":{"__typename":"Paragraph","id":"79f5c4786e38_73","name":"fc50","type":"P","href":null,"layout":null,"metadata":null,"text":"This asserts that when the ViewModel is instantiated it:","hasDropCap":null,"dropCapImage":null,"markups":[],"codeBlockMetadata":null,"iframe":null,"mixtapeMetadata":null},"Paragraph:79f5c4786e38_74":{"__typename":"Paragraph","id":"79f5c4786e38_74","name":"b9b8","type":"OLI","href":null,"layout":null,"metadata":null,"text":"Makes a network GET request to an expected API path","hasDropCap":null,"dropCapImage":null,"markups":[],"codeBlockMetadata":null,"iframe":null,"mixtapeMetadata":null},"Paragraph:79f5c4786e38_75":{"__typename":"Paragraph","id":"79f5c4786e38_75","name":"0541","type":"OLI","href":null,"layout":null,"metadata":null,"text":"Sets the text property to a “Loading” state","hasDropCap":null,"dropCapImage":null,"markups":[],"codeBlockMetadata":null,"iframe":null,"mixtapeMetadata":null},"Paragraph:79f5c4786e38_76":{"__typename":"Paragraph","id":"79f5c4786e38_76","name":"ca5d","type":"P","href":null,"layout":null,"metadata":null,"text":"This syntax is required compared to the normal function testing syntax because it must wrap the instantiation of the ViewModel and isolate the behavior there. On the other hand, when testing functions we exclude the behavior during instantiation in order to not conflate them.","hasDropCap":null,"dropCapImage":null,"markups":[],"codeBlockMetadata":null,"iframe":null,"mixtapeMetadata":null},"Paragraph:79f5c4786e38_77":{"__typename":"Paragraph","id":"79f5c4786e38_77","name":"0b27","type":"H4","href":null,"layout":null,"metadata":null,"text":"Generating Test Scaffolding","hasDropCap":null,"dropCapImage":null,"markups":[],"codeBlockMetadata":null,"iframe":null,"mixtapeMetadata":null},"Paragraph:79f5c4786e38_78":{"__typename":"Paragraph","id":"79f5c4786e38_78","name":"ee22","type":"P","href":null,"layout":null,"metadata":null,"text":"Finally, in a multi module world it can be tedious to set up a unit test environment for each new module that is created (we have hundreds of modules!). For each module we need:","hasDropCap":null,"dropCapImage":null,"markups":[],"codeBlockMetadata":null,"iframe":null,"mixtapeMetadata":null},"Paragraph:79f5c4786e38_79":{"__typename":"Paragraph","id":"79f5c4786e38_79","name":"4156","type":"ULI","href":null,"layout":null,"metadata":null,"text":"A Robolectric test runner","hasDropCap":null,"dropCapImage":null,"markups":[],"codeBlockMetadata":null,"iframe":null,"mixtapeMetadata":null},"Paragraph:79f5c4786e38_80":{"__typename":"Paragraph","id":"79f5c4786e38_80","name":"a719","type":"ULI","href":null,"layout":null,"metadata":null,"text":"A base test for test classes to extend so the runner is applied","hasDropCap":null,"dropCapImage":null,"markups":[],"codeBlockMetadata":null,"iframe":null,"mixtapeMetadata":null},"Paragraph:79f5c4786e38_81":{"__typename":"Paragraph","id":"79f5c4786e38_81","name":"ea28","type":"ULI","href":null,"layout":null,"metadata":null,"text":"Scaffolding to support dagger test overrides (a new Dagger module plus a Test application to setup injection of the dagger module).","hasDropCap":null,"dropCapImage":null,"markups":[],"codeBlockMetadata":null,"iframe":null,"mixtapeMetadata":null},"Paragraph:79f5c4786e38_82":{"__typename":"Paragraph","id":"79f5c4786e38_82","name":"96e4","type":"ULI","href":null,"layout":null,"metadata":null,"text":"A mockito plugin to support mocking final classes and functions (for Kotlin usage)","hasDropCap":null,"dropCapImage":null,"markups":[],"codeBlockMetadata":null,"iframe":null,"mixtapeMetadata":null},"Paragraph:79f5c4786e38_83":{"__typename":"Paragraph","id":"79f5c4786e38_83","name":"660d","type":"P","href":null,"layout":null,"metadata":null,"text":"We’ve created tooling that automatically generates all of this test scaffolding for a module, so a developer can instantly start writing unit tests without dealing with any of the tedium of configuration.","hasDropCap":null,"dropCapImage":null,"markups":[],"codeBlockMetadata":null,"iframe":null,"mixtapeMetadata":null},"Paragraph:79f5c4786e38_84":{"__typename":"Paragraph","id":"79f5c4786e38_84","name":"b3f4","type":"P","href":null,"layout":null,"metadata":null,"text":"Additionally, we’ve created an Intellij IDEA plugin that can generate new MvRx ViewModels for us. This allows us to automatically create a test file for each new ViewModel that we add.","hasDropCap":null,"dropCapImage":null,"markups":[],"codeBlockMetadata":null,"iframe":null,"mixtapeMetadata":null},"Paragraph:79f5c4786e38_85":{"__typename":"Paragraph","id":"79f5c4786e38_85","name":"ee96","type":"H3","href":null,"layout":null,"metadata":null,"text":"Next: Our Automation Infrastructure","hasDropCap":null,"dropCapImage":null,"markups":[],"codeBlockMetadata":null,"iframe":null,"mixtapeMetadata":null},"Paragraph:79f5c4786e38_86":{"__typename":"Paragraph","id":"79f5c4786e38_86","name":"fb5c","type":"P","href":null,"layout":null,"metadata":null,"text":"Overall, our goal for this unit test framework was to:","hasDropCap":null,"dropCapImage":null,"markups":[],"codeBlockMetadata":null,"iframe":null,"mixtapeMetadata":null},"Paragraph:79f5c4786e38_87":{"__typename":"Paragraph","id":"79f5c4786e38_87","name":"cc6d","type":"OLI","href":null,"layout":null,"metadata":null,"text":"Remove friction from testing ViewModel logic, while;","hasDropCap":null,"dropCapImage":null,"markups":[],"codeBlockMetadata":null,"iframe":null,"mixtapeMetadata":null},"Paragraph:79f5c4786e38_88":{"__typename":"Paragraph","id":"79f5c4786e38_88","name":"a313","type":"OLI","href":null,"layout":null,"metadata":null,"text":"Providing a simple, yet flexible, API that can cover all use cases of a ViewModel.","hasDropCap":null,"dropCapImage":null,"markups":[],"codeBlockMetadata":null,"iframe":null,"mixtapeMetadata":null},"Paragraph:79f5c4786e38_89":{"__typename":"Paragraph","id":"79f5c4786e38_89","name":"d695","type":"P","href":null,"layout":null,"metadata":null,"text":"Additionally, we designed the library to be extensible so we can open source it, allowing teams to easily add their own assertions to the DSL.","hasDropCap":null,"dropCapImage":null,"markups":[],"codeBlockMetadata":null,"iframe":null,"mixtapeMetadata":null},"Paragraph:79f5c4786e38_90":{"__typename":"Paragraph","id":"79f5c4786e38_90","name":"7538","type":"P","href":null,"layout":null,"metadata":null,"text":"While this has been great for us, and was necessary for comprehensively testing business logic, the nicest tests are ones we can automatically generate! Next, in Part 5 of the series, we’ll revisit our automated integration testing framework to see how it powers our interaction and screenshot tests.","hasDropCap":null,"dropCapImage":null,"markups":[{"__typename":"Markup","type":"A","start":162,"end":182,"href":"https:\u002F\u002Fmedium.com\u002Fairbnb-engineering\u002Fbetter-android-testing-at-airbnb-661a554a8c8b","anchorType":"LINK","userId":null,"linkMetadata":null}],"codeBlockMetadata":null,"iframe":null,"mixtapeMetadata":null},"Paragraph:79f5c4786e38_91":{"__typename":"Paragraph","id":"79f5c4786e38_91","name":"41bb","type":"H4","href":null,"layout":null,"metadata":null,"text":"Series Index","hasDropCap":null,"dropCapImage":null,"markups":[],"codeBlockMetadata":null,"iframe":null,"mixtapeMetadata":null},"Paragraph:79f5c4786e38_92":{"__typename":"Paragraph","id":"79f5c4786e38_92","name":"18cd","type":"P","href":null,"layout":null,"metadata":null,"text":"This is a seven part article series on testing at Airbnb.","hasDropCap":null,"dropCapImage":null,"markups":[],"codeBlockMetadata":null,"iframe":null,"mixtapeMetadata":null},"Paragraph:79f5c4786e38_93":{"__typename":"Paragraph","id":"79f5c4786e38_93","name":"c733","type":"P","href":null,"layout":null,"metadata":null,"text":"Part 1 — Testing Philosophy and a Mocking System","hasDropCap":null,"dropCapImage":null,"markups":[{"__typename":"Markup","type":"A","start":9,"end":48,"href":"https:\u002F\u002Fmedium.com\u002Fairbnb-engineering\u002Fbetter-android-testing-at-airbnb-3f5b90b9c40a","anchorType":"LINK","userId":null,"linkMetadata":null}],"codeBlockMetadata":null,"iframe":null,"mixtapeMetadata":null},"Paragraph:79f5c4786e38_94":{"__typename":"Paragraph","id":"79f5c4786e38_94","name":"221c","type":"P","href":null,"layout":null,"metadata":null,"text":"Part 2 — Screenshot Testing with MvRx and Happo","hasDropCap":null,"dropCapImage":null,"markups":[{"__typename":"Markup","type":"A","start":9,"end":47,"href":"https:\u002F\u002Fmedium.com\u002Fairbnb-engineering\u002Fbetter-android-testing-at-airbnb-a77ac9531cab","anchorType":"LINK","userId":null,"linkMetadata":null}],"codeBlockMetadata":null,"iframe":null,"mixtapeMetadata":null},"Paragraph:79f5c4786e38_95":{"__typename":"Paragraph","id":"79f5c4786e38_95","name":"3c1e","type":"P","href":null,"layout":null,"metadata":null,"text":"Part 3 — Automated Interaction Testing","hasDropCap":null,"dropCapImage":null,"markups":[{"__typename":"Markup","type":"A","start":9,"end":38,"href":"https:\u002F\u002Fmedium.com\u002Fairbnb-engineering\u002Fbetter-android-testing-at-airbnb-1d1e91e489b4","anchorType":"LINK","userId":null,"linkMetadata":null}],"codeBlockMetadata":null,"iframe":null,"mixtapeMetadata":null},"Paragraph:79f5c4786e38_96":{"__typename":"Paragraph","id":"79f5c4786e38_96","name":"992a","type":"P","href":null,"layout":null,"metadata":null,"text":"Part 4 (This article) — A Framework for Unit Testing ViewModels","hasDropCap":null,"dropCapImage":null,"markups":[{"__typename":"Markup","type":"A","start":24,"end":63,"href":"https:\u002F\u002Fmedium.com\u002Fairbnb-engineering\u002Fbetter-android-testing-at-airbnb-part-4-testing-viewmodels-550d929126c8","anchorType":"LINK","userId":null,"linkMetadata":null},{"__typename":"Markup","type":"STRONG","start":0,"end":21,"href":null,"anchorType":null,"userId":null,"linkMetadata":null}],"codeBlockMetadata":null,"iframe":null,"mixtapeMetadata":null},"Paragraph:79f5c4786e38_97":{"__typename":"Paragraph","id":"79f5c4786e38_97","name":"6103","type":"P","href":null,"layout":null,"metadata":null,"text":"Part 5 — Architecture of our Automated Testing Framework","hasDropCap":null,"dropCapImage":null,"markups":[{"__typename":"Markup","type":"A","start":9,"end":56,"href":"https:\u002F\u002Fmedium.com\u002Fairbnb-engineering\u002Fbetter-android-testing-at-airbnb-661a554a8c8b","anchorType":"LINK","userId":null,"linkMetadata":null}],"codeBlockMetadata":null,"iframe":null,"mixtapeMetadata":null},"Paragraph:79f5c4786e38_98":{"__typename":"Paragraph","id":"79f5c4786e38_98","name":"c39d","type":"P","href":null,"layout":null,"metadata":null,"text":"Part 6 — Obstacles to Consistent Mocking","hasDropCap":null,"dropCapImage":null,"markups":[{"__typename":"Markup","type":"A","start":9,"end":40,"href":"https:\u002F\u002Fmedium.com\u002Fairbnb-engineering\u002Fbetter-android-testing-at-airbnb-a11f6832773f","anchorType":"LINK","userId":null,"linkMetadata":null}],"codeBlockMetadata":null,"iframe":null,"mixtapeMetadata":null},"Paragraph:79f5c4786e38_99":{"__typename":"Paragraph","id":"79f5c4786e38_99","name":"461f","type":"P","href":null,"layout":null,"metadata":null,"text":"Part 7 — Test Generation and CI Configuration","hasDropCap":null,"dropCapImage":null,"markups":[{"__typename":"Markup","type":"A","start":9,"end":45,"href":"https:\u002F\u002Fmedium.com\u002Fairbnb-engineering\u002Fbetter-android-testing-at-airbnb-eacec3a8a72f","anchorType":"LINK","userId":null,"linkMetadata":null}],"codeBlockMetadata":null,"iframe":null,"mixtapeMetadata":null},"Paragraph:79f5c4786e38_100":{"__typename":"Paragraph","id":"79f5c4786e38_100","name":"f73d","type":"H4","href":null,"layout":null,"metadata":null,"text":"We’re Hiring!","hasDropCap":null,"dropCapImage":null,"markups":[],"codeBlockMetadata":null,"iframe":null,"mixtapeMetadata":null},"Paragraph:79f5c4786e38_101":{"__typename":"Paragraph","id":"79f5c4786e38_101","name":"e896","type":"P","href":null,"layout":null,"metadata":null,"text":"Want to work with us on these and other Android projects at scale? Airbnb is hiring for several Android engineer positions across the company! See https:\u002F\u002Fcareers.airbnb.com for current openings.","hasDropCap":null,"dropCapImage":null,"markups":[{"__typename":"Markup","type":"A","start":147,"end":173,"href":"https:\u002F\u002Fcareers.airbnb.com\u002F","anchorType":"LINK","userId":null,"linkMetadata":null}],"codeBlockMetadata":null,"iframe":null,"mixtapeMetadata":null},"CollectionViewerEdge:collectionId:53c7c27702d5-viewerId:lo_caae6e9abf64":{"__typename":"CollectionViewerEdge","id":"collectionId:53c7c27702d5-viewerId:lo_caae6e9abf64","isEditor":false,"isMuting":false},"ImageMetadata:1*JZl-TXoSiG0VmYn3qWLdTA.png":{"__typename":"ImageMetadata","id":"1*JZl-TXoSiG0VmYn3qWLdTA.png","originalWidth":280,"originalHeight":280},"PostViewerEdge:postId:550d929126c8-viewerId:lo_caae6e9abf64":{"__typename":"PostViewerEdge","shouldIndexPostForExternalSearch":true,"id":"postId:550d929126c8-viewerId:lo_caae6e9abf64"},"Tag:android":{"__typename":"Tag","id":"android","displayTitle":"Android","normalizedTagSlug":"android"},"Tag:testing":{"__typename":"Tag","id":"testing","displayTitle":"Testing","normalizedTagSlug":"testing"},"Tag:mobile":{"__typename":"Tag","id":"mobile","displayTitle":"Mobile","normalizedTagSlug":"mobile"},"Tag:native":{"__typename":"Tag","id":"native","displayTitle":"Native","normalizedTagSlug":"native"},"Post:550d929126c8":{"__typename":"Post","id":"550d929126c8","collection":{"__ref":"Collection:53c7c27702d5"},"content({\"postMeteringOptions\":{}})":{"__typename":"PostContent","isLockedPreviewOnly":false,"bodyModel":{"__typename":"RichText","sections":[{"__typename":"Section","name":"7fdf","startIndex":0,"textLayout":null,"imageLayout":null,"backgroundImage":null,"videoLayout":null,"backgroundVideo":null}],"paragraphs":[{"__ref":"Paragraph:79f5c4786e38_0"},{"__ref":"Paragraph:79f5c4786e38_1"},{"__ref":"Paragraph:79f5c4786e38_2"},{"__ref":"Paragraph:79f5c4786e38_3"},{"__ref":"Paragraph:79f5c4786e38_4"},{"__ref":"Paragraph:79f5c4786e38_5"},{"__ref":"Paragraph:79f5c4786e38_6"},{"__ref":"Paragraph:79f5c4786e38_7"},{"__ref":"Paragraph:79f5c4786e38_8"},{"__ref":"Paragraph:79f5c4786e38_9"},{"__ref":"Paragraph:79f5c4786e38_10"},{"__ref":"Paragraph:79f5c4786e38_11"},{"__ref":"Paragraph:79f5c4786e38_12"},{"__ref":"Paragraph:79f5c4786e38_13"},{"__ref":"Paragraph:79f5c4786e38_14"},{"__ref":"Paragraph:79f5c4786e38_15"},{"__ref":"Paragraph:79f5c4786e38_16"},{"__ref":"Paragraph:79f5c4786e38_17"},{"__ref":"Paragraph:79f5c4786e38_18"},{"__ref":"Paragraph:79f5c4786e38_19"},{"__ref":"Paragraph:79f5c4786e38_20"},{"__ref":"Paragraph:79f5c4786e38_21"},{"__ref":"Paragraph:79f5c4786e38_22"},{"__ref":"Paragraph:79f5c4786e38_23"},{"__ref":"Paragraph:79f5c4786e38_24"},{"__ref":"Paragraph:79f5c4786e38_25"},{"__ref":"Paragraph:79f5c4786e38_26"},{"__ref":"Paragraph:79f5c4786e38_27"},{"__ref":"Paragraph:79f5c4786e38_28"},{"__ref":"Paragraph:79f5c4786e38_29"},{"__ref":"Paragraph:79f5c4786e38_30"},{"__ref":"Paragraph:79f5c4786e38_31"},{"__ref":"Paragraph:79f5c4786e38_32"},{"__ref":"Paragraph:79f5c4786e38_33"},{"__ref":"Paragraph:79f5c4786e38_34"},{"__ref":"Paragraph:79f5c4786e38_35"},{"__ref":"Paragraph:79f5c4786e38_36"},{"__ref":"Paragraph:79f5c4786e38_37"},{"__ref":"Paragraph:79f5c4786e38_38"},{"__ref":"Paragraph:79f5c4786e38_39"},{"__ref":"Paragraph:79f5c4786e38_40"},{"__ref":"Paragraph:79f5c4786e38_41"},{"__ref":"Paragraph:79f5c4786e38_42"},{"__ref":"Paragraph:79f5c4786e38_43"},{"__ref":"Paragraph:79f5c4786e38_44"},{"__ref":"Paragraph:79f5c4786e38_45"},{"__ref":"Paragraph:79f5c4786e38_46"},{"__ref":"Paragraph:79f5c4786e38_47"},{"__ref":"Paragraph:79f5c4786e38_48"},{"__ref":"Paragraph:79f5c4786e38_49"},{"__ref":"Paragraph:79f5c4786e38_50"},{"__ref":"Paragraph:79f5c4786e38_51"},{"__ref":"Paragraph:79f5c4786e38_52"},{"__ref":"Paragraph:79f5c4786e38_53"},{"__ref":"Paragraph:79f5c4786e38_54"},{"__ref":"Paragraph:79f5c4786e38_55"},{"__ref":"Paragraph:79f5c4786e38_56"},{"__ref":"Paragraph:79f5c4786e38_57"},{"__ref":"Paragraph:79f5c4786e38_58"},{"__ref":"Paragraph:79f5c4786e38_59"},{"__ref":"Paragraph:79f5c4786e38_60"},{"__ref":"Paragraph:79f5c4786e38_61"},{"__ref":"Paragraph:79f5c4786e38_62"},{"__ref":"Paragraph:79f5c4786e38_63"},{"__ref":"Paragraph:79f5c4786e38_64"},{"__ref":"Paragraph:79f5c4786e38_65"},{"__ref":"Paragraph:79f5c4786e38_66"},{"__ref":"Paragraph:79f5c4786e38_67"},{"__ref":"Paragraph:79f5c4786e38_68"},{"__ref":"Paragraph:79f5c4786e38_69"},{"__ref":"Paragraph:79f5c4786e38_70"},{"__ref":"Paragraph:79f5c4786e38_71"},{"__ref":"Paragraph:79f5c4786e38_72"},{"__ref":"Paragraph:79f5c4786e38_73"},{"__ref":"Paragraph:79f5c4786e38_74"},{"__ref":"Paragraph:79f5c4786e38_75"},{"__ref":"Paragraph:79f5c4786e38_76"},{"__ref":"Paragraph:79f5c4786e38_77"},{"__ref":"Paragraph:79f5c4786e38_78"},{"__ref":"Paragraph:79f5c4786e38_79"},{"__ref":"Paragraph:79f5c4786e38_80"},{"__ref":"Paragraph:79f5c4786e38_81"},{"__ref":"Paragraph:79f5c4786e38_82"},{"__ref":"Paragraph:79f5c4786e38_83"},{"__ref":"Paragraph:79f5c4786e38_84"},{"__ref":"Paragraph:79f5c4786e38_85"},{"__ref":"Paragraph:79f5c4786e38_86"},{"__ref":"Paragraph:79f5c4786e38_87"},{"__ref":"Paragraph:79f5c4786e38_88"},{"__ref":"Paragraph:79f5c4786e38_89"},{"__ref":"Paragraph:79f5c4786e38_90"},{"__ref":"Paragraph:79f5c4786e38_91"},{"__ref":"Paragraph:79f5c4786e38_92"},{"__ref":"Paragraph:79f5c4786e38_93"},{"__ref":"Paragraph:79f5c4786e38_94"},{"__ref":"Paragraph:79f5c4786e38_95"},{"__ref":"Paragraph:79f5c4786e38_96"},{"__ref":"Paragraph:79f5c4786e38_97"},{"__ref":"Paragraph:79f5c4786e38_98"},{"__ref":"Paragraph:79f5c4786e38_99"},{"__ref":"Paragraph:79f5c4786e38_100"},{"__ref":"Paragraph:79f5c4786e38_101"}]},"validatedShareKey":"","shareKeyCreator":null},"creator":{"__ref":"User:9f3427a69792"},"inResponseToEntityType":null,"isLocked":false,"isMarkedPaywallOnly":false,"lockedSource":"LOCKED_POST_SOURCE_NONE","mediumUrl":"https:\u002F\u002Fmedium.com\u002Fairbnb-engineering\u002Fbetter-android-testing-at-airbnb-part-4-testing-viewmodels-550d929126c8","primaryTopic":{"__ref":"Topic:decb52b64abf"},"topics":[{"__typename":"Topic","slug":"programming"}],"isPublished":true,"latestPublishedVersion":"79f5c4786e38","visibility":"PUBLIC","postResponses":{"__typename":"PostResponses","count":0},"clapCount":312,"allowResponses":true,"isLimitedState":false,"title":"Better Android Testing at Airbnb, Part 4","isSeries":false,"sequence":null,"uniqueSlug":"better-android-testing-at-airbnb-part-4-testing-viewmodels-550d929126c8","socialTitle":"","socialDek":"","canonicalUrl":"","metaDescription":"In the fourth installment of our series on Android Testing at Airbnb, we look at a framework for unit testing logic in ViewModels.","latestPublishedAt":1626120630921,"readingTime":6.694339622641509,"previewContent":{"__typename":"PreviewContent","subtitle":"Testing ViewModels"},"previewImage":{"__ref":"ImageMetadata:1*SzUfC_eUHmH-O9CZwUDMtQ.jpeg"},"isShortform":false,"seoTitle":"","firstPublishedAt":1576262381583,"updatedAt":1639348970993,"shortformType":"SHORTFORM_TYPE_LINK","seoDescription":"","viewerEdge":{"__ref":"PostViewerEdge:postId:550d929126c8-viewerId:lo_caae6e9abf64"},"isSuspended":false,"license":"ALL_RIGHTS_RESERVED","tags":[{"__ref":"Tag:android"},{"__ref":"Tag:testing"},{"__ref":"Tag:mobile"},{"__ref":"Tag:native"}],"isNewsletter":false,"statusForCollection":"APPROVED","pendingCollection":null,"detectedLanguage":"en","wordCount":1721,"layerCake":3,"responsesLocked":false}}</script><script>window.__MIDDLEWARE_STATE__={"session":{"xsrf":""},"cache":{"cacheStatus":"MISS"}}</script><script src="https://cdn-client.medium.com/lite/static/js/manifest.b2314f6d.js"></script><script src="https://cdn-client.medium.com/lite/static/js/9865.1496d74a.js"></script><script src="https://cdn-client.medium.com/lite/static/js/main.24534aeb.js"></script><script src="https://cdn-client.medium.com/lite/static/js/instrumentation.d9108df7.chunk.js"></script> <script src="https://cdn-client.medium.com/lite/static/js/reporting.ff22a7a5.chunk.js"></script> <script src="https://cdn-client.medium.com/lite/static/js/9120.5df29668.chunk.js"></script> <script src="https://cdn-client.medium.com/lite/static/js/5049.d1ead72d.chunk.js"></script> <script src="https://cdn-client.medium.com/lite/static/js/4810.6318add7.chunk.js"></script> <script src="https://cdn-client.medium.com/lite/static/js/6618.db187378.chunk.js"></script> <script src="https://cdn-client.medium.com/lite/static/js/2707.b0942613.chunk.js"></script> <script src="https://cdn-client.medium.com/lite/static/js/9977.5b3eb23a.chunk.js"></script> <script src="https://cdn-client.medium.com/lite/static/js/8599.1ab63137.chunk.js"></script> <script src="https://cdn-client.medium.com/lite/static/js/5250.9f9e01d2.chunk.js"></script> <script src="https://cdn-client.medium.com/lite/static/js/6349.b071a958.chunk.js"></script> <script src="https://cdn-client.medium.com/lite/static/js/2648.26563adf.chunk.js"></script> <script src="https://cdn-client.medium.com/lite/static/js/8393.826a25fb.chunk.js"></script> <script src="https://cdn-client.medium.com/lite/static/js/7079.67349d50.chunk.js"></script> <script src="https://cdn-client.medium.com/lite/static/js/3735.afb7e926.chunk.js"></script> <script src="https://cdn-client.medium.com/lite/static/js/5642.a2d9f6a1.chunk.js"></script> <script src="https://cdn-client.medium.com/lite/static/js/6546.cd03f950.chunk.js"></script> <script src="https://cdn-client.medium.com/lite/static/js/6834.08de95de.chunk.js"></script> <script src="https://cdn-client.medium.com/lite/static/js/7346.72622eb9.chunk.js"></script> <script src="https://cdn-client.medium.com/lite/static/js/2420.2a5e2d95.chunk.js"></script> <script src="https://cdn-client.medium.com/lite/static/js/839.ca7937c2.chunk.js"></script> <script src="https://cdn-client.medium.com/lite/static/js/7975.d195c6f1.chunk.js"></script> <script src="https://cdn-client.medium.com/lite/static/js/2106.21ff89d3.chunk.js"></script> <script src="https://cdn-client.medium.com/lite/static/js/7394.3d049572.chunk.js"></script> <script src="https://cdn-client.medium.com/lite/static/js/2961.00a48598.chunk.js"></script> <script src="https://cdn-client.medium.com/lite/static/js/8204.c4082863.chunk.js"></script> <script src="https://cdn-client.medium.com/lite/static/js/4391.59acaed3.chunk.js"></script> <script src="https://cdn-client.medium.com/lite/static/js/PostPage.MainContent.c8a11795.chunk.js"></script> <script src="https://cdn-client.medium.com/lite/static/js/8414.6565ad5f.chunk.js"></script> <script src="https://cdn-client.medium.com/lite/static/js/3974.8d3e0217.chunk.js"></script> <script src="https://cdn-client.medium.com/lite/static/js/2527.a0afad8a.chunk.js"></script> <script src="https://cdn-client.medium.com/lite/static/js/PostResponsesContent.36c2ecf4.chunk.js"></script><script>window.main();</script><script>(function(){function c(){var b=a.contentDocument||a.contentWindow.document;if(b){var d=b.createElement('script');d.innerHTML="window.__CF$cv$params={r:'8e7363f4db89cdde',t:'MTczMjM4ODMwNC4wMDAwMDA='};var a=document.createElement('script');a.nonce='';a.src='/cdn-cgi/challenge-platform/scripts/jsd/main.js';document.getElementsByTagName('head')[0].appendChild(a);";b.getElementsByTagName('head')[0].appendChild(d)}}if(document.body){var a=document.createElement('iframe');a.height=1;a.width=1;a.style.position='absolute';a.style.top=0;a.style.left=0;a.style.border='none';a.style.visibility='hidden';document.body.appendChild(a);if('loading'!==document.readyState)c();else if(window.addEventListener)document.addEventListener('DOMContentLoaded',c);else{var e=document.onreadystatechange||function(){};document.onreadystatechange=function(b){e(b);'loading'!==document.readyState&&(document.onreadystatechange=e,c())}}}})();</script></body></html>