CINXE.COM
<!doctype html><html lang="en"><head><title data-rh="true">Unit Testing Combine Publishers. Let’s review what kind of expectations… | by Joan Duat | New Work Development</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="2023-05-19T09:49:47.671Z"/><meta data-rh="true" name="title" content="Unit Testing Combine Publishers. Let’s review what kind of expectations… | by Joan Duat | New Work Development"/><meta data-rh="true" property="og:title" content="Unit Testing Combine Publishers"/><meta data-rh="true" property="al:android:url" content="medium://p/6f581e30c370"/><meta data-rh="true" property="al:ios:url" content="medium://p/6f581e30c370"/><meta data-rh="true" property="al:android:app_name" content="Medium"/><meta data-rh="true" name="description" content="Tools and tips for testing Combine related code in Swift with Xcode and XCTestExpectation"/><meta data-rh="true" property="og:description" content="Let’s review what kind of expectations are available in Xcode and which ones can be used for testing combine Publishers"/><meta data-rh="true" property="og:url" content="https://tech.new-work.se/unit-testing-combine-publishers-6f581e30c370"/><meta data-rh="true" property="al:web:url" content="https://tech.new-work.se/unit-testing-combine-publishers-6f581e30c370"/><meta data-rh="true" property="og:image" content="https://miro.medium.com/v2/resize:fit:640/1*8rVBw8xq4W8WvGjW--BNeA.jpeg"/><meta data-rh="true" property="article:author" content="https://medium.com/@joanduat"/><meta data-rh="true" name="author" content="Joan Duat"/><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="Unit Testing Combine Publishers"/><meta data-rh="true" name="twitter:site" content="@xingdevs"/><meta data-rh="true" name="twitter:app:url:iphone" content="medium://p/6f581e30c370"/><meta data-rh="true" property="twitter:description" content="Let’s review what kind of expectations are available in Xcode and which ones can be used for testing combine Publishers"/><meta data-rh="true" name="twitter:image:src" content="https://miro.medium.com/v2/resize:fit:640/1*8rVBw8xq4W8WvGjW--BNeA.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="4 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" 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/@joanduat"/><link data-rh="true" rel="canonical" href="https://tech.new-work.se/unit-testing-combine-publishers-6f581e30c370"/><link data-rh="true" rel="alternate" href="android-app://com.medium.reader/https/medium.com/p/6f581e30c370"/><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*8rVBw8xq4W8WvGjW--BNeA.jpeg"],"url":"https:\u002F\u002Ftech.new-work.se\u002Funit-testing-combine-publishers-6f581e30c370","dateCreated":"2023-05-19T09:49:47.671Z","datePublished":"2023-05-19T09:49:47.671Z","dateModified":"2023-05-19T09:49:56.148Z","headline":"Unit Testing Combine Publishers - New Work Development","name":"Unit Testing Combine Publishers - New Work Development","description":"Tools and tips for testing Combine related code in Swift with Xcode and XCTestExpectation","identifier":"6f581e30c370","author":{"@type":"Person","name":"Joan Duat","url":"https:\u002F\u002Ftech.new-work.se\u002F@joanduat"},"creator":["Joan Duat"],"publisher":{"@type":"Organization","name":"New Work Development","url":"tech.new-work.se","logo":{"@type":"ImageObject","width":272,"height":60,"url":"https:\u002F\u002Fmiro.medium.com\u002Fv2\u002Fresize:fit:544\u002F7*V1_7XP4snlmqrc_0Njontw.png"}},"mainEntityOfPage":"https:\u002F\u002Ftech.new-work.se\u002Funit-testing-combine-publishers-6f581e30c370"}</script><style type="text/css" data-fela-rehydration="555" 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="555" 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="555" 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(26, 137, 23, 1)}.es{border-color:rgba(26, 137, 23, 1)}.ew:disabled{cursor:inherit !important}.ex:disabled{opacity:0.3}.ey:disabled:hover{background:rgba(26, 137, 23, 1)}.ez:disabled:hover{border-color:rgba(26, 137, 23, 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(26, 137, 23, 1)}.iu{fill:rgba(26, 137, 23, 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{margin-left:auto}.nw{margin-right:auto}.nx{max-width:640px}.od{clear:both}.oe{height:auto}.of{margin-top:10px}.og{max-width:728px}.oj{text-decoration:underline}.ok{line-height:1.18}.ol{letter-spacing:-0.022em}.om{font-weight:600}.pc{margin-bottom:-0.31em}.pi{padding:2px 4px}.pj{font-size:75%}.pk> strong{font-family:inherit}.pl{font-family:source-code-pro, Menlo, Monaco, "Courier New", Courier, monospace}.pm{overflow-x:auto}.pn{padding:32px}.po{border:1px solid #E5E5E5}.pp{line-height:1.4}.pq{margin-top:-0.2em}.pr{margin-bottom:-0.2em}.ps{white-space:pre}.pt{min-width:fit-content}.pu{list-style-type:disc}.pv{margin-left:30px}.pw{padding-left:0px}.qc{line-height:1.12}.qv{margin-bottom:-0.28em}.qw{margin-bottom:26px}.qx{margin-top:6px}.qy{margin-top:8px}.qz{margin-right:8px}.ra{padding:8px 16px}.rb{border-radius:100px}.rc{transition:background 300ms ease}.re{white-space:nowrap}.rf{border-top:none}.rg{margin-bottom:14px}.rh{height:52px}.ri{max-height:52px}.rj{box-sizing:content-box}.rk{position:static}.rm{max-width:155px}.rs{margin-right:20px}.ry{height:0px}.rz{margin-bottom:40px}.sa{margin-bottom:48px}.so{border-radius:2px}.sq{height:64px}.sr{width:64px}.ss{align-self:flex-end}.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{flex:1 1 auto}.ti{padding-right:4px}.tj{font-weight:500}.tq{margin-top:16px}.tr{margin-bottom:54px}.tx{gap:18px}.ty{fill:rgba(61, 61, 61, 1)}.uf{border-bottom:solid 1px #E5E5E5}.ug{margin-top:72px}.uh{padding:24px 0}.ui{margin-bottom:0px}.uj{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(15, 115, 12, 1)}.eu:hover{border-color:rgba(15, 115, 12, 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(15, 115, 12, 1)}.iw:hover:not(:disabled){fill:rgba(15, 115, 12, 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}.rd:hover{background-color:#F2F2F2}.sp:hover{background-color:none}.sx:hover{background:#000000}.sy:hover{border-color:#242424}.tz: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="555" 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}.oc{margin-top:56px}.oz{margin-top:1.72em}.pa{line-height:24px}.pb{letter-spacing:0}.ph{margin-top:0.94em}.qb{margin-top:1.14em}.qr{font-size:24px}.qs{margin-top:1.95em}.qt{line-height:30px}.qu{letter-spacing:-0.016em}.rr{display:inline-block}.rx{margin-bottom:104px}.sb{flex-direction:row}.se{margin-bottom:0}.sf{margin-right:20px}.td{max-width:500px}.tw{margin-bottom:72px}.ue{padding-top:72px}</style><style type="text/css" data-fela-rehydration="555" data-fela-type="RULE" media="all and (max-width: 1079.98px)">.e{display:none}.ln{margin-top:0px}.oh{margin-left:auto}.oi{text-align:center}.rq{display:inline-block}</style><style type="text/css" data-fela-rehydration="555" data-fela-type="RULE" media="all and (max-width: 903.98px)">.f{display:none}.lm{margin-top:0px}.rp{display:inline-block}</style><style type="text/css" data-fela-rehydration="555" data-fela-type="RULE" media="all and (max-width: 727.98px)">.g{display:none}.lk{margin-top:0px}.ll{margin-right:0px}.ro{display:inline-block}</style><style type="text/css" data-fela-rehydration="555" 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}.ny{margin-top:40px}.on{font-size:16px}.oo{margin-top:1.23em}.op{letter-spacing:0}.pd{margin-top:0.67em}.px{margin-top:1.34em}.qd{font-size:20px}.qe{margin-top:1.2em}.qf{line-height:24px}.rn{display:inline-block}.rt{margin-bottom:96px}.sm{margin-bottom:20px}.sn{margin-right:0}.th{max-width:100%}.tk{font-size:24px}.tl{line-height:30px}.tm{letter-spacing:-0.016em}.ts{margin-bottom:64px}.ua{padding-top:48px}.mn:hover{border-color:#E5E5E5}</style><style type="text/css" data-fela-rehydration="555" 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}.ob{margin-top:56px}.ow{margin-top:1.72em}.ox{line-height:24px}.oy{letter-spacing:0}.pg{margin-top:0.94em}.qa{margin-top:1.14em}.qn{font-size:24px}.qo{margin-top:1.95em}.qp{line-height:30px}.qq{letter-spacing:-0.016em}.rw{margin-bottom:104px}.sc{flex-direction:row}.sg{margin-bottom:0}.sh{margin-right:20px}.te{max-width:500px}.tv{margin-bottom:72px}.ud{padding-top:72px}</style><style type="text/css" data-fela-rehydration="555" 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}.oa{margin-top:56px}.ot{margin-top:1.72em}.ou{line-height:24px}.ov{letter-spacing:0}.pf{margin-top:0.94em}.pz{margin-top:1.14em}.qj{font-size:24px}.qk{margin-top:1.95em}.ql{line-height:30px}.qm{letter-spacing:-0.016em}.rv{margin-bottom:104px}.sd{flex-direction:row}.si{margin-bottom:0}.sj{margin-right:20px}.tf{max-width:500px}.tu{margin-bottom:72px}.uc{padding-top:72px}</style><style type="text/css" data-fela-rehydration="555" 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}.nz{margin-top:40px}.oq{font-size:16px}.or{margin-top:1.23em}.os{letter-spacing:0}.pe{margin-top:0.67em}.py{margin-top:1.34em}.qg{font-size:20px}.qh{margin-top:1.2em}.qi{line-height:24px}.ru{margin-bottom:96px}.sk{margin-bottom:20px}.sl{margin-right:0}.tg{max-width:100%}.tn{font-size:24px}.to{line-height:30px}.tp{letter-spacing:-0.016em}.tt{margin-bottom:64px}.ub{padding-top:48px}.mu:hover{border-color:#E5E5E5}</style><style type="text/css" data-fela-rehydration="555" data-fela-type="RULE" media="print">.rl{display:none}</style><style type="text/css" data-fela-rehydration="555" 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%2F6f581e30c370&%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><a 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" href="https://medium.com/m/signin?operation=register&redirect=https%3A%2F%2Ftech.new-work.se%2Funit-testing-combine-publishers-6f581e30c370&source=post_page---top_nav_layout_nav-----------------------global_nav-----------" rel="noopener follow">Sign up</a></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" href="https://medium.com/m/signin?operation=login&redirect=https%3A%2F%2Ftech.new-work.se%2Funit-testing-combine-publishers-6f581e30c370&source=post_page---top_nav_layout_nav-----------------------global_nav-----------" rel="noopener follow">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" href="https://medium.com/?source=---top_nav_layout_nav----------------------------------" rel="noopener follow"><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" href="https://medium.com/m/signin?operation=register&redirect=https%3A%2F%2Fmedium.com%2Fnew-story&source=---top_nav_layout_nav-----------------------new_post_topnav-----------" rel="noopener follow"><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" href="https://medium.com/search?source=---top_nav_layout_nav----------------------------------" rel="noopener follow"><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><a 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" href="https://medium.com/m/signin?operation=register&redirect=https%3A%2F%2Ftech.new-work.se%2Funit-testing-combine-publishers-6f581e30c370&source=post_page---top_nav_layout_nav-----------------------global_nav-----------" rel="noopener follow">Sign up</a></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" href="https://medium.com/m/signin?operation=login&redirect=https%3A%2F%2Ftech.new-work.se%2Funit-testing-combine-publishers-6f581e30c370&source=post_page---top_nav_layout_nav-----------------------global_nav-----------" rel="noopener follow">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="b4a2" 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">Unit Testing Combine Publishers</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 href="https://medium.com/@joanduat?source=post_page---byline--6f581e30c370--------------------------------" rel="noopener follow"><div class="l ib ic by id ie"><div class="l fj"><img alt="Joan Duat" class="l fd by dd de cx" src="https://miro.medium.com/v2/resize:fill:88:88/1*ty0A4uaPE0lAW18VI8XNWg.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://tech.new-work.se/?source=post_page---byline--6f581e30c370--------------------------------" rel="noopener ugc nofollow"><div class="l ii ij by id ik"><div class="l fj"><img alt="New Work Development" class="l fd by br il cx" src="https://miro.medium.com/v2/resize:fill:48:48/1*B1MD9EGz7rhVkx3WE3Rkog.png" 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" href="https://medium.com/@joanduat?source=post_page---byline--6f581e30c370--------------------------------" rel="noopener follow">Joan Duat</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" href="https://medium.com/m/signin?actionUrl=https%3A%2F%2Fmedium.com%2F_%2Fsubscribe%2Fuser%2F52fa09649919&operation=register&redirect=https%3A%2F%2Ftech.new-work.se%2Funit-testing-combine-publishers-6f581e30c370&user=Joan+Duat&userId=52fa09649919&source=post_page-52fa09649919--byline--6f581e30c370---------------------post_header-----------" rel="noopener follow">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://tech.new-work.se/?source=post_page---byline--6f581e30c370--------------------------------" rel="noopener ugc nofollow"><p class="bf b bg z jf jg jh ji jj jk jl jm bk">New Work Development</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">4 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">May 19, 2023</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" href="https://medium.com/m/signin?actionUrl=https%3A%2F%2Fmedium.com%2F_%2Fvote%2Fxing-engineering%2F6f581e30c370&operation=register&redirect=https%3A%2F%2Ftech.new-work.se%2Funit-testing-combine-publishers-6f581e30c370&user=Joan+Duat&userId=52fa09649919&source=---header_actions--6f581e30c370---------------------clap_footer-----------" rel="noopener follow"><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" href="https://medium.com/m/signin?actionUrl=https%3A%2F%2Fmedium.com%2F_%2Fbookmark%2Fp%2F6f581e30c370&operation=register&redirect=https%3A%2F%2Ftech.new-work.se%2Funit-testing-combine-publishers-6f581e30c370&source=---header_actions--6f581e30c370---------------------bookmark_footer-----------" rel="noopener follow"><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="8834" 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">When testing Combine-related code we must keep in mind that we’re most probably testing asynchronous code. And how do we <em class="nu">expect</em> to test asynchronous code in Xcode? Right, with expectations.</p><p id="87ba" 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">Expectations in Xcode allow us to define specific conditions that must be met before a test case can be considered successful or failed. By using expectations, we can accurately validate the behaviour of Combine publishers and other asynchronous code.</p><p id="2cda" 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 this article, we will analyse the various kinds of expectations available in Xcode and the code required to set them up. Finally, we’ll propose a new method that not only reduces the boilerplate code but also improves the readability of our tests.</p><figure class="ny nz oa ob oc od nv nw paragraph-image"><div class="nv nw nx"><picture><source srcSet="https://miro.medium.com/v2/resize:fit:640/format:webp/1*8rVBw8xq4W8WvGjW--BNeA.jpeg 640w, https://miro.medium.com/v2/resize:fit:720/format:webp/1*8rVBw8xq4W8WvGjW--BNeA.jpeg 720w, https://miro.medium.com/v2/resize:fit:750/format:webp/1*8rVBw8xq4W8WvGjW--BNeA.jpeg 750w, https://miro.medium.com/v2/resize:fit:786/format:webp/1*8rVBw8xq4W8WvGjW--BNeA.jpeg 786w, https://miro.medium.com/v2/resize:fit:828/format:webp/1*8rVBw8xq4W8WvGjW--BNeA.jpeg 828w, https://miro.medium.com/v2/resize:fit:1100/format:webp/1*8rVBw8xq4W8WvGjW--BNeA.jpeg 1100w, https://miro.medium.com/v2/resize:fit:1280/format:webp/1*8rVBw8xq4W8WvGjW--BNeA.jpeg 1280w" 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, 640px" type="image/webp"/><source data-testid="og" srcSet="https://miro.medium.com/v2/resize:fit:640/1*8rVBw8xq4W8WvGjW--BNeA.jpeg 640w, https://miro.medium.com/v2/resize:fit:720/1*8rVBw8xq4W8WvGjW--BNeA.jpeg 720w, https://miro.medium.com/v2/resize:fit:750/1*8rVBw8xq4W8WvGjW--BNeA.jpeg 750w, https://miro.medium.com/v2/resize:fit:786/1*8rVBw8xq4W8WvGjW--BNeA.jpeg 786w, https://miro.medium.com/v2/resize:fit:828/1*8rVBw8xq4W8WvGjW--BNeA.jpeg 828w, https://miro.medium.com/v2/resize:fit:1100/1*8rVBw8xq4W8WvGjW--BNeA.jpeg 1100w, https://miro.medium.com/v2/resize:fit:1280/1*8rVBw8xq4W8WvGjW--BNeA.jpeg 1280w" 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, 640px"/><img alt="" class="bh md oe c" width="640" height="349" loading="eager" role="presentation"/></picture></div><figcaption class="of ff og nv nw oh oi bf b bg z du">Photo by <a class="af oj" href="https://unsplash.com/@alex_vicol?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText" rel="noopener ugc nofollow" target="_blank">Alexandru Yicol</a> on <a class="af oj" href="https://unsplash.com/photos/D9FQYwAclwQ?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText" rel="noopener ugc nofollow" target="_blank">Unsplash</a></figcaption></figure><p id="6b7a" 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">Let’s start by looking at what kind of expectations are available in Xcode and which ones are best suited for testing Publishers:</p><h2 id="6343" class="ok ol gu bf om on oo dy op oq or ea os nh ot ou ov nl ow ox oy np oz pa pb pc bk"><a class="af oj" href="https://developer.apple.com/documentation/xctest/xctestexpectation" rel="noopener ugc nofollow" target="_blank"><strong class="al">XCTestExpectation</strong></a></h2><p id="c6d2" class="pw-post-body-paragraph mw mx gu my b mz pd nb nc nd pe nf ng nh pf nj nk nl pg nn no np ph nr ns nt gn bk">This is the basic type of expectation since it allows us to manually control its outcome by calling <code class="cx pi pj pk pl b">fulfill()</code>. It can be used to wait until a publisher emits some specific value and we can easily apply this to any <code class="cx pi pj pk pl b">@Published</code> property wrapper for that matter:</p><pre class="ny nz oa ob oc pm pl pn bp po bb bk"><span id="2cbb" class="pp ol gu pl b bg pq pr l ps pt">let expectation = XCTestExpectation(description: "Wait for the publisher to emit the expected value")<br/>viewModel.$keywords.sink { _ in<br/>} receiveValue: { value in<br/> if value.contains("Cool") {<br/> expectation.fulfill()<br/> }<br/>}<br/>.store(in: &cancellables)<br/><br/>wait(for: [expectation], timeout: 1)</span></pre><h2 id="394d" class="ok ol gu bf om on oo dy op oq or ea os nh ot ou ov nl ow ox oy np oz pa pb pc bk"><a class="af oj" href="https://developer.apple.com/documentation/xctest/xctkvoexpectation" rel="noopener ugc nofollow" target="_blank"><strong class="al">XCTKVOExpectation</strong></a></h2><p id="082e" class="pw-post-body-paragraph mw mx gu my b mz pd nb nc nd pe nf ng nh pf nj nk nl pg nn no np ph nr ns nt gn bk">This is an expectation that fulfils on a specific <a class="af oj" href="https://developer.apple.com/documentation/swift/using-key-value-observing-in-swift" rel="noopener ugc nofollow" target="_blank">key-value observing</a> (KVO) condition. But this one is deprecated and Apple promotes using the next one:</p><h2 id="41c9" class="ok ol gu bf om on oo dy op oq or ea os nh ot ou ov nl ow ox oy np oz pa pb pc bk"><a class="af oj" href="https://developer.apple.com/documentation/xctest/xctkeypathexpectation" rel="noopener ugc nofollow" target="_blank"><strong class="al">XCTKeyPathExpectation</strong></a></h2><p id="1fe9" class="pw-post-body-paragraph mw mx gu my b mz pd nb nc nd pe nf ng nh pf nj nk nl pg nn no np ph nr ns nt gn bk">This expectation allows waiting for changes to a property specified by key path for a given object. It can be used like this:</p><pre class="ny nz oa ob oc pm pl pn bp po bb bk"><span id="a8f8" class="pp ol gu pl b bg pq pr l ps pt">let loadedExpectation = XCTKeyPathExpectation(keyPath: \ViewModel.isLoaded, observedObject: viewModel, expectedValue: true)</span></pre><p id="05de" 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 cannot be applied directly to publishers we may think it can be used at least for testing <code class="cx pi pj pk pl b">@Published</code> properties but it has some constraints:</p><ul class=""><li id="60c0" 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 pu pv pw bk">The tested object must inherit from <code class="cx pi pj pk pl b">NSObject</code></li><li id="eaed" class="mw mx gu my b mz px nb nc nd py nf ng nh pz nj nk nl qa nn no np qb nr ns nt pu pv pw bk">The object properties we want to observe must be declared as <code class="cx pi pj pk pl b">@objc dynamic</code></li><li id="89bc" class="mw mx gu my b mz px nb nc nd py nf ng nh pz nj nk nl qa nn no np qb nr ns nt pu pv pw bk">The property types that we want to observe must also be representable in Objective-C.</li></ul><p id="b350" 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">Even if we adapt our code with all those requirements we would still be able to test only a specific value, not the stream of values from the publisher or their completion.</p><p id="ceeb" 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 is far from ideal so let’s look at the next one:</p><h2 id="07b3" class="ok ol gu bf om on oo dy op oq or ea os nh ot ou ov nl ow ox oy np oz pa pb pc bk"><a class="af oj" href="https://developer.apple.com/documentation/xctest/xctnspredicateexpectation" rel="noopener ugc nofollow" target="_blank"><strong class="al">XCTNSPredicateExpectation</strong></a></h2><p id="4472" class="pw-post-body-paragraph mw mx gu my b mz pd nb nc nd pe nf ng nh pf nj nk nl pg nn no np ph nr ns nt gn bk">This is an expectation that is fulfilled when an <code class="cx pi pj pk pl b">NSPredicate</code> is satisfied. It can be used as:</p><pre class="ny nz oa ob oc pm pl pn bp po bb bk"><span id="c639" class="pp ol gu pl b bg pq pr l ps pt">let predicateExpectation = XCTNSPredicateExpectation(predicate: NSPredicate { _,_ in<br/> viewModel.isLoaded<br/>}, object: .none)</span></pre><p id="cd73" 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">But when using this you probably found out that, even if the tested code is fast, the expectation must be awaited for at least more than a second:</p><pre class="ny nz oa ob oc pm pl pn bp po bb bk"><span id="bbf4" class="pp ol gu pl b bg pq pr l ps pt">wait(for: [predicateExpectation], timeout: 1) // this fails<br/>wait(for: [predicateExpectation], timeout: 1.5) // this succeeds</span></pre><p id="c2a9" 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">It turns out that <code class="cx pi pj pk pl b">XCTNSPredicateExpectation</code> is slow because it uses some kind of polling mechanism to check the predicate periodically thus makes it best suited for UI tests. So it’s better to avoid it in unit tests if we want them to run as fast as possible.</p><h2 id="29cc" class="ok ol gu bf om on oo dy op oq or ea os nh ot ou ov nl ow ox oy np oz pa pb pc bk"><a class="af oj" href="https://developer.apple.com/documentation/xctest/xctnsnotificationexpectation" rel="noopener ugc nofollow" target="_blank"><strong class="al">XCTNSNotificationExpectation</strong></a></h2><p id="1074" class="pw-post-body-paragraph mw mx gu my b mz pd nb nc nd pe nf ng nh pf nj nk nl pg nn no np ph nr ns nt gn bk">Finally, there’s this expectation that is fulfilled when an expected <code class="cx pi pj pk pl b">NSNotification</code> is received. It can be useful in some scenarios but again, not so much for testing Combine-based code.</p><h1 id="0d3a" class="qc ol gu bf om qd qe qf op qg qh qi os qj qk ql qm qn qo qp qq qr qs qt qu qv bk">Level-set the expectations</h1><p id="3f28" class="pw-post-body-paragraph mw mx gu my b mz pd nb nc nd pe nf ng nh pf nj nk nl pg nn no np ph nr ns nt gn bk">After looking at the options above it’s clear that we’re just left with the <code class="cx pi pj pk pl b">XCTestExpectation</code> as the only choice for testing Publishers.</p><p id="9280" 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">But writing tests this way always involves some boilerplate code, starting from the the expectation creation, subscribing to the publisher, manually fulfilling the expectation and finally storing the cancellable.</p><p id="b427" 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">If we care about test readability in such a way they are easy to understand then let’s try to simplify this process by introducing our own custom expectation:</p><h1 id="a04d" class="qc ol gu bf om qd qe qf op qg qh qi os qj qk ql qm qn qo qp qq qr qs qt qu qv bk">PublisherValueExpectation</h1><p id="73d7" class="pw-post-body-paragraph mw mx gu my b mz pd nb nc nd pe nf ng nh pf nj nk nl pg nn no np ph nr ns nt gn bk">We can create an <code class="cx pi pj pk pl b">XCTestExpectation</code> subclass that does this repetitive task for us. Here, the <code class="cx pi pj pk pl b">PublisherValueExpectation</code> is an expectation that will be fulfilled automatically when the publisher emits a value that matches a given condition.</p><pre class="ny nz oa ob oc pm pl pn bp po bb bk"><span id="f659" class="pp ol gu pl b bg pq pr l ps pt">public final class PublisherValueExpectation<P: Publisher>: XCTestExpectation {<br/> private var cancellable: AnyCancellable?<br/><br/> public init(<br/> _ publisher: P,<br/> condition: @escaping (P.Output) -> Bool)<br/> {<br/> super.init(description: "Publisher expected to emit a value that matches the condition.")<br/> cancellable = publisher.sink { _ in<br/> } receiveValue: { [weak self] value in<br/> if condition(value) {<br/> self?.fulfill()<br/> }<br/> }<br/> }</span></pre><p id="d885" 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 we can just write:</p><pre class="ny nz oa ob oc pm pl pn bp po bb bk"><span id="e039" class="pp ol gu pl b bg pq pr l ps pt">let publisherExpectation = PublisherValueExpectation(viewModel.$keywords) { $0.contains("Cool") }</span></pre><p id="8513" 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 can also add a convenience initializer to simply pass an expected value if that conforms to <code class="cx pi pj pk pl b">Equatable</code>:</p><pre class="ny nz oa ob oc pm pl pn bp po bb bk"><span id="3003" class="pp ol gu pl b bg pq pr l ps pt">public convenience init(<br/> _ publisher: P,<br/> expectedValue: P.Output<br/>) where P.Output: Equatable<br/>{<br/> let description = "Publisher expected to emit the value '\(expectedValue)'"<br/> self.init(publisher, condition: { $0 == expectedValue }, description: description)<br/>}</span></pre><p id="0283" 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">And this allows us writing an even more compact expectation that reads nicely:</p><pre class="ny nz oa ob oc pm pl pn bp po bb bk"><span id="afa8" class="pp ol gu pl b bg pq pr l ps pt">let publisherExpectation = PublisherValueExpectation(viewModel.$isLoaded, expectedValue: true)</span></pre><p id="8449" 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">Thanks to Combine we can adapt the tested publisher to check many things. For instance:</p><ul class=""><li id="d34a" 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 pu pv pw bk">Expect many values to be emitted by the publisher</li></ul><pre class="ny nz oa ob oc pm pl pn bp po bb bk"><span id="1abf" class="pp ol gu pl b bg pq pr l ps pt">let publisherExpectation = PublisherValueExpectation(publisher.collect(3), expectedValue: [1,2,3])</span></pre><ul class=""><li id="b77e" 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 pu pv pw bk">Expect the first or last value being emitted by the publisher</li></ul><pre class="ny nz oa ob oc pm pl pn bp po bb bk"><span id="1d49" class="pp ol gu pl b bg pq pr l ps pt">let publisherExpectation = PublisherValueExpectation(publisher.first(), expectedValue: 1)<br/>let publisherExpectation = PublisherValueExpectation(publisher.last(), expectedValue: 5)</span></pre><h1 id="7689" class="qc ol gu bf om qd qe qf op qg qh qi os qj qk ql qm qn qo qp qq qr qs qt qu qv bk">Keep up with the expectations</h1><p id="956c" class="pw-post-body-paragraph mw mx gu my b mz pd nb nc nd pe nf ng nh pf nj nk nl pg nn no np ph nr ns nt gn bk">The full project can be found in <a class="af oj" href="https://github.com/yannxou/PublisherExpectations" rel="noopener ugc nofollow" target="_blank">GitHub</a>. It also includes two more useful expectations:</p><ul class=""><li id="60f8" 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 pu pv pw bk"><strong class="my gv">PublisherFinishedExpectation</strong>: Wait for a publisher to complete successfully (optionally after emitting a certain value or condition)</li></ul><pre class="ny nz oa ob oc pm pl pn bp po bb bk"><span id="900c" class="pp ol gu pl b bg pq pr l ps pt">let publisherExpectation = PublisherFinishedExpectation(publisher, expectedValue: 2)</span></pre><ul class=""><li id="7db9" 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 pu pv pw bk"><strong class="my gv">PublisherFailureExpectation</strong>: Wait for a publisher to complete with a failure (optionally with an expected error)</li></ul><pre class="ny nz oa ob oc pm pl pn bp po bb bk"><span id="c348" class="pp ol gu pl b bg pq pr l ps pt">let publisherExpectation = PublisherFailureExpectation(publisher, expectedError: ApiError(code: 100))</span></pre><h1 id="7a42" class="qc ol gu bf om qd qe qf op qg qh qi os qj qk ql qm qn qo qp qq qr qs qt qu qv bk">Conclusion</h1><p id="3dbf" class="pw-post-body-paragraph mw mx gu my b mz pd nb nc nd pe nf ng nh pf nj nk nl pg nn no np ph nr ns nt gn bk">There are many alternatives to do assertions on publishers but this is a way that is familiar to anyone that already uses test expectations in Xcode and can be easily adopted to existing tests.</p><p id="a51b" 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">Thanks for reading.</p></div></div></div></div></section></div></div></article></div><div class="ab cb"><div class="ci bh fz ga gb gc"><div class="qw qx ab ja"><div class="qy ab"><a class="qz ay am ao" href="https://medium.com/tag/swift?source=post_page-----6f581e30c370--------------------------------" rel="noopener follow"><div class="ra fj cx rb ge rc rd bf b bg z bk re">Swift</div></a></div><div class="qy ab"><a class="qz ay am ao" href="https://medium.com/tag/combine?source=post_page-----6f581e30c370--------------------------------" rel="noopener follow"><div class="ra fj cx rb ge rc rd bf b bg z bk re">Combine</div></a></div><div class="qy ab"><a class="qz ay am ao" href="https://medium.com/tag/unit-testing?source=post_page-----6f581e30c370--------------------------------" rel="noopener follow"><div class="ra fj cx rb ge rc rd bf b bg z bk re">Unit Testing</div></a></div><div class="qy ab"><a class="qz ay am ao" href="https://medium.com/tag/xcode?source=post_page-----6f581e30c370--------------------------------" rel="noopener follow"><div class="ra fj cx rb ge rc rd bf b bg z bk re">Xcode</div></a></div><div class="qy ab"><a class="qz ay am ao" href="https://medium.com/tag/expectations?source=post_page-----6f581e30c370--------------------------------" rel="noopener follow"><div class="ra fj cx rb ge rc rd bf b bg z bk re">Expectations</div></a></div></div></div></div><div class="l"></div><footer class="rf rg rh ri rj ab q rk ik c"><div class="l ae"><div class="ab cb"><div class="ci bh fz ga gb gc"><div class="ab cp rl"><div class="ab q kv"><div class="rm l"><span class="l rn ro rp 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" href="https://medium.com/m/signin?actionUrl=https%3A%2F%2Fmedium.com%2F_%2Fvote%2Fxing-engineering%2F6f581e30c370&operation=register&redirect=https%3A%2F%2Ftech.new-work.se%2Funit-testing-combine-publishers-6f581e30c370&user=Joan+Duat&userId=52fa09649919&source=---footer_actions--6f581e30c370---------------------clap_footer-----------" rel="noopener follow"><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 rq rr"><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" href="https://medium.com/m/signin?actionUrl=https%3A%2F%2Fmedium.com%2F_%2Fvote%2Fxing-engineering%2F6f581e30c370&operation=register&redirect=https%3A%2F%2Ftech.new-work.se%2Funit-testing-combine-publishers-6f581e30c370&user=Joan+Duat&userId=52fa09649919&source=---footer_actions--6f581e30c370---------------------clap_footer-----------" rel="noopener follow"><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="rs 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" href="https://medium.com/m/signin?actionUrl=https%3A%2F%2Fmedium.com%2F_%2Fbookmark%2Fp%2F6f581e30c370&operation=register&redirect=https%3A%2F%2Ftech.new-work.se%2Funit-testing-combine-publishers-6f581e30c370&source=---footer_actions--6f581e30c370---------------------bookmark_footer-----------" rel="noopener follow"><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="rs 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="rt ru rv rw rx l"><div class="ab cb"><div class="ci bh fz ga gb gc"><div class="ry bh r rz"></div><div class="sa l"><div class="ab sb sc sd iz iy"><div class="se sf sg sh si sj sk sl sm sn ab cp"><div class="h k"><a href="https://tech.new-work.se/?source=post_page---post_publication_info--6f581e30c370--------------------------------" rel="noopener follow"><div class="fj ab"><img alt="New Work Development" class="so ib ic cx" src="https://miro.medium.com/v2/resize:fill:96:96/1*B1MD9EGz7rhVkx3WE3Rkog.png" width="48" height="48" loading="lazy"/><div class="so l ic ib fs n fr sp"></div></div></a></div><div class="j i d"><a href="https://tech.new-work.se/?source=post_page---post_publication_info--6f581e30c370--------------------------------" rel="noopener follow"><div class="fj ab"><img alt="New Work Development" class="so sr sq cx" src="https://miro.medium.com/v2/resize:fill:128:128/1*B1MD9EGz7rhVkx3WE3Rkog.png" width="64" height="64" loading="lazy"/><div class="so l sq sr fs n fr sp"></div></div></a></div><div class="j i d ss ix"><div class="ab"><span><a class="bf b bg z st ra su sv sw sx sy ev ew sz ta tb fa fb fc fd bm fe ff" href="https://medium.com/m/signin?actionUrl=https%3A%2F%2Fmedium.com%2F_%2Fsubscribe%2Fcollection%2Fxing-engineering&operation=register&redirect=https%3A%2F%2Ftech.new-work.se%2Funit-testing-combine-publishers-6f581e30c370&collection=New+Work+Development&collectionId=35cb8c78d3cf&source=post_page---post_publication_info--6f581e30c370---------------------follow_profile-----------" rel="noopener follow">Follow</a></span></div></div></div><div class="ab co tc"><div class="td te tf tg th l"><a class="af ag ah aj ak al am an ao ap aq ar as at ab q" href="https://tech.new-work.se/?source=post_page---post_publication_info--6f581e30c370--------------------------------" rel="noopener follow"><h2 class="pw-author-name bf tj tk tl tm tn to tp nh ou ov nl ox oy np pa pb bk"><span class="gn ti">Published in <!-- -->New Work Development</span></h2></a><div class="qy 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="/followers?source=post_page---post_publication_info--6f581e30c370--------------------------------">1K 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="/protecting-sensitive-data-in-elixir-genservers-fac4a8b0ae15?source=post_page---post_publication_info--6f581e30c370--------------------------------">Last published <span>Nov 27, 2023</span></a></div></div><div class="tq l"><p class="bf b bg z bk"><span class="gn">Stories about building a better working world</span></p></div></div></div><div class="h k"><div class="ab"><span><a class="bf b bg z st ra su sv sw sx sy ev ew sz ta tb fa fb fc fd bm fe ff" href="https://medium.com/m/signin?actionUrl=https%3A%2F%2Fmedium.com%2F_%2Fsubscribe%2Fcollection%2Fxing-engineering&operation=register&redirect=https%3A%2F%2Ftech.new-work.se%2Funit-testing-combine-publishers-6f581e30c370&collection=New+Work+Development&collectionId=35cb8c78d3cf&source=post_page---post_publication_info--6f581e30c370---------------------follow_profile-----------" rel="noopener follow">Follow</a></span></div></div></div></div><div class="ab sb sc sd iz iy"><div class="se sf sg sh si sj sk sl sm sn ab cp"><div class="h k"><a tabindex="0" href="https://medium.com/@joanduat?source=post_page---post_author_info--6f581e30c370--------------------------------" rel="noopener follow"><div class="l fj"><img alt="Joan Duat" class="l fd by ic ib cx" src="https://miro.medium.com/v2/resize:fill:96:96/1*ty0A4uaPE0lAW18VI8XNWg.jpeg" width="48" height="48" loading="lazy"/><div class="fr by l ic ib fs n ay sp"></div></div></a></div><div class="j i d"><a tabindex="0" href="https://medium.com/@joanduat?source=post_page---post_author_info--6f581e30c370--------------------------------" rel="noopener follow"><div class="l fj"><img alt="Joan Duat" class="l fd by sq sr cx" src="https://miro.medium.com/v2/resize:fill:128:128/1*ty0A4uaPE0lAW18VI8XNWg.jpeg" width="64" height="64" loading="lazy"/><div class="fr by l sq sr fs n ay sp"></div></div></a></div><div class="j i d ss ix"><div class="ab"><span><a class="bf b bg z st ra su sv sw sx sy ev ew sz ta tb fa fb fc fd bm fe ff" href="https://medium.com/m/signin?actionUrl=https%3A%2F%2Fmedium.com%2F_%2Fsubscribe%2Fuser%2F52fa09649919&operation=register&redirect=https%3A%2F%2Ftech.new-work.se%2Funit-testing-combine-publishers-6f581e30c370&user=Joan+Duat&userId=52fa09649919&source=post_page-52fa09649919--post_author_info--6f581e30c370---------------------follow_profile-----------" rel="noopener follow">Follow</a></span></div></div></div><div class="ab co tc"><div class="td te tf tg th l"><a class="af ag ah aj ak al am an ao ap aq ar as at ab q" href="https://medium.com/@joanduat?source=post_page---post_author_info--6f581e30c370--------------------------------" rel="noopener follow"><h2 class="pw-author-name bf tj tk tl tm tn to tp nh ou ov nl ox oy np pa pb bk"><span class="gn ti">Written by <!-- -->Joan Duat</span></h2></a><div class="qy 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" href="https://medium.com/@joanduat/followers?source=post_page---post_author_info--6f581e30c370--------------------------------" rel="noopener follow">7 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" href="https://medium.com/@joanduat/following?source=post_page---post_author_info--6f581e30c370--------------------------------" rel="noopener follow">13 Following</a></div></div><div class="tq l"><p class="bf b bg z bk"><span class="gn">iOS engineer living in Barcelona. Software player and music coder.</span></p></div></div></div><div class="h k"><div class="ab"><span><a class="bf b bg z st ra su sv sw sx sy ev ew sz ta tb fa fb fc fd bm fe ff" href="https://medium.com/m/signin?actionUrl=https%3A%2F%2Fmedium.com%2F_%2Fsubscribe%2Fuser%2F52fa09649919&operation=register&redirect=https%3A%2F%2Ftech.new-work.se%2Funit-testing-combine-publishers-6f581e30c370&user=Joan+Duat&userId=52fa09649919&source=post_page-52fa09649919--post_author_info--6f581e30c370---------------------follow_profile-----------" rel="noopener follow">Follow</a></span></div></div></div></div></div></div><div class="tr l"><div class="ry bh r ts tt tu tv tw"></div><div class="ab cb"><div class="ci bh fz ga gb gc"><div class="ab q cp"><h2 class="bf tj qd qf op qg qi os qj ql qm qn qp qq qr qt qu bk">No responses yet</h2><div class="ab tx"><div><div class="bm" aria-hidden="false"><a class="ty tz" href="https://policy.medium.com/medium-rules-30e5502c4eb4?source=post_page---post_responses--6f581e30c370--------------------------------" 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="ua ub uc ud ue l bx"><div class="h k j"><div class="ry bh uf ug"></div><div class="ab cb"><div class="ci bh fz ga gb gc"><div class="uh ab kv ja"><div class="ui uj 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-----6f581e30c370--------------------------------" rel="noopener follow"><p class="bf b dv z du">Help</p></a></div><div class="ui uj 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-----6f581e30c370--------------------------------" rel="noopener follow"><p class="bf b dv z du">Status</p></a></div><div class="ui uj l"><a class="af ag ah ai aj ak al am an ao ap aq ar as at" href="https://medium.com/about?autoplay=1&source=post_page-----6f581e30c370--------------------------------" rel="noopener follow"><p class="bf b dv z du">About</p></a></div><div class="ui uj l"><a class="af ag ah ai aj ak al am an ao ap aq ar as at" href="https://medium.com/jobs-at-medium/work-at-medium-959d1a85284e?source=post_page-----6f581e30c370--------------------------------" rel="noopener follow"><p class="bf b dv z du">Careers</p></a></div><div class="ui uj 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-----6f581e30c370--------------------------------" rel="noopener follow"><p class="bf b dv z du">Press</p></a></div><div class="ui uj 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-----6f581e30c370--------------------------------" rel="noopener follow"><p class="bf b dv z du">Blog</p></a></div><div class="ui uj 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-----6f581e30c370--------------------------------" rel="noopener follow"><p class="bf b dv z du">Privacy</p></a></div><div class="ui uj 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-----6f581e30c370--------------------------------" rel="noopener follow"><p class="bf b dv z du">Terms</p></a></div><div class="ui uj 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-----6f581e30c370--------------------------------" rel="noopener follow"><p class="bf b dv z du">Text to speech</p></a></div><div class="ui l"><a class="af ag ah ai aj ak al am an ao ap aq ar as at" href="https://medium.com/business?source=post_page-----6f581e30c370--------------------------------" rel="noopener follow"><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://tech.new-work.se/_/graphql"</script><script>window.__PRELOADED_STATE__ = {"algolia":{"queries":{}},"cache":{"experimentGroupSet":true,"reason":"This request is not using the cache middleware worker","group":"disabled","tags":["group-edgeCachePosts","post-6f581e30c370","user-52fa09649919","collection-35cb8c78d3cf"],"serverVariantState":"","middlewareEnabled":false,"cacheStatus":"DYNAMIC","shouldUseCache":false,"vary":[],"lohpSummerUpsellEnabled":false,"publicationHierarchyEnabledWeb":false,"postBottomResponsesEnabled":false},"client":{"hydrated":false,"isUs":false,"isNativeMedium":false,"isSafariMobile":false,"isSafari":false,"isFirefox":false,"routingEntity":{"type":"COLLECTION","id":"35cb8c78d3cf","explicit":true},"viewerIsBot":false},"debug":{"requestId":"c126c1d3-c3df-4cc5-9035-3088e42b0e39","hybridDevServices":[],"originalSpanCarrier":{"traceparent":"00-6fe1c544aa38744885ccbc730d2aec3b-ad449de8e29272e1-01"}},"multiVote":{"clapsPerPost":{}},"navigation":{"branch":{"show":null,"hasRendered":null,"blockedByCTA":false},"hideGoogleOneTap":false,"hasRenderedAlternateUserBanner":null,"currentLocation":"https:\u002F\u002Ftech.new-work.se\u002Funit-testing-combine-publishers-6f581e30c370","host":"tech.new-work.se","hostname":"tech.new-work.se","referrer":"","hasSetReferrer":false,"susiModal":{"step":null,"operation":"register"},"postRead":false,"partnerProgram":{"selectedCountryCode":null},"queryString":"?source=collection_home---5------3-----------------------"},"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\":\"tech.new-work.se\"})":{"__ref":"Collection:35cb8c78d3cf"},"variantFlags":[{"__typename":"VariantFlag","name":"enable_app_flirty_thirty","valueType":{"__typename":"VariantFlagBoolean","value":true}},{"__typename":"VariantFlag","name":"enable_ios_easy_resubscribe","valueType":{"__typename":"VariantFlagBoolean","value":true}},{"__typename":"VariantFlag","name":"signup_services","valueType":{"__typename":"VariantFlagString","value":"twitter,facebook,google,email,google-fastidv,google-one-tap,apple"}},{"__typename":"VariantFlag","name":"android_enable_editor_new_publishing_flow","valueType":{"__typename":"VariantFlagBoolean","value":true}},{"__typename":"VariantFlag","name":"enable_iceland_forced_android","valueType":{"__typename":"VariantFlagBoolean","value":true}},{"__typename":"VariantFlag","name":"android_enable_friend_links_postpage_banners","valueType":{"__typename":"VariantFlagBoolean","value":true}},{"__typename":"VariantFlag","name":"android_rating_prompt_stories_read_threshold","valueType":{"__typename":"VariantFlagNumber","value":2}},{"__typename":"VariantFlag","name":"enable_verifications_service","valueType":{"__typename":"VariantFlagBoolean","value":true}},{"__typename":"VariantFlag","name":"enable_android_dynamic_programming_paywall","valueType":{"__typename":"VariantFlagBoolean","value":true}},{"__typename":"VariantFlag","name":"enable_braintree_google_pay","valueType":{"__typename":"VariantFlagBoolean","value":true}},{"__typename":"VariantFlag","name":"enable_import","valueType":{"__typename":"VariantFlagBoolean","value":true}},{"__typename":"VariantFlag","name":"android_enable_lists_v2","valueType":{"__typename":"VariantFlagBoolean","value":true}},{"__typename":"VariantFlag","name":"enable_ml_rank_rex_anno","valueType":{"__typename":"VariantFlagBoolean","value":true}},{"__typename":"VariantFlag","name":"enable_recaptcha_enterprise","valueType":{"__typename":"VariantFlagBoolean","value":true}},{"__typename":"VariantFlag","name":"ios_social_share_sheet","valueType":{"__typename":"VariantFlagBoolean","value":true}},{"__typename":"VariantFlag","name":"reengagement_notification_duration","valueType":{"__typename":"VariantFlagNumber","value":3}},{"__typename":"VariantFlag","name":"enable_author_cards","valueType":{"__typename":"VariantFlagBoolean","value":true}},{"__typename":"VariantFlag","name":"available_annual_plan","valueType":{"__typename":"VariantFlagString","value":"2c754bcc2995"}},{"__typename":"VariantFlag","name":"enable_speechify_widget","valueType":{"__typename":"VariantFlagBoolean","value":true}},{"__typename":"VariantFlag","name":"enable_conversion_model_v2","valueType":{"__typename":"VariantFlagString","value":"group_2"}},{"__typename":"VariantFlag","name":"price_smoke_test_yearly","valueType":{"__typename":"VariantFlagString","value":""}},{"__typename":"VariantFlag","name":"enable_mastodon_avatar_upload","valueType":{"__typename":"VariantFlagBoolean","value":true}},{"__typename":"VariantFlag","name":"ios_enable_verified_book_author","valueType":{"__typename":"VariantFlagBoolean","value":true}},{"__typename":"VariantFlag","name":"redefined_top_posts","valueType":{"__typename":"VariantFlagBoolean","value":true}},{"__typename":"VariantFlag","name":"enable_entities_to_follow_v2","valueType":{"__typename":"VariantFlagBoolean","value":true}},{"__typename":"VariantFlag","name":"goliath_externalsearch_enable_comment_deindexation","valueType":{"__typename":"VariantFlagBoolean","value":true}},{"__typename":"VariantFlag","name":"can_receive_tips_v0","valueType":{"__typename":"VariantFlagBoolean","value":true}},{"__typename":"VariantFlag","name":"enable_automod","valueType":{"__typename":"VariantFlagBoolean","value":true}},{"__typename":"VariantFlag","name":"enable_mastodon_for_members_username_selection","valueType":{"__typename":"VariantFlagBoolean","value":true}},{"__typename":"VariantFlag","name":"enable_medium2_kbfd","valueType":{"__typename":"VariantFlagBoolean","value":true}},{"__typename":"VariantFlag","name":"enable_post_bottom_responses","valueType":{"__typename":"VariantFlagBoolean","value":true}},{"__typename":"VariantFlag","name":"enable_speechify_ios","valueType":{"__typename":"VariantFlagBoolean","value":true}},{"__typename":"VariantFlag","name":"android_enable_image_sharer","valueType":{"__typename":"VariantFlagBoolean","value":true}},{"__typename":"VariantFlag","name":"enable_braintree_client","valueType":{"__typename":"VariantFlagBoolean","value":true}},{"__typename":"VariantFlag","name":"enable_seamless_social_sharing","valueType":{"__typename":"VariantFlagBoolean","value":true}},{"__typename":"VariantFlag","name":"enable_mastodon_for_members","valueType":{"__typename":"VariantFlagBoolean","value":true}},{"__typename":"VariantFlag","name":"can_send_tips_v0","valueType":{"__typename":"VariantFlagBoolean","value":true}},{"__typename":"VariantFlag","name":"enable_boost_nia_v01","valueType":{"__typename":"VariantFlagBoolean","value":true}},{"__typename":"VariantFlag","name":"enable_pp_v4","valueType":{"__typename":"VariantFlagBoolean","value":true}},{"__typename":"VariantFlag","name":"allow_signup","valueType":{"__typename":"VariantFlagBoolean","value":true}},{"__typename":"VariantFlag","name":"enable_lo_homepage","valueType":{"__typename":"VariantFlagString","value":"control"}},{"__typename":"VariantFlag","name":"enable_bayesian_average_pub_search","valueType":{"__typename":"VariantFlagBoolean","value":true}},{"__typename":"VariantFlag","name":"enable_conversion_ranker_v2","valueType":{"__typename":"VariantFlagString","value":"control"}},{"__typename":"VariantFlag","name":"enable_ios_offline_reading","valueType":{"__typename":"VariantFlagBoolean","value":true}},{"__typename":"VariantFlag","name":"enable_lite_response_markup","valueType":{"__typename":"VariantFlagBoolean","value":true}},{"__typename":"VariantFlag","name":"android_enable_syntax_highlight","valueType":{"__typename":"VariantFlagBoolean","value":true}},{"__typename":"VariantFlag","name":"enable_apple_sign_in","valueType":{"__typename":"VariantFlagBoolean","value":true}},{"__typename":"VariantFlag","name":"ios_display_paywall_after_onboarding","valueType":{"__typename":"VariantFlagBoolean","value":true}},{"__typename":"VariantFlag","name":"enable_lite_homepage","valueType":{"__typename":"VariantFlagBoolean","value":true}},{"__typename":"VariantFlag","name":"enable_apple_webhook","valueType":{"__typename":"VariantFlagBoolean","value":true}},{"__typename":"VariantFlag","name":"enable_braintree_paypal","valueType":{"__typename":"VariantFlagBoolean","value":true}},{"__typename":"VariantFlag","name":"enable_diversification_rex","valueType":{"__typename":"VariantFlagBoolean","value":true}},{"__typename":"VariantFlag","name":"enable_pre_pp_v4","valueType":{"__typename":"VariantFlagBoolean","value":true}},{"__typename":"VariantFlag","name":"enable_recirc_model","valueType":{"__typename":"VariantFlagBoolean","value":true}},{"__typename":"VariantFlag","name":"browsable_stream_config_bucket","valueType":{"__typename":"VariantFlagString","value":"curated-topics"}},{"__typename":"VariantFlag","name":"enable_author_cards_byline","valueType":{"__typename":"VariantFlagBoolean","value":true}},{"__typename":"VariantFlag","name":"enable_marketing_emails","valueType":{"__typename":"VariantFlagBoolean","value":true}},{"__typename":"VariantFlag","name":"enable_members_only_audio","valueType":{"__typename":"VariantFlagBoolean","value":true}},{"__typename":"VariantFlag","name":"available_annual_premium_plan","valueType":{"__typename":"VariantFlagString","value":"4a442ace1476"}},{"__typename":"VariantFlag","name":"coronavirus_topic_recirc","valueType":{"__typename":"VariantFlagBoolean","value":true}},{"__typename":"VariantFlag","name":"enable_deprecate_legacy_providers_v3","valueType":{"__typename":"VariantFlagBoolean","value":true}},{"__typename":"VariantFlag","name":"price_smoke_test_monthly","valueType":{"__typename":"VariantFlagString","value":""}},{"__typename":"VariantFlag","name":"enable_track_user_interactions","valueType":{"__typename":"VariantFlagBoolean","value":true}},{"__typename":"VariantFlag","name":"enable_ranker_v10","valueType":{"__typename":"VariantFlagString","value":"control"}},{"__typename":"VariantFlag","name":"enable_see_pronouns","valueType":{"__typename":"VariantFlagBoolean","value":true}},{"__typename":"VariantFlag","name":"enable_publication_hierarchy_web","valueType":{"__typename":"VariantFlagBoolean","value":true}},{"__typename":"VariantFlag","name":"enable_moc_load_processor_first_story","valueType":{"__typename":"VariantFlagBoolean","value":true}},{"__typename":"VariantFlag","name":"enable_tribute_landing_page","valueType":{"__typename":"VariantFlagBoolean","value":true}},{"__typename":"VariantFlag","name":"textshots_userid","valueType":{"__typename":"VariantFlagString","value":""}},{"__typename":"VariantFlag","name":"mobile_custom_app_icon","valueType":{"__typename":"VariantFlagBoolean","value":true}},{"__typename":"VariantFlag","name":"android_two_hour_refresh","valueType":{"__typename":"VariantFlagBoolean","value":true}},{"__typename":"VariantFlag","name":"enable_ios_autorefresh","valueType":{"__typename":"VariantFlagBoolean","value":true}},{"__typename":"VariantFlag","name":"enable_rex_aggregator_v2","valueType":{"__typename":"VariantFlagBoolean","value":true}},{"__typename":"VariantFlag","name":"enable_lite_server_upstream_deadlines","valueType":{"__typename":"VariantFlagBoolean","value":true}},{"__typename":"VariantFlag","name":"enable_tag_recs","valueType":{"__typename":"VariantFlagBoolean","value":true}},{"__typename":"VariantFlag","name":"enable_creator_welcome_email","valueType":{"__typename":"VariantFlagBoolean","value":true}},{"__typename":"VariantFlag","name":"ios_remove_twitter_onboarding_step","valueType":{"__typename":"VariantFlagBoolean","value":true}},{"__typename":"VariantFlag","name":"enable_android_offline_reading","valueType":{"__typename":"VariantFlagBoolean","value":true}},{"__typename":"VariantFlag","name":"enable_google_one_tap","valueType":{"__typename":"VariantFlagBoolean","value":true}},{"__typename":"VariantFlag","name":"enable_simplified_digest_v2_b","valueType":{"__typename":"VariantFlagBoolean","value":true}},{"__typename":"VariantFlag","name":"enable_update_explore_wtf","valueType":{"__typename":"VariantFlagBoolean","value":true}},{"__typename":"VariantFlag","name":"ios_enable_home_post_menu","valueType":{"__typename":"VariantFlagBoolean","value":true}},{"__typename":"VariantFlag","name":"enable_new_stripe_customers","valueType":{"__typename":"VariantFlagBoolean","value":true}},{"__typename":"VariantFlag","name":"enable_rex_new_push_notification_endpoint","valueType":{"__typename":"VariantFlagBoolean","value":true}},{"__typename":"VariantFlag","name":"enable_susi_redesign_ios","valueType":{"__typename":"VariantFlagBoolean","value":true}},{"__typename":"VariantFlag","name":"enable_tick_landing_page","valueType":{"__typename":"VariantFlagBoolean","value":true}},{"__typename":"VariantFlag","name":"ios_iceland_nux","valueType":{"__typename":"VariantFlagBoolean","value":true}},{"__typename":"VariantFlag","name":"enable_ios_dynamic_paywall_programming","valueType":{"__typename":"VariantFlagBoolean","value":true}},{"__typename":"VariantFlag","name":"enable_pill_based_home_feed","valueType":{"__typename":"VariantFlagBoolean","value":true}},{"__typename":"VariantFlag","name":"allow_access","valueType":{"__typename":"VariantFlagBoolean","value":true}},{"__typename":"VariantFlag","name":"disable_partner_program_enrollment","valueType":{"__typename":"VariantFlagBoolean","value":true}},{"__typename":"VariantFlag","name":"enable_aurora_pub_follower_page","valueType":{"__typename":"VariantFlagBoolean","value":true}},{"__typename":"VariantFlag","name":"enable_maim_the_meter","valueType":{"__typename":"VariantFlagBoolean","value":true}},{"__typename":"VariantFlag","name":"android_enable_friend_links_creation","valueType":{"__typename":"VariantFlagBoolean","value":true}},{"__typename":"VariantFlag","name":"enable_abandoned_cart_promotion_email","valueType":{"__typename":"VariantFlagBoolean","value":true}},{"__typename":"VariantFlag","name":"enable_sharer_create_post_share_key","valueType":{"__typename":"VariantFlagBoolean","value":true}},{"__typename":"VariantFlag","name":"enable_sprig","valueType":{"__typename":"VariantFlagBoolean","value":true}},{"__typename":"VariantFlag","name":"enable_android_miro_v2","valueType":{"__typename":"VariantFlagBoolean","value":true}},{"__typename":"VariantFlag","name":"ios_enable_lock_responses","valueType":{"__typename":"VariantFlagBoolean","value":true}},{"__typename":"VariantFlag","name":"enable_explicit_signals","valueType":{"__typename":"VariantFlagBoolean","value":true}},{"__typename":"VariantFlag","name":"enable_moc_load_processor_all_recs_surfaces","valueType":{"__typename":"VariantFlagBoolean","value":true}},{"__typename":"VariantFlag","name":"enable_sharer_validate_post_share_key","valueType":{"__typename":"VariantFlagBoolean","value":true}},{"__typename":"VariantFlag","name":"enable_switch_plan_premium_tier","valueType":{"__typename":"VariantFlagBoolean","value":true}},{"__typename":"VariantFlag","name":"enable_group_gifting","valueType":{"__typename":"VariantFlagBoolean","value":true}},{"__typename":"VariantFlag","name":"enable_recommended_publishers_query","valueType":{"__typename":"VariantFlagBoolean","value":true}},{"__typename":"VariantFlag","name":"enable_updated_pub_recs_ui","valueType":{"__typename":"VariantFlagBoolean","value":true}},{"__typename":"VariantFlag","name":"enable_braintree_apple_pay","valueType":{"__typename":"VariantFlagBoolean","value":true}},{"__typename":"VariantFlag","name":"ios_enable_friend_links_postpage_banners","valueType":{"__typename":"VariantFlagBoolean","value":true}},{"__typename":"VariantFlag","name":"enable_footer_app_buttons","valueType":{"__typename":"VariantFlagBoolean","value":true}},{"__typename":"VariantFlag","name":"enable_rex_reading_history","valueType":{"__typename":"VariantFlagBoolean","value":true}},{"__typename":"VariantFlag","name":"enable_new_manage_membership_flow","valueType":{"__typename":"VariantFlagBoolean","value":true}},{"__typename":"VariantFlag","name":"enable_lite_archive_page","valueType":{"__typename":"VariantFlagBoolean","value":true}},{"__typename":"VariantFlag","name":"enable_premium_tier_badge","valueType":{"__typename":"VariantFlagBoolean","value":true}},{"__typename":"VariantFlag","name":"available_monthly_plan","valueType":{"__typename":"VariantFlagString","value":"60e220181034"}},{"__typename":"VariantFlag","name":"enable_braintree_trial_membership","valueType":{"__typename":"VariantFlagBoolean","value":true}},{"__typename":"VariantFlag","name":"limit_user_follows","valueType":{"__typename":"VariantFlagBoolean","value":true}},{"__typename":"VariantFlag","name":"enable_billing_frequency_on_step2","valueType":{"__typename":"VariantFlagString","value":"group_1"}},{"__typename":"VariantFlag","name":"enable_moc_load_processor_c","valueType":{"__typename":"VariantFlagBoolean","value":true}},{"__typename":"VariantFlag","name":"enable_starspace","valueType":{"__typename":"VariantFlagBoolean","value":true}},{"__typename":"VariantFlag","name":"available_monthly_premium_plan","valueType":{"__typename":"VariantFlagString","value":"12a660186432"}},{"__typename":"VariantFlag","name":"enable_braintree_integration","valueType":{"__typename":"VariantFlagBoolean","value":true}},{"__typename":"VariantFlag","name":"enable_configure_pronouns","valueType":{"__typename":"VariantFlagBoolean","value":true}},{"__typename":"VariantFlag","name":"rex_generator_max_candidates","valueType":{"__typename":"VariantFlagNumber","value":1000}},{"__typename":"VariantFlag","name":"enable_ios_dynamic_paywall_aspiriational","valueType":{"__typename":"VariantFlagBoolean","value":true}},{"__typename":"VariantFlag","name":"enable_newsletter_lo_flow_custom_domains","valueType":{"__typename":"VariantFlagBoolean","value":true}},{"__typename":"VariantFlag","name":"glyph_font_set","valueType":{"__typename":"VariantFlagString","value":"m2-unbound-source-serif-pro"}},{"__typename":"VariantFlag","name":"enable_tipping_v0_ios","valueType":{"__typename":"VariantFlagBoolean","value":true}},{"__typename":"VariantFlag","name":"enable_bg_post_post","valueType":{"__typename":"VariantFlagBoolean","value":true}},{"__typename":"VariantFlag","name":"enable_post_bottom_responses_input","valueType":{"__typename":"VariantFlagBoolean","value":true}},{"__typename":"VariantFlag","name":"enable_rito_upstream_deadlines","valueType":{"__typename":"VariantFlagBoolean","value":true}},{"__typename":"VariantFlag","name":"enable_pp_country_expansion","valueType":{"__typename":"VariantFlagBoolean","value":true}},{"__typename":"VariantFlag","name":"enable_tipping_v0_android","valueType":{"__typename":"VariantFlagBoolean","value":true}},{"__typename":"VariantFlag","name":"onboarding_tags_from_top_views","valueType":{"__typename":"VariantFlagBoolean","value":true}},{"__typename":"VariantFlag","name":"enable_android_verified_author","valueType":{"__typename":"VariantFlagBoolean","value":true}},{"__typename":"VariantFlag","name":"enable_explicit_signals_updated_post_previews","valueType":{"__typename":"VariantFlagBoolean","value":true}},{"__typename":"VariantFlag","name":"enable_legacy_feed_in_iceland","valueType":{"__typename":"VariantFlagBoolean","value":true}},{"__typename":"VariantFlag","name":"enable_update_topic_portals_wtf","valueType":{"__typename":"VariantFlagBoolean","value":true}},{"__typename":"VariantFlag","name":"allow_test_auth","valueType":{"__typename":"VariantFlagString","value":"disallow"}},{"__typename":"VariantFlag","name":"enable_android_dynamic_aspirational_paywall","valueType":{"__typename":"VariantFlagBoolean","value":true}},{"__typename":"VariantFlag","name":"enable_braintree_webhook","valueType":{"__typename":"VariantFlagBoolean","value":true}},{"__typename":"VariantFlag","name":"reader_fair_distribution_non_qp","valueType":{"__typename":"VariantFlagBoolean","value":true}},{"__typename":"VariantFlag","name":"skip_fs_cache_user_vals","valueType":{"__typename":"VariantFlagBoolean","value":true}},{"__typename":"VariantFlag","name":"enable_intrinsic_automatic_actions","valueType":{"__typename":"VariantFlagBoolean","value":true}},{"__typename":"VariantFlag","name":"ios_enable_friend_links_creation","valueType":{"__typename":"VariantFlagBoolean","value":true}},{"__typename":"VariantFlag","name":"ios_in_app_free_trial","valueType":{"__typename":"VariantFlagBoolean","value":true}},{"__typename":"VariantFlag","name":"enable_susi_redesign_android","valueType":{"__typename":"VariantFlagBoolean","value":true}},{"__typename":"VariantFlag","name":"signin_services","valueType":{"__typename":"VariantFlagString","value":"twitter,facebook,google,email,google-fastidv,google-one-tap,apple"}},{"__typename":"VariantFlag","name":"enable_auto_follow_on_subscribe","valueType":{"__typename":"VariantFlagBoolean","value":true}},{"__typename":"VariantFlag","name":"enable_eventstats_event_processing","valueType":{"__typename":"VariantFlagBoolean","value":true}},{"__typename":"VariantFlag","name":"limit_post_referrers","valueType":{"__typename":"VariantFlagBoolean","value":true}},{"__typename":"VariantFlag","name":"enable_branch_io","valueType":{"__typename":"VariantFlagBoolean","value":true}},{"__typename":"VariantFlag","name":"enable_cache_less_following_feed","valueType":{"__typename":"VariantFlagBoolean","value":true}},{"__typename":"VariantFlag","name":"num_post_bottom_responses_to_show","valueType":{"__typename":"VariantFlagString","value":"3"}},{"__typename":"VariantFlag","name":"android_enable_topic_portals","valueType":{"__typename":"VariantFlagBoolean","value":true}},{"__typename":"VariantFlag","name":"enable_lite_continue_this_thread","valueType":{"__typename":"VariantFlagBoolean","value":true}},{"__typename":"VariantFlag","name":"enable_premium_tier","valueType":{"__typename":"VariantFlagBoolean","value":true}},{"__typename":"VariantFlag","name":"enable_google_webhook","valueType":{"__typename":"VariantFlagBoolean","value":true}}],"postResult({\"id\":\"6f581e30c370\"})":{"__ref":"Post:6f581e30c370"}},"ImageMetadata:":{"__typename":"ImageMetadata","id":"","originalWidth":0,"originalHeight":0},"Collection:35cb8c78d3cf":{"__typename":"Collection","id":"35cb8c78d3cf","favicon":{"__ref":"ImageMetadata:"},"customStyleSheet":null,"colorPalette":{"__typename":"ColorPalette","highlightSpectrum":{"__typename":"ColorSpectrum","backgroundColor":"#FFFFFFFF","colorPoints":[{"__typename":"ColorPoint","color":"#FFFFFFFF","point":0},{"__typename":"ColorPoint","color":"#FFE8F3E8","point":0.1},{"__typename":"ColorPoint","color":"#FFE8F3E8","point":0.2},{"__typename":"ColorPoint","color":"#FFD1E7D1","point":0.6},{"__typename":"ColorPoint","color":"#FFA3D0A2","point":1}]},"defaultBackgroundSpectrum":{"__typename":"ColorSpectrum","backgroundColor":"#FFFFFFFF","colorPoints":[{"__typename":"ColorPoint","color":"#FF1A8917","point":0},{"__typename":"ColorPoint","color":"#FF11800E","point":0.1},{"__typename":"ColorPoint","color":"#FF0F730C","point":0.2},{"__typename":"ColorPoint","color":"#FF095407","point":1}]},"tintBackgroundSpectrum":null},"domain":"tech.new-work.se","slug":"xing-engineering","googleAnalyticsId":null,"editors":[{"__typename":"CollectionMastheadUserItem","user":{"__ref":"User:19daab9f6796"}},{"__typename":"CollectionMastheadUserItem","user":{"__ref":"User:a8e9214ea31c"}},{"__typename":"CollectionMastheadUserItem","user":{"__ref":"User:d1124cb73b2b"}},{"__typename":"CollectionMastheadUserItem","user":{"__ref":"User:cabadc1ec49e"}},{"__typename":"CollectionMastheadUserItem","user":{"__ref":"User:9650cd942799"}},{"__typename":"CollectionMastheadUserItem","user":{"__ref":"User:8928a8596e72"}}],"name":"New Work Development","avatar":{"__ref":"ImageMetadata:1*B1MD9EGz7rhVkx3WE3Rkog.png"},"description":"Stories about building a better working world","subscriberCount":1029,"latestPostsConnection({\"paging\":{\"limit\":1}})":{"__typename":"PostConnection","posts":[{"__ref":"Post:fac4a8b0ae15"}]},"viewerEdge":{"__ref":"CollectionViewerEdge:collectionId:35cb8c78d3cf-viewerId:lo_c50e0cd28248"},"twitterUsername":"xingdevs","facebookPageId":null,"logo":{"__ref":"ImageMetadata:"}},"User:19daab9f6796":{"__typename":"User","id":"19daab9f6796"},"User:a8e9214ea31c":{"__typename":"User","id":"a8e9214ea31c"},"User:d1124cb73b2b":{"__typename":"User","id":"d1124cb73b2b"},"User:cabadc1ec49e":{"__typename":"User","id":"cabadc1ec49e"},"User:9650cd942799":{"__typename":"User","id":"9650cd942799"},"User:8928a8596e72":{"__typename":"User","id":"8928a8596e72"},"ImageMetadata:1*B1MD9EGz7rhVkx3WE3Rkog.png":{"__typename":"ImageMetadata","id":"1*B1MD9EGz7rhVkx3WE3Rkog.png"},"User:6ca9d8d9fad3":{"__typename":"User","id":"6ca9d8d9fad3","customDomainState":null,"hasSubdomain":false,"username":"gabrielpedepera"},"Post:fac4a8b0ae15":{"__typename":"Post","id":"fac4a8b0ae15","firstPublishedAt":1701045261726,"creator":{"__ref":"User:6ca9d8d9fad3"},"collection":{"__ref":"Collection:35cb8c78d3cf"},"isSeries":false,"mediumUrl":"https:\u002F\u002Ftech.new-work.se\u002Fprotecting-sensitive-data-in-elixir-genservers-fac4a8b0ae15","sequence":null,"uniqueSlug":"protecting-sensitive-data-in-elixir-genservers-fac4a8b0ae15"},"LinkedAccounts:52fa09649919":{"__typename":"LinkedAccounts","mastodon":null,"id":"52fa09649919"},"UserViewerEdge:userId:52fa09649919-viewerId:lo_c50e0cd28248":{"__typename":"UserViewerEdge","id":"userId:52fa09649919-viewerId:lo_c50e0cd28248","isFollowing":false,"isUser":false,"isMuting":false},"NewsletterV3:4ba218698e51":{"__typename":"NewsletterV3","id":"4ba218698e51","type":"NEWSLETTER_TYPE_AUTHOR","slug":"52fa09649919","name":"52fa09649919","collection":null,"user":{"__ref":"User:52fa09649919"}},"User:52fa09649919":{"__typename":"User","id":"52fa09649919","name":"Joan Duat","username":"joanduat","newsletterV3":{"__ref":"NewsletterV3:4ba218698e51"},"linkedAccounts":{"__ref":"LinkedAccounts:52fa09649919"},"isSuspended":false,"imageId":"1*ty0A4uaPE0lAW18VI8XNWg.jpeg","mediumMemberAt":0,"verifications":{"__typename":"VerifiedInfo","isBookAuthor":false},"socialStats":{"__typename":"SocialStats","followerCount":7,"followingCount":10,"collectionFollowingCount":3},"customDomainState":null,"hasSubdomain":false,"bio":"iOS engineer living in Barcelona. Software player and music coder.","isPartnerProgramEnrolled":false,"viewerEdge":{"__ref":"UserViewerEdge:userId:52fa09649919-viewerId:lo_c50e0cd28248"},"viewerIsUser":false,"postSubscribeMembershipUpsellShownAt":0,"membership":null,"allowNotes":true,"twitterScreenName":""},"Paragraph:3621ec5eeb32_0":{"__typename":"Paragraph","id":"3621ec5eeb32_0","name":"b4a2","type":"H3","href":null,"layout":null,"metadata":null,"text":"Unit Testing Combine Publishers","hasDropCap":null,"dropCapImage":null,"markups":[],"codeBlockMetadata":null,"iframe":null,"mixtapeMetadata":null},"Paragraph:3621ec5eeb32_1":{"__typename":"Paragraph","id":"3621ec5eeb32_1","name":"8834","type":"P","href":null,"layout":null,"metadata":null,"text":"When testing Combine-related code we must keep in mind that we’re most probably testing asynchronous code. And how do we expect to test asynchronous code in Xcode? Right, with expectations.","hasDropCap":null,"dropCapImage":null,"markups":[{"__typename":"Markup","type":"EM","start":121,"end":127,"href":null,"anchorType":null,"userId":null,"linkMetadata":null}],"codeBlockMetadata":null,"iframe":null,"mixtapeMetadata":null},"Paragraph:3621ec5eeb32_2":{"__typename":"Paragraph","id":"3621ec5eeb32_2","name":"87ba","type":"P","href":null,"layout":null,"metadata":null,"text":"Expectations in Xcode allow us to define specific conditions that must be met before a test case can be considered successful or failed. By using expectations, we can accurately validate the behaviour of Combine publishers and other asynchronous code.","hasDropCap":null,"dropCapImage":null,"markups":[],"codeBlockMetadata":null,"iframe":null,"mixtapeMetadata":null},"Paragraph:3621ec5eeb32_3":{"__typename":"Paragraph","id":"3621ec5eeb32_3","name":"2cda","type":"P","href":null,"layout":null,"metadata":null,"text":"In this article, we will analyse the various kinds of expectations available in Xcode and the code required to set them up. Finally, we’ll propose a new method that not only reduces the boilerplate code but also improves the readability of our tests.","hasDropCap":null,"dropCapImage":null,"markups":[],"codeBlockMetadata":null,"iframe":null,"mixtapeMetadata":null},"ImageMetadata:1*8rVBw8xq4W8WvGjW--BNeA.jpeg":{"__typename":"ImageMetadata","id":"1*8rVBw8xq4W8WvGjW--BNeA.jpeg","originalHeight":349,"originalWidth":640,"focusPercentX":null,"focusPercentY":null,"alt":null},"Paragraph:3621ec5eeb32_4":{"__typename":"Paragraph","id":"3621ec5eeb32_4","name":"7ff4","type":"IMG","href":null,"layout":"INSET_CENTER","metadata":{"__ref":"ImageMetadata:1*8rVBw8xq4W8WvGjW--BNeA.jpeg"},"text":"Photo by Alexandru Yicol on Unsplash","hasDropCap":null,"dropCapImage":null,"markups":[{"__typename":"Markup","type":"A","start":9,"end":24,"href":"https:\u002F\u002Funsplash.com\u002F@alex_vicol?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText","anchorType":"LINK","userId":null,"linkMetadata":null},{"__typename":"Markup","type":"A","start":28,"end":36,"href":"https:\u002F\u002Funsplash.com\u002Fphotos\u002FD9FQYwAclwQ?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText","anchorType":"LINK","userId":null,"linkMetadata":null}],"codeBlockMetadata":null,"iframe":null,"mixtapeMetadata":null},"Paragraph:3621ec5eeb32_5":{"__typename":"Paragraph","id":"3621ec5eeb32_5","name":"6b7a","type":"P","href":null,"layout":null,"metadata":null,"text":"Let’s start by looking at what kind of expectations are available in Xcode and which ones are best suited for testing Publishers:","hasDropCap":null,"dropCapImage":null,"markups":[],"codeBlockMetadata":null,"iframe":null,"mixtapeMetadata":null},"Paragraph:3621ec5eeb32_6":{"__typename":"Paragraph","id":"3621ec5eeb32_6","name":"6343","type":"H4","href":null,"layout":null,"metadata":null,"text":"XCTestExpectation","hasDropCap":null,"dropCapImage":null,"markups":[{"__typename":"Markup","type":"A","start":0,"end":17,"href":"https:\u002F\u002Fdeveloper.apple.com\u002Fdocumentation\u002Fxctest\u002Fxctestexpectation","anchorType":"LINK","userId":null,"linkMetadata":null},{"__typename":"Markup","type":"STRONG","start":0,"end":17,"href":null,"anchorType":null,"userId":null,"linkMetadata":null}],"codeBlockMetadata":null,"iframe":null,"mixtapeMetadata":null},"Paragraph:3621ec5eeb32_7":{"__typename":"Paragraph","id":"3621ec5eeb32_7","name":"c6d2","type":"P","href":null,"layout":null,"metadata":null,"text":"This is the basic type of expectation since it allows us to manually control its outcome by calling fulfill(). It can be used to wait until a publisher emits some specific value and we can easily apply this to any @Published property wrapper for that matter:","hasDropCap":null,"dropCapImage":null,"markups":[{"__typename":"Markup","type":"CODE","start":100,"end":109,"href":null,"anchorType":null,"userId":null,"linkMetadata":null},{"__typename":"Markup","type":"CODE","start":214,"end":224,"href":null,"anchorType":null,"userId":null,"linkMetadata":null}],"codeBlockMetadata":null,"iframe":null,"mixtapeMetadata":null},"Paragraph:3621ec5eeb32_8":{"__typename":"Paragraph","id":"3621ec5eeb32_8","name":"2cbb","type":"PRE","href":null,"layout":null,"metadata":null,"text":"let expectation = XCTestExpectation(description: \"Wait for the publisher to emit the expected value\")\nviewModel.$keywords.sink { _ in\n} receiveValue: { value in\n if value.contains(\"Cool\") {\n expectation.fulfill()\n }\n}\n.store(in: &cancellables)\n\nwait(for: [expectation], timeout: 1)","hasDropCap":null,"dropCapImage":null,"markups":[],"codeBlockMetadata":{"__typename":"CodeBlockMetadata","mode":"EXPLICIT","lang":"swift"},"iframe":null,"mixtapeMetadata":null},"Paragraph:3621ec5eeb32_9":{"__typename":"Paragraph","id":"3621ec5eeb32_9","name":"394d","type":"H4","href":null,"layout":null,"metadata":null,"text":"XCTKVOExpectation","hasDropCap":null,"dropCapImage":null,"markups":[{"__typename":"Markup","type":"A","start":0,"end":17,"href":"https:\u002F\u002Fdeveloper.apple.com\u002Fdocumentation\u002Fxctest\u002Fxctkvoexpectation","anchorType":"LINK","userId":null,"linkMetadata":null},{"__typename":"Markup","type":"STRONG","start":0,"end":17,"href":null,"anchorType":null,"userId":null,"linkMetadata":null}],"codeBlockMetadata":null,"iframe":null,"mixtapeMetadata":null},"Paragraph:3621ec5eeb32_10":{"__typename":"Paragraph","id":"3621ec5eeb32_10","name":"082e","type":"P","href":null,"layout":null,"metadata":null,"text":"This is an expectation that fulfils on a specific key-value observing (KVO) condition. But this one is deprecated and Apple promotes using the next one:","hasDropCap":null,"dropCapImage":null,"markups":[{"__typename":"Markup","type":"A","start":50,"end":69,"href":"https:\u002F\u002Fdeveloper.apple.com\u002Fdocumentation\u002Fswift\u002Fusing-key-value-observing-in-swift","anchorType":"LINK","userId":null,"linkMetadata":null}],"codeBlockMetadata":null,"iframe":null,"mixtapeMetadata":null},"Paragraph:3621ec5eeb32_11":{"__typename":"Paragraph","id":"3621ec5eeb32_11","name":"41c9","type":"H4","href":null,"layout":null,"metadata":null,"text":"XCTKeyPathExpectation","hasDropCap":null,"dropCapImage":null,"markups":[{"__typename":"Markup","type":"A","start":0,"end":21,"href":"https:\u002F\u002Fdeveloper.apple.com\u002Fdocumentation\u002Fxctest\u002Fxctkeypathexpectation","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:3621ec5eeb32_12":{"__typename":"Paragraph","id":"3621ec5eeb32_12","name":"1fe9","type":"P","href":null,"layout":null,"metadata":null,"text":"This expectation allows waiting for changes to a property specified by key path for a given object. It can be used like this:","hasDropCap":null,"dropCapImage":null,"markups":[],"codeBlockMetadata":null,"iframe":null,"mixtapeMetadata":null},"Paragraph:3621ec5eeb32_13":{"__typename":"Paragraph","id":"3621ec5eeb32_13","name":"a8f8","type":"PRE","href":null,"layout":null,"metadata":null,"text":"let loadedExpectation = XCTKeyPathExpectation(keyPath: \\ViewModel.isLoaded, observedObject: viewModel, expectedValue: true)","hasDropCap":null,"dropCapImage":null,"markups":[],"codeBlockMetadata":{"__typename":"CodeBlockMetadata","mode":"EXPLICIT","lang":"swift"},"iframe":null,"mixtapeMetadata":null},"Paragraph:3621ec5eeb32_14":{"__typename":"Paragraph","id":"3621ec5eeb32_14","name":"05de","type":"P","href":null,"layout":null,"metadata":null,"text":"While this cannot be applied directly to publishers we may think it can be used at least for testing @Published properties but it has some constraints:","hasDropCap":null,"dropCapImage":null,"markups":[{"__typename":"Markup","type":"CODE","start":101,"end":111,"href":null,"anchorType":null,"userId":null,"linkMetadata":null}],"codeBlockMetadata":null,"iframe":null,"mixtapeMetadata":null},"Paragraph:3621ec5eeb32_15":{"__typename":"Paragraph","id":"3621ec5eeb32_15","name":"60c0","type":"ULI","href":null,"layout":null,"metadata":null,"text":"The tested object must inherit from NSObject","hasDropCap":null,"dropCapImage":null,"markups":[{"__typename":"Markup","type":"CODE","start":36,"end":44,"href":null,"anchorType":null,"userId":null,"linkMetadata":null}],"codeBlockMetadata":null,"iframe":null,"mixtapeMetadata":null},"Paragraph:3621ec5eeb32_16":{"__typename":"Paragraph","id":"3621ec5eeb32_16","name":"eaed","type":"ULI","href":null,"layout":null,"metadata":null,"text":"The object properties we want to observe must be declared as @objc dynamic","hasDropCap":null,"dropCapImage":null,"markups":[{"__typename":"Markup","type":"CODE","start":61,"end":74,"href":null,"anchorType":null,"userId":null,"linkMetadata":null}],"codeBlockMetadata":null,"iframe":null,"mixtapeMetadata":null},"Paragraph:3621ec5eeb32_17":{"__typename":"Paragraph","id":"3621ec5eeb32_17","name":"89bc","type":"ULI","href":null,"layout":null,"metadata":null,"text":"The property types that we want to observe must also be representable in Objective-C.","hasDropCap":null,"dropCapImage":null,"markups":[],"codeBlockMetadata":null,"iframe":null,"mixtapeMetadata":null},"Paragraph:3621ec5eeb32_18":{"__typename":"Paragraph","id":"3621ec5eeb32_18","name":"b350","type":"P","href":null,"layout":null,"metadata":null,"text":"Even if we adapt our code with all those requirements we would still be able to test only a specific value, not the stream of values from the publisher or their completion.","hasDropCap":null,"dropCapImage":null,"markups":[],"codeBlockMetadata":null,"iframe":null,"mixtapeMetadata":null},"Paragraph:3621ec5eeb32_19":{"__typename":"Paragraph","id":"3621ec5eeb32_19","name":"ceeb","type":"P","href":null,"layout":null,"metadata":null,"text":"This is far from ideal so let’s look at the next one:","hasDropCap":null,"dropCapImage":null,"markups":[],"codeBlockMetadata":null,"iframe":null,"mixtapeMetadata":null},"Paragraph:3621ec5eeb32_20":{"__typename":"Paragraph","id":"3621ec5eeb32_20","name":"07b3","type":"H4","href":null,"layout":null,"metadata":null,"text":"XCTNSPredicateExpectation","hasDropCap":null,"dropCapImage":null,"markups":[{"__typename":"Markup","type":"A","start":0,"end":25,"href":"https:\u002F\u002Fdeveloper.apple.com\u002Fdocumentation\u002Fxctest\u002Fxctnspredicateexpectation","anchorType":"LINK","userId":null,"linkMetadata":null},{"__typename":"Markup","type":"STRONG","start":0,"end":25,"href":null,"anchorType":null,"userId":null,"linkMetadata":null}],"codeBlockMetadata":null,"iframe":null,"mixtapeMetadata":null},"Paragraph:3621ec5eeb32_21":{"__typename":"Paragraph","id":"3621ec5eeb32_21","name":"4472","type":"P","href":null,"layout":null,"metadata":null,"text":"This is an expectation that is fulfilled when an NSPredicate is satisfied. It can be used as:","hasDropCap":null,"dropCapImage":null,"markups":[{"__typename":"Markup","type":"CODE","start":49,"end":60,"href":null,"anchorType":null,"userId":null,"linkMetadata":null}],"codeBlockMetadata":null,"iframe":null,"mixtapeMetadata":null},"Paragraph:3621ec5eeb32_22":{"__typename":"Paragraph","id":"3621ec5eeb32_22","name":"c639","type":"PRE","href":null,"layout":null,"metadata":null,"text":"let predicateExpectation = XCTNSPredicateExpectation(predicate: NSPredicate { _,_ in\n viewModel.isLoaded\n}, object: .none)","hasDropCap":null,"dropCapImage":null,"markups":[],"codeBlockMetadata":{"__typename":"CodeBlockMetadata","mode":"EXPLICIT","lang":"swift"},"iframe":null,"mixtapeMetadata":null},"Paragraph:3621ec5eeb32_23":{"__typename":"Paragraph","id":"3621ec5eeb32_23","name":"cd73","type":"P","href":null,"layout":null,"metadata":null,"text":"But when using this you probably found out that, even if the tested code is fast, the expectation must be awaited for at least more than a second:","hasDropCap":null,"dropCapImage":null,"markups":[],"codeBlockMetadata":null,"iframe":null,"mixtapeMetadata":null},"Paragraph:3621ec5eeb32_24":{"__typename":"Paragraph","id":"3621ec5eeb32_24","name":"bbf4","type":"PRE","href":null,"layout":null,"metadata":null,"text":"wait(for: [predicateExpectation], timeout: 1) \u002F\u002F this fails\nwait(for: [predicateExpectation], timeout: 1.5) \u002F\u002F this succeeds","hasDropCap":null,"dropCapImage":null,"markups":[],"codeBlockMetadata":{"__typename":"CodeBlockMetadata","mode":"EXPLICIT","lang":"swift"},"iframe":null,"mixtapeMetadata":null},"Paragraph:3621ec5eeb32_25":{"__typename":"Paragraph","id":"3621ec5eeb32_25","name":"c2a9","type":"P","href":null,"layout":null,"metadata":null,"text":"It turns out that XCTNSPredicateExpectation is slow because it uses some kind of polling mechanism to check the predicate periodically thus makes it best suited for UI tests. So it’s better to avoid it in unit tests if we want them to run as fast as possible.","hasDropCap":null,"dropCapImage":null,"markups":[{"__typename":"Markup","type":"CODE","start":18,"end":43,"href":null,"anchorType":null,"userId":null,"linkMetadata":null}],"codeBlockMetadata":null,"iframe":null,"mixtapeMetadata":null},"Paragraph:3621ec5eeb32_26":{"__typename":"Paragraph","id":"3621ec5eeb32_26","name":"29cc","type":"H4","href":null,"layout":null,"metadata":null,"text":"XCTNSNotificationExpectation","hasDropCap":null,"dropCapImage":null,"markups":[{"__typename":"Markup","type":"A","start":0,"end":28,"href":"https:\u002F\u002Fdeveloper.apple.com\u002Fdocumentation\u002Fxctest\u002Fxctnsnotificationexpectation","anchorType":"LINK","userId":null,"linkMetadata":null},{"__typename":"Markup","type":"STRONG","start":0,"end":28,"href":null,"anchorType":null,"userId":null,"linkMetadata":null}],"codeBlockMetadata":null,"iframe":null,"mixtapeMetadata":null},"Paragraph:3621ec5eeb32_27":{"__typename":"Paragraph","id":"3621ec5eeb32_27","name":"1074","type":"P","href":null,"layout":null,"metadata":null,"text":"Finally, there’s this expectation that is fulfilled when an expected NSNotification is received. It can be useful in some scenarios but again, not so much for testing Combine-based code.","hasDropCap":null,"dropCapImage":null,"markups":[{"__typename":"Markup","type":"CODE","start":69,"end":83,"href":null,"anchorType":null,"userId":null,"linkMetadata":null}],"codeBlockMetadata":null,"iframe":null,"mixtapeMetadata":null},"Paragraph:3621ec5eeb32_28":{"__typename":"Paragraph","id":"3621ec5eeb32_28","name":"0d3a","type":"H3","href":null,"layout":null,"metadata":null,"text":"Level-set the expectations","hasDropCap":null,"dropCapImage":null,"markups":[],"codeBlockMetadata":null,"iframe":null,"mixtapeMetadata":null},"Paragraph:3621ec5eeb32_29":{"__typename":"Paragraph","id":"3621ec5eeb32_29","name":"3f28","type":"P","href":null,"layout":null,"metadata":null,"text":"After looking at the options above it’s clear that we’re just left with the XCTestExpectation as the only choice for testing Publishers.","hasDropCap":null,"dropCapImage":null,"markups":[{"__typename":"Markup","type":"CODE","start":76,"end":93,"href":null,"anchorType":null,"userId":null,"linkMetadata":null}],"codeBlockMetadata":null,"iframe":null,"mixtapeMetadata":null},"Paragraph:3621ec5eeb32_30":{"__typename":"Paragraph","id":"3621ec5eeb32_30","name":"9280","type":"P","href":null,"layout":null,"metadata":null,"text":"But writing tests this way always involves some boilerplate code, starting from the the expectation creation, subscribing to the publisher, manually fulfilling the expectation and finally storing the cancellable.","hasDropCap":null,"dropCapImage":null,"markups":[],"codeBlockMetadata":null,"iframe":null,"mixtapeMetadata":null},"Paragraph:3621ec5eeb32_31":{"__typename":"Paragraph","id":"3621ec5eeb32_31","name":"b427","type":"P","href":null,"layout":null,"metadata":null,"text":"If we care about test readability in such a way they are easy to understand then let’s try to simplify this process by introducing our own custom expectation:","hasDropCap":null,"dropCapImage":null,"markups":[],"codeBlockMetadata":null,"iframe":null,"mixtapeMetadata":null},"Paragraph:3621ec5eeb32_32":{"__typename":"Paragraph","id":"3621ec5eeb32_32","name":"a04d","type":"H3","href":null,"layout":null,"metadata":null,"text":"PublisherValueExpectation","hasDropCap":null,"dropCapImage":null,"markups":[],"codeBlockMetadata":null,"iframe":null,"mixtapeMetadata":null},"Paragraph:3621ec5eeb32_33":{"__typename":"Paragraph","id":"3621ec5eeb32_33","name":"73d7","type":"P","href":null,"layout":null,"metadata":null,"text":"We can create an XCTestExpectation subclass that does this repetitive task for us. Here, the PublisherValueExpectation is an expectation that will be fulfilled automatically when the publisher emits a value that matches a given condition.","hasDropCap":null,"dropCapImage":null,"markups":[{"__typename":"Markup","type":"CODE","start":17,"end":34,"href":null,"anchorType":null,"userId":null,"linkMetadata":null},{"__typename":"Markup","type":"CODE","start":93,"end":118,"href":null,"anchorType":null,"userId":null,"linkMetadata":null}],"codeBlockMetadata":null,"iframe":null,"mixtapeMetadata":null},"Paragraph:3621ec5eeb32_34":{"__typename":"Paragraph","id":"3621ec5eeb32_34","name":"f659","type":"PRE","href":null,"layout":null,"metadata":null,"text":"public final class PublisherValueExpectation\u003CP: Publisher\u003E: XCTestExpectation {\n private var cancellable: AnyCancellable?\n\n public init(\n _ publisher: P,\n condition: @escaping (P.Output) -\u003E Bool)\n {\n super.init(description: \"Publisher expected to emit a value that matches the condition.\")\n cancellable = publisher.sink { _ in\n } receiveValue: { [weak self] value in\n if condition(value) {\n self?.fulfill()\n }\n }\n }","hasDropCap":null,"dropCapImage":null,"markups":[],"codeBlockMetadata":{"__typename":"CodeBlockMetadata","mode":"EXPLICIT","lang":"swift"},"iframe":null,"mixtapeMetadata":null},"Paragraph:3621ec5eeb32_35":{"__typename":"Paragraph","id":"3621ec5eeb32_35","name":"d885","type":"P","href":null,"layout":null,"metadata":null,"text":"With this we can just write:","hasDropCap":null,"dropCapImage":null,"markups":[],"codeBlockMetadata":null,"iframe":null,"mixtapeMetadata":null},"Paragraph:3621ec5eeb32_36":{"__typename":"Paragraph","id":"3621ec5eeb32_36","name":"e039","type":"PRE","href":null,"layout":null,"metadata":null,"text":"let publisherExpectation = PublisherValueExpectation(viewModel.$keywords) { $0.contains(\"Cool\") }","hasDropCap":null,"dropCapImage":null,"markups":[],"codeBlockMetadata":{"__typename":"CodeBlockMetadata","mode":"EXPLICIT","lang":"swift"},"iframe":null,"mixtapeMetadata":null},"Paragraph:3621ec5eeb32_37":{"__typename":"Paragraph","id":"3621ec5eeb32_37","name":"8513","type":"P","href":null,"layout":null,"metadata":null,"text":"We can also add a convenience initializer to simply pass an expected value if that conforms to Equatable:","hasDropCap":null,"dropCapImage":null,"markups":[{"__typename":"Markup","type":"CODE","start":95,"end":104,"href":null,"anchorType":null,"userId":null,"linkMetadata":null}],"codeBlockMetadata":null,"iframe":null,"mixtapeMetadata":null},"Paragraph:3621ec5eeb32_38":{"__typename":"Paragraph","id":"3621ec5eeb32_38","name":"3003","type":"PRE","href":null,"layout":null,"metadata":null,"text":"public convenience init(\n _ publisher: P,\n expectedValue: P.Output\n) where P.Output: Equatable\n{\n let description = \"Publisher expected to emit the value '\\(expectedValue)'\"\n self.init(publisher, condition: { $0 == expectedValue }, description: description)\n}","hasDropCap":null,"dropCapImage":null,"markups":[],"codeBlockMetadata":{"__typename":"CodeBlockMetadata","mode":"EXPLICIT","lang":"swift"},"iframe":null,"mixtapeMetadata":null},"Paragraph:3621ec5eeb32_39":{"__typename":"Paragraph","id":"3621ec5eeb32_39","name":"0283","type":"P","href":null,"layout":null,"metadata":null,"text":"And this allows us writing an even more compact expectation that reads nicely:","hasDropCap":null,"dropCapImage":null,"markups":[],"codeBlockMetadata":null,"iframe":null,"mixtapeMetadata":null},"Paragraph:3621ec5eeb32_40":{"__typename":"Paragraph","id":"3621ec5eeb32_40","name":"afa8","type":"PRE","href":null,"layout":null,"metadata":null,"text":"let publisherExpectation = PublisherValueExpectation(viewModel.$isLoaded, expectedValue: true)","hasDropCap":null,"dropCapImage":null,"markups":[],"codeBlockMetadata":{"__typename":"CodeBlockMetadata","mode":"EXPLICIT","lang":"swift"},"iframe":null,"mixtapeMetadata":null},"Paragraph:3621ec5eeb32_41":{"__typename":"Paragraph","id":"3621ec5eeb32_41","name":"8449","type":"P","href":null,"layout":null,"metadata":null,"text":"Thanks to Combine we can adapt the tested publisher to check many things. For instance:","hasDropCap":null,"dropCapImage":null,"markups":[],"codeBlockMetadata":null,"iframe":null,"mixtapeMetadata":null},"Paragraph:3621ec5eeb32_42":{"__typename":"Paragraph","id":"3621ec5eeb32_42","name":"d34a","type":"ULI","href":null,"layout":null,"metadata":null,"text":"Expect many values to be emitted by the publisher","hasDropCap":null,"dropCapImage":null,"markups":[],"codeBlockMetadata":null,"iframe":null,"mixtapeMetadata":null},"Paragraph:3621ec5eeb32_43":{"__typename":"Paragraph","id":"3621ec5eeb32_43","name":"1abf","type":"PRE","href":null,"layout":null,"metadata":null,"text":"let publisherExpectation = PublisherValueExpectation(publisher.collect(3), expectedValue: [1,2,3])","hasDropCap":null,"dropCapImage":null,"markups":[],"codeBlockMetadata":{"__typename":"CodeBlockMetadata","mode":"EXPLICIT","lang":"swift"},"iframe":null,"mixtapeMetadata":null},"Paragraph:3621ec5eeb32_44":{"__typename":"Paragraph","id":"3621ec5eeb32_44","name":"b77e","type":"ULI","href":null,"layout":null,"metadata":null,"text":"Expect the first or last value being emitted by the publisher","hasDropCap":null,"dropCapImage":null,"markups":[],"codeBlockMetadata":null,"iframe":null,"mixtapeMetadata":null},"Paragraph:3621ec5eeb32_45":{"__typename":"Paragraph","id":"3621ec5eeb32_45","name":"1d49","type":"PRE","href":null,"layout":null,"metadata":null,"text":"let publisherExpectation = PublisherValueExpectation(publisher.first(), expectedValue: 1)\nlet publisherExpectation = PublisherValueExpectation(publisher.last(), expectedValue: 5)","hasDropCap":null,"dropCapImage":null,"markups":[],"codeBlockMetadata":{"__typename":"CodeBlockMetadata","mode":"EXPLICIT","lang":"swift"},"iframe":null,"mixtapeMetadata":null},"Paragraph:3621ec5eeb32_46":{"__typename":"Paragraph","id":"3621ec5eeb32_46","name":"7689","type":"H3","href":null,"layout":null,"metadata":null,"text":"Keep up with the expectations","hasDropCap":null,"dropCapImage":null,"markups":[],"codeBlockMetadata":null,"iframe":null,"mixtapeMetadata":null},"Paragraph:3621ec5eeb32_47":{"__typename":"Paragraph","id":"3621ec5eeb32_47","name":"956c","type":"P","href":null,"layout":null,"metadata":null,"text":"The full project can be found in GitHub. It also includes two more useful expectations:","hasDropCap":null,"dropCapImage":null,"markups":[{"__typename":"Markup","type":"A","start":33,"end":39,"href":"https:\u002F\u002Fgithub.com\u002Fyannxou\u002FPublisherExpectations","anchorType":"LINK","userId":null,"linkMetadata":null}],"codeBlockMetadata":null,"iframe":null,"mixtapeMetadata":null},"Paragraph:3621ec5eeb32_48":{"__typename":"Paragraph","id":"3621ec5eeb32_48","name":"60f8","type":"ULI","href":null,"layout":null,"metadata":null,"text":"PublisherFinishedExpectation: Wait for a publisher to complete successfully (optionally after emitting a certain value or condition)","hasDropCap":null,"dropCapImage":null,"markups":[{"__typename":"Markup","type":"STRONG","start":0,"end":28,"href":null,"anchorType":null,"userId":null,"linkMetadata":null}],"codeBlockMetadata":null,"iframe":null,"mixtapeMetadata":null},"Paragraph:3621ec5eeb32_49":{"__typename":"Paragraph","id":"3621ec5eeb32_49","name":"900c","type":"PRE","href":null,"layout":null,"metadata":null,"text":"let publisherExpectation = PublisherFinishedExpectation(publisher, expectedValue: 2)","hasDropCap":null,"dropCapImage":null,"markups":[],"codeBlockMetadata":{"__typename":"CodeBlockMetadata","mode":"EXPLICIT","lang":"swift"},"iframe":null,"mixtapeMetadata":null},"Paragraph:3621ec5eeb32_50":{"__typename":"Paragraph","id":"3621ec5eeb32_50","name":"7db9","type":"ULI","href":null,"layout":null,"metadata":null,"text":"PublisherFailureExpectation: Wait for a publisher to complete with a failure (optionally with an expected error)","hasDropCap":null,"dropCapImage":null,"markups":[{"__typename":"Markup","type":"STRONG","start":0,"end":27,"href":null,"anchorType":null,"userId":null,"linkMetadata":null}],"codeBlockMetadata":null,"iframe":null,"mixtapeMetadata":null},"Paragraph:3621ec5eeb32_51":{"__typename":"Paragraph","id":"3621ec5eeb32_51","name":"c348","type":"PRE","href":null,"layout":null,"metadata":null,"text":"let publisherExpectation = PublisherFailureExpectation(publisher, expectedError: ApiError(code: 100))","hasDropCap":null,"dropCapImage":null,"markups":[],"codeBlockMetadata":{"__typename":"CodeBlockMetadata","mode":"EXPLICIT","lang":"swift"},"iframe":null,"mixtapeMetadata":null},"Paragraph:3621ec5eeb32_52":{"__typename":"Paragraph","id":"3621ec5eeb32_52","name":"7a42","type":"H3","href":null,"layout":null,"metadata":null,"text":"Conclusion","hasDropCap":null,"dropCapImage":null,"markups":[],"codeBlockMetadata":null,"iframe":null,"mixtapeMetadata":null},"Paragraph:3621ec5eeb32_53":{"__typename":"Paragraph","id":"3621ec5eeb32_53","name":"3dbf","type":"P","href":null,"layout":null,"metadata":null,"text":"There are many alternatives to do assertions on publishers but this is a way that is familiar to anyone that already uses test expectations in Xcode and can be easily adopted to existing tests.","hasDropCap":null,"dropCapImage":null,"markups":[],"codeBlockMetadata":null,"iframe":null,"mixtapeMetadata":null},"Paragraph:3621ec5eeb32_54":{"__typename":"Paragraph","id":"3621ec5eeb32_54","name":"a51b","type":"P","href":null,"layout":null,"metadata":null,"text":"Thanks for reading.","hasDropCap":null,"dropCapImage":null,"markups":[],"codeBlockMetadata":null,"iframe":null,"mixtapeMetadata":null},"CollectionViewerEdge:collectionId:35cb8c78d3cf-viewerId:lo_c50e0cd28248":{"__typename":"CollectionViewerEdge","id":"collectionId:35cb8c78d3cf-viewerId:lo_c50e0cd28248","isEditor":false,"isMuting":false},"PostViewerEdge:postId:6f581e30c370-viewerId:lo_c50e0cd28248":{"__typename":"PostViewerEdge","shouldIndexPostForExternalSearch":true,"id":"postId:6f581e30c370-viewerId:lo_c50e0cd28248"},"Tag:swift":{"__typename":"Tag","id":"swift","displayTitle":"Swift","normalizedTagSlug":"swift"},"Tag:combine":{"__typename":"Tag","id":"combine","displayTitle":"Combine","normalizedTagSlug":"combine"},"Tag:unit-testing":{"__typename":"Tag","id":"unit-testing","displayTitle":"Unit Testing","normalizedTagSlug":"unit-testing"},"Tag:xcode":{"__typename":"Tag","id":"xcode","displayTitle":"Xcode","normalizedTagSlug":"xcode"},"Tag:expectations":{"__typename":"Tag","id":"expectations","displayTitle":"Expectations","normalizedTagSlug":"expectations"},"Post:6f581e30c370":{"__typename":"Post","id":"6f581e30c370","collection":{"__ref":"Collection:35cb8c78d3cf"},"content({\"postMeteringOptions\":{}})":{"__typename":"PostContent","isLockedPreviewOnly":false,"bodyModel":{"__typename":"RichText","sections":[{"__typename":"Section","name":"b1e6","startIndex":0,"textLayout":null,"imageLayout":null,"backgroundImage":null,"videoLayout":null,"backgroundVideo":null}],"paragraphs":[{"__ref":"Paragraph:3621ec5eeb32_0"},{"__ref":"Paragraph:3621ec5eeb32_1"},{"__ref":"Paragraph:3621ec5eeb32_2"},{"__ref":"Paragraph:3621ec5eeb32_3"},{"__ref":"Paragraph:3621ec5eeb32_4"},{"__ref":"Paragraph:3621ec5eeb32_5"},{"__ref":"Paragraph:3621ec5eeb32_6"},{"__ref":"Paragraph:3621ec5eeb32_7"},{"__ref":"Paragraph:3621ec5eeb32_8"},{"__ref":"Paragraph:3621ec5eeb32_9"},{"__ref":"Paragraph:3621ec5eeb32_10"},{"__ref":"Paragraph:3621ec5eeb32_11"},{"__ref":"Paragraph:3621ec5eeb32_12"},{"__ref":"Paragraph:3621ec5eeb32_13"},{"__ref":"Paragraph:3621ec5eeb32_14"},{"__ref":"Paragraph:3621ec5eeb32_15"},{"__ref":"Paragraph:3621ec5eeb32_16"},{"__ref":"Paragraph:3621ec5eeb32_17"},{"__ref":"Paragraph:3621ec5eeb32_18"},{"__ref":"Paragraph:3621ec5eeb32_19"},{"__ref":"Paragraph:3621ec5eeb32_20"},{"__ref":"Paragraph:3621ec5eeb32_21"},{"__ref":"Paragraph:3621ec5eeb32_22"},{"__ref":"Paragraph:3621ec5eeb32_23"},{"__ref":"Paragraph:3621ec5eeb32_24"},{"__ref":"Paragraph:3621ec5eeb32_25"},{"__ref":"Paragraph:3621ec5eeb32_26"},{"__ref":"Paragraph:3621ec5eeb32_27"},{"__ref":"Paragraph:3621ec5eeb32_28"},{"__ref":"Paragraph:3621ec5eeb32_29"},{"__ref":"Paragraph:3621ec5eeb32_30"},{"__ref":"Paragraph:3621ec5eeb32_31"},{"__ref":"Paragraph:3621ec5eeb32_32"},{"__ref":"Paragraph:3621ec5eeb32_33"},{"__ref":"Paragraph:3621ec5eeb32_34"},{"__ref":"Paragraph:3621ec5eeb32_35"},{"__ref":"Paragraph:3621ec5eeb32_36"},{"__ref":"Paragraph:3621ec5eeb32_37"},{"__ref":"Paragraph:3621ec5eeb32_38"},{"__ref":"Paragraph:3621ec5eeb32_39"},{"__ref":"Paragraph:3621ec5eeb32_40"},{"__ref":"Paragraph:3621ec5eeb32_41"},{"__ref":"Paragraph:3621ec5eeb32_42"},{"__ref":"Paragraph:3621ec5eeb32_43"},{"__ref":"Paragraph:3621ec5eeb32_44"},{"__ref":"Paragraph:3621ec5eeb32_45"},{"__ref":"Paragraph:3621ec5eeb32_46"},{"__ref":"Paragraph:3621ec5eeb32_47"},{"__ref":"Paragraph:3621ec5eeb32_48"},{"__ref":"Paragraph:3621ec5eeb32_49"},{"__ref":"Paragraph:3621ec5eeb32_50"},{"__ref":"Paragraph:3621ec5eeb32_51"},{"__ref":"Paragraph:3621ec5eeb32_52"},{"__ref":"Paragraph:3621ec5eeb32_53"},{"__ref":"Paragraph:3621ec5eeb32_54"}]},"validatedShareKey":"","shareKeyCreator":null},"creator":{"__ref":"User:52fa09649919"},"inResponseToEntityType":null,"isLocked":false,"isMarkedPaywallOnly":false,"lockedSource":"LOCKED_POST_SOURCE_NONE","mediumUrl":"https:\u002F\u002Ftech.new-work.se\u002Funit-testing-combine-publishers-6f581e30c370","primaryTopic":null,"topics":[{"__typename":"Topic","slug":"ios-development"},{"__typename":"Topic","slug":"programming"}],"isPublished":true,"latestPublishedVersion":"3621ec5eeb32","visibility":"PUBLIC","postResponses":{"__typename":"PostResponses","count":0},"clapCount":15,"allowResponses":true,"isLimitedState":false,"title":"Unit Testing Combine Publishers","isSeries":false,"sequence":null,"uniqueSlug":"unit-testing-combine-publishers-6f581e30c370","socialTitle":"","socialDek":"","canonicalUrl":"","metaDescription":"","latestPublishedAt":1684489787671,"readingTime":3.7660377358490567,"previewContent":{"__typename":"PreviewContent","subtitle":"Let’s review what kind of expectations are available in Xcode and which ones can be used for testing combine Publishers"},"previewImage":{"__ref":"ImageMetadata:1*8rVBw8xq4W8WvGjW--BNeA.jpeg"},"isShortform":false,"seoTitle":"","firstPublishedAt":1684489787671,"updatedAt":1684489796148,"shortformType":"SHORTFORM_TYPE_LINK","seoDescription":"Tools and tips for testing Combine related code in Swift with Xcode and XCTestExpectation","viewerEdge":{"__ref":"PostViewerEdge:postId:6f581e30c370-viewerId:lo_c50e0cd28248"},"isSuspended":false,"license":"ALL_RIGHTS_RESERVED","tags":[{"__ref":"Tag:swift"},{"__ref":"Tag:combine"},{"__ref":"Tag:unit-testing"},{"__ref":"Tag:xcode"},{"__ref":"Tag:expectations"}],"isNewsletter":false,"statusForCollection":"APPROVED","pendingCollection":null,"detectedLanguage":"en","wordCount":945,"layerCake":0,"responsesLocked":false}}</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:'8e746e9fac15ce31',t:'MTczMjM5OTIyNy4wMDAwMDA='};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>