You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
957 lines
176 KiB
HTML
957 lines
176 KiB
HTML
<!DOCTYPE html><html lang="en-US"><head><title>8. Testing</title><meta property="og:title" content="8. Testing"><meta charset="UTF-8"><meta name="viewport" content="width=device-width,height=device-height,initial-scale=1.0"><meta name="apple-mobile-web-app-capable" content="yes"><meta http-equiv="X-UA-Compatible" content="ie=edge"><meta property="og:type" content="website"><meta name="twitter:card" content="summary"><style>@media screen{body[data-bespoke-view=""] .bespoke-marp-parent>.bespoke-marp-osc>button,body[data-bespoke-view=next] .bespoke-marp-parent>.bespoke-marp-osc>button,body[data-bespoke-view=presenter] .bespoke-marp-presenter-container .bespoke-marp-presenter-info-container button,body[data-bespoke-view=presenter] .bespoke-marp-presenter-container .bespoke-marp-presenter-note-container button{-webkit-tap-highlight-color:transparent;-webkit-appearance:none;appearance:none;background-color:transparent;border:0;color:inherit;cursor:pointer;font-size:inherit;opacity:.8;outline:none;padding:0;transition:opacity .2s linear}body[data-bespoke-view=""] .bespoke-marp-parent>.bespoke-marp-osc>button:disabled,body[data-bespoke-view=next] .bespoke-marp-parent>.bespoke-marp-osc>button:disabled,body[data-bespoke-view=presenter] .bespoke-marp-presenter-container .bespoke-marp-presenter-info-container button:disabled,body[data-bespoke-view=presenter] .bespoke-marp-presenter-container .bespoke-marp-presenter-note-container button:disabled{cursor:not-allowed;opacity:.15!important}body[data-bespoke-view=""] .bespoke-marp-parent>.bespoke-marp-osc>button:hover,body[data-bespoke-view=next] .bespoke-marp-parent>.bespoke-marp-osc>button:hover,body[data-bespoke-view=presenter] .bespoke-marp-presenter-container .bespoke-marp-presenter-info-container button:hover,body[data-bespoke-view=presenter] .bespoke-marp-presenter-container .bespoke-marp-presenter-note-container button:hover{opacity:1}body[data-bespoke-view=""] .bespoke-marp-parent>.bespoke-marp-osc>button:hover:active,body[data-bespoke-view=next] .bespoke-marp-parent>.bespoke-marp-osc>button:hover:active,body[data-bespoke-view=presenter] .bespoke-marp-presenter-container .bespoke-marp-presenter-info-container button:hover:active,body[data-bespoke-view=presenter] .bespoke-marp-presenter-container .bespoke-marp-presenter-note-container button:hover:active{opacity:.6}body[data-bespoke-view=""] .bespoke-marp-parent>.bespoke-marp-osc>button:hover:not(:disabled),body[data-bespoke-view=next] .bespoke-marp-parent>.bespoke-marp-osc>button:hover:not(:disabled),body[data-bespoke-view=presenter] .bespoke-marp-presenter-container .bespoke-marp-presenter-info-container button:hover:not(:disabled),body[data-bespoke-view=presenter] .bespoke-marp-presenter-container .bespoke-marp-presenter-note-container button:hover:not(:disabled){transition:none}body[data-bespoke-view=""] .bespoke-marp-parent>.bespoke-marp-osc>button[data-bespoke-marp-osc=prev],body[data-bespoke-view=next] .bespoke-marp-parent>.bespoke-marp-osc>button[data-bespoke-marp-osc=prev],body[data-bespoke-view=presenter] .bespoke-marp-presenter-container .bespoke-marp-presenter-info-container button.bespoke-marp-presenter-info-page-prev{background:transparent url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDAgMTAwIj48cGF0aCBmaWxsPSJub25lIiBzdHJva2U9IiNmZmYiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgc3Ryb2tlLXdpZHRoPSI1IiBkPSJNNjggOTAgMjggNTBsNDAtNDAiLz48L3N2Zz4=") no-repeat 50%;background-size:contain;overflow:hidden;text-indent:100%;white-space:nowrap}body[data-bespoke-view=""] .bespoke-marp-parent>.bespoke-marp-osc>button[data-bespoke-marp-osc=next],body[data-bespoke-view=next] .bespoke-marp-parent>.bespoke-marp-osc>button[data-bespoke-marp-osc=next],body[data-bespoke-view=presenter] .bespoke-marp-presenter-container .bespoke-marp-presenter-info-container button.bespoke-marp-presenter-info-page-next{background:transparent url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDAgMTAwIj48cGF0aCBmaWxsPSJub25lIiBzdHJva2U9IiNmZmYiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgc3Ryb2tlLXdpZHRoPSI1IiBkPSJtMzIgOTAgNDAtNDAtNDAtNDAiLz48L3N2Zz4=") no-repeat 50%;background-size:contain;overflow:hidden;text-indent:100%;white-space:nowrap}body[data-bespoke-view=""] .bespoke-marp-parent>.bespoke-marp-osc>button[data-bespoke-marp-osc=fullscreen],body[data-bespoke-view=next] .bespoke-marp-parent>.bespoke-marp-osc>button[data-bespoke-marp-osc=fullscreen]{background:transparent url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDAgMTAwIj48ZGVmcz48c3R5bGU+LmF7ZmlsbDpub25lO3N0cm9rZTojZmZmO3N0cm9rZS1saW5lY2FwOnJvdW5kO3N0cm9rZS1saW5lam9pbjpyb3VuZDtzdHJva2Utd2lkdGg6NXB4fTwvc3R5bGU+PC9kZWZzPjxyZWN0IHdpZHRoPSI4MCIgaGVpZ2h0PSI2MCIgeD0iMTAiIHk9IjIwIiBjbGFzcz0iYSIgcng9IjUuNjciLz48cGF0aCBkPSJNNDAgNzBIMjBWNTBtMjAgMEwyMCA3MG00MC00MGgyMHYyMG0tMjAgMCAyMC0yMCIgY2xhc3M9ImEiLz48L3N2Zz4=") no-repeat 50%;background-size:contain;overflow:hidden;text-indent:100%;white-space:nowrap}body[data-bespoke-view=""] .bespoke-marp-parent>.bespoke-marp-osc>button.exit[data-bespoke-marp-osc=fullscreen],body[data-bespoke-view=next] .bespoke-marp-parent>.bespoke-marp-osc>button.exit[data-bespoke-marp-osc=fullscreen]{background-image:url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDAgMTAwIj48ZGVmcz48c3R5bGU+LmF7ZmlsbDpub25lO3N0cm9rZTojZmZmO3N0cm9rZS1saW5lY2FwOnJvdW5kO3N0cm9rZS1saW5lam9pbjpyb3VuZDtzdHJva2Utd2lkdGg6NXB4fTwvc3R5bGU+PC9kZWZzPjxyZWN0IHdpZHRoPSI4MCIgaGVpZ2h0PSI2MCIgeD0iMTAiIHk9IjIwIiBjbGFzcz0iYSIgcng9IjUuNjciLz48cGF0aCBkPSJNMjAgNTBoMjB2MjBtLTIwIDAgMjAtMjBtNDAgMEg2MFYzMG0yMCAwTDYwIDUwIiBjbGFzcz0iYSIvPjwvc3ZnPg==")}body[data-bespoke-view=""] .bespoke-marp-parent>.bespoke-marp-osc>button[data-bespoke-marp-osc=presenter],body[data-bespoke-view=next] .bespoke-marp-parent>.bespoke-marp-osc>button[data-bespoke-marp-osc=presenter]{background:transparent url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDAgMTAwIj48cGF0aCBmaWxsPSJub25lIiBzdHJva2U9IiNmZmYiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgc3Ryb2tlLXdpZHRoPSI1IiBkPSJNODcuOCA0Ny41Qzg5IDUwIDg3LjcgNTIgODUgNTJIMzVhOC43IDguNyAwIDAgMS03LjItNC41bC0xNS42LTMxQzExIDE0IDEyLjIgMTIgMTUgMTJoNTBhOC44IDguOCAwIDAgMSA3LjIgNC41ek02MCA1MnYzNm0tMTAgMGgyME00NSA0MmgyMCIvPjwvc3ZnPg==") no-repeat 50%;background-size:contain;overflow:hidden;text-indent:100%;white-space:nowrap}body[data-bespoke-view=presenter] .bespoke-marp-presenter-container .bespoke-marp-presenter-note-container button.bespoke-marp-presenter-note-bigger{background:transparent url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDAgMTAwIj48cGF0aCBzdHJva2U9IiNmZmYiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgc3Ryb2tlLXdpZHRoPSI1IiBkPSJNMTIgNTBoODBNNTIgOTBWMTAiLz48L3N2Zz4=") no-repeat 50%;background-size:contain;overflow:hidden;text-indent:100%;white-space:nowrap}body[data-bespoke-view=presenter] .bespoke-marp-presenter-container .bespoke-marp-presenter-note-container button.bespoke-marp-presenter-note-smaller{background:transparent url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDAgMTAwIj48cGF0aCBmaWxsPSJub25lIiBzdHJva2U9IiNmZmYiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgc3Ryb2tlLXdpZHRoPSI1IiBkPSJNMTIgNTBoODAiLz48L3N2Zz4=") no-repeat 50%;background-size:contain;overflow:hidden;text-indent:100%;white-space:nowrap}}@keyframes __bespoke_marp_transition_reduced_outgoing__{0%{opacity:1}to{opacity:0}}@keyframes __bespoke_marp_transition_reduced_incoming__{0%{mix-blend-mode:plus-lighter;opacity:0}to{mix-blend-mode:plus-lighter;opacity:1}}.bespoke-marp-note,.bespoke-marp-osc,.bespoke-progress-parent{display:none;transition:none}@media screen{::view-transition-group(*){animation-duration:var(--marp-bespoke-transition-animation-duration,.5s);animation-timing-function:ease}::view-transition-new(*),::view-transition-old(*){animation-delay:0s;animation-direction:var(--marp-bespoke-transition-animation-direction,normal);animation-duration:var(--marp-bespoke-transition-animation-duration,.5s);animation-fill-mode:both;animation-name:var(--marp-bespoke-transition-animation-name,var(--marp-bespoke-transition-animation-name-fallback,__bespoke_marp_transition_no_animation__));mix-blend-mode:normal}::view-transition-old(*){--marp-bespoke-transition-animation-name-fallback:__bespoke_marp_transition_reduced_outgoing__;animation-timing-function:ease}::view-transition-new(*){--marp-bespoke-transition-animation-name-fallback:__bespoke_marp_transition_reduced_incoming__;animation-timing-function:ease}::view-transition-new(root),::view-transition-old(root){animation-timing-function:linear}::view-transition-new(__bespoke_marp_transition_osc__),::view-transition-old(__bespoke_marp_transition_osc__){animation-duration:0s!important;animation-name:__bespoke_marp_transition_osc__!important}::view-transition-new(__bespoke_marp_transition_osc__){opacity:0!important}.bespoke-marp-transition-warming-up::view-transition-group(*),.bespoke-marp-transition-warming-up::view-transition-new(*),.bespoke-marp-transition-warming-up::view-transition-old(*){animation-play-state:paused!important}body,html{height:100%;margin:0}body{background:#000;overflow:hidden}svg.bespoke-marp-slide{content-visibility:hidden;opacity:0;pointer-events:none;z-index:-1}svg.bespoke-marp-slide:not(.bespoke-marp-active) *{view-transition-name:none!important}svg.bespoke-marp-slide.bespoke-marp-active{content-visibility:visible;opacity:1;pointer-events:auto;z-index:0}svg.bespoke-marp-slide.bespoke-marp-active.bespoke-marp-active-ready *{animation-name:__bespoke_marp__!important}@supports not (content-visibility:hidden){svg.bespoke-marp-slide[data-bespoke-marp-load=hideable]{display:none}svg.bespoke-marp-slide[data-bespoke-marp-load=hideable].bespoke-marp-active{display:block}}}@media screen and (prefers-reduced-motion:reduce){svg.bespoke-marp-slide *{view-transition-name:none!important}}@media screen{[data-bespoke-marp-fragment=inactive]{visibility:hidden}body[data-bespoke-view=""] .bespoke-marp-parent,body[data-bespoke-view=next] .bespoke-marp-parent{inset:0;position:absolute}body[data-bespoke-view=""] .bespoke-marp-parent>.bespoke-marp-osc,body[data-bespoke-view=next] .bespoke-marp-parent>.bespoke-marp-osc{view-transition-name:__bespoke_marp_transition_osc__;background:rgba(0,0,0,.65);border-radius:7px;bottom:50px;color:#fff;contain:paint;display:block;font-family:Helvetica,Arial,sans-serif;font-size:16px;left:50%;line-height:0;opacity:1;padding:12px;position:absolute;touch-action:manipulation;transform:translateX(-50%);transition:opacity .2s linear;-webkit-user-select:none;user-select:none;white-space:nowrap;will-change:transform;z-index:1}body[data-bespoke-view=""] .bespoke-marp-parent>.bespoke-marp-osc>*,body[data-bespoke-view=next] .bespoke-marp-parent>.bespoke-marp-osc>*{margin-left:6px}body[data-bespoke-view=""] .bespoke-marp-parent>.bespoke-marp-osc>:first-child,body[data-bespoke-view=next] .bespoke-marp-parent>.bespoke-marp-osc>:first-child{margin-left:0}body[data-bespoke-view=""] .bespoke-marp-parent>.bespoke-marp-osc>span,body[data-bespoke-view=next] .bespoke-marp-parent>.bespoke-marp-osc>span{opacity:.8}body[data-bespoke-view=""] .bespoke-marp-parent>.bespoke-marp-osc>span[data-bespoke-marp-osc=page],body[data-bespoke-view=next] .bespoke-marp-parent>.bespoke-marp-osc>span[data-bespoke-marp-osc=page]{display:inline-block;min-width:140px;text-align:center}body[data-bespoke-view=""] .bespoke-marp-parent>.bespoke-marp-osc>button[data-bespoke-marp-osc=fullscreen],body[data-bespoke-view=""] .bespoke-marp-parent>.bespoke-marp-osc>button[data-bespoke-marp-osc=next],body[data-bespoke-view=""] .bespoke-marp-parent>.bespoke-marp-osc>button[data-bespoke-marp-osc=presenter],body[data-bespoke-view=""] .bespoke-marp-parent>.bespoke-marp-osc>button[data-bespoke-marp-osc=prev],body[data-bespoke-view=next] .bespoke-marp-parent>.bespoke-marp-osc>button[data-bespoke-marp-osc=fullscreen],body[data-bespoke-view=next] .bespoke-marp-parent>.bespoke-marp-osc>button[data-bespoke-marp-osc=next],body[data-bespoke-view=next] .bespoke-marp-parent>.bespoke-marp-osc>button[data-bespoke-marp-osc=presenter],body[data-bespoke-view=next] .bespoke-marp-parent>.bespoke-marp-osc>button[data-bespoke-marp-osc=prev]{height:32px;line-height:32px;width:32px}body[data-bespoke-view=""] .bespoke-marp-parent.bespoke-marp-inactive,body[data-bespoke-view=next] .bespoke-marp-parent.bespoke-marp-inactive{cursor:none}body[data-bespoke-view=""] .bespoke-marp-parent.bespoke-marp-inactive>.bespoke-marp-osc,body[data-bespoke-view=next] .bespoke-marp-parent.bespoke-marp-inactive>.bespoke-marp-osc{opacity:0;pointer-events:none}body[data-bespoke-view=""] svg.bespoke-marp-slide,body[data-bespoke-view=next] svg.bespoke-marp-slide{height:100%;left:0;position:absolute;top:0;width:100%}body[data-bespoke-view=""] .bespoke-progress-parent{background:#222;display:flex;height:5px;width:100%}body[data-bespoke-view=""] .bespoke-progress-parent+.bespoke-marp-parent{top:5px}body[data-bespoke-view=""] .bespoke-progress-parent .bespoke-progress-bar{background:#0288d1;flex:0 0 0;transition:flex-basis .2s cubic-bezier(0,1,1,1)}body[data-bespoke-view=next]{background:transparent}body[data-bespoke-view=presenter]{background:#161616}body[data-bespoke-view=presenter] .bespoke-marp-presenter-container{display:grid;font-family:Helvetica,Arial,sans-serif;grid-template:"current dragbar next" minmax(140px,1fr) "current dragbar note" 2fr "info dragbar note" 3em;grid-template-columns:minmax(3px,var(--bespoke-marp-presenter-split-ratio,66%)) 0 minmax(3px,1fr);height:100%;left:0;position:absolute;top:0;width:100%}body[data-bespoke-view=presenter] .bespoke-marp-presenter-container .bespoke-marp-parent{grid-area:current;overflow:hidden;position:relative}body[data-bespoke-view=presenter] .bespoke-marp-presenter-container .bespoke-marp-parent svg.bespoke-marp-slide{height:calc(100% - 40px);left:20px;pointer-events:none;position:absolute;top:20px;-webkit-user-select:none;user-select:none;width:calc(100% - 40px)}body[data-bespoke-view=presenter] .bespoke-marp-presenter-container .bespoke-marp-parent svg.bespoke-marp-slide.bespoke-marp-active{filter:drop-shadow(0 3px 10px rgba(0,0,0,.5))}body[data-bespoke-view=presenter] .bespoke-marp-presenter-container .bespoke-marp-presenter-dragbar-container{background:#0288d1;cursor:col-resize;grid-area:dragbar;margin-left:-3px;opacity:0;position:relative;transition:opacity .4s linear .1s;width:6px;z-index:10}body[data-bespoke-view=presenter] .bespoke-marp-presenter-container .bespoke-marp-presenter-dragbar-container:hover{opacity:1}body[data-bespoke-view=presenter] .bespoke-marp-presenter-container .bespoke-marp-presenter-dragbar-container.active{opacity:1;transition-delay:0s}body[data-bespoke-view=presenter] .bespoke-marp-presenter-container .bespoke-marp-presenter-next-container{background:#222;cursor:pointer;display:none;grid-area:next;overflow:hidden;position:relative}body[data-bespoke-view=presenter] .bespoke-marp-presenter-container .bespoke-marp-presenter-next-container.active{display:block}body[data-bespoke-view=presenter] .bespoke-marp-presenter-container .bespoke-marp-presenter-next-container iframe.bespoke-marp-presenter-next{background:transparent;border:0;display:block;filter:drop-shadow(0 3px 10px rgba(0,0,0,.5));height:calc(100% - 40px);left:20px;pointer-events:none;position:absolute;top:20px;-webkit-user-select:none;user-select:none;width:calc(100% - 40px)}body[data-bespoke-view=presenter] .bespoke-marp-presenter-container .bespoke-marp-presenter-note-container{background:#222;color:#eee;grid-area:note;position:relative;z-index:1}body[data-bespoke-view=presenter] .bespoke-marp-presenter-container .bespoke-marp-presenter-note-container button{height:1.5em;line-height:1.5em;width:1.5em}body[data-bespoke-view=presenter] .bespoke-marp-presenter-container .bespoke-marp-presenter-note-container .bespoke-marp-presenter-note-wrapper{display:block;inset:0;position:absolute}body[data-bespoke-view=presenter] .bespoke-marp-presenter-container .bespoke-marp-presenter-note-container .bespoke-marp-presenter-note-buttons{background:rgba(0,0,0,.65);border-radius:4px;bottom:0;display:flex;gap:4px;margin:12px;opacity:0;padding:6px;pointer-events:none;position:absolute;right:0;transition:opacity .2s linear}body[data-bespoke-view=presenter] .bespoke-marp-presenter-container .bespoke-marp-presenter-note-container .bespoke-marp-presenter-note-buttons:focus-within,body[data-bespoke-view=presenter] .bespoke-marp-presenter-container .bespoke-marp-presenter-note-container .bespoke-marp-presenter-note-wrapper:focus-within+.bespoke-marp-presenter-note-buttons,body[data-bespoke-view=presenter] .bespoke-marp-presenter-container .bespoke-marp-presenter-note-container:hover .bespoke-marp-presenter-note-buttons{opacity:1;pointer-events:auto}body[data-bespoke-view=presenter] .bespoke-marp-presenter-container .bespoke-marp-presenter-note-container .bespoke-marp-note{word-wrap:break-word;box-sizing:border-box;font-size:calc(1.1em*var(--bespoke-marp-note-font-scale, 1));height:calc(100% - 40px);margin:20px;overflow:auto;padding-right:3px;scrollbar-color:hsla(0,0%,93%,.5) transparent;scrollbar-width:thin;white-space:pre-wrap;width:calc(100% - 40px)}body[data-bespoke-view=presenter] .bespoke-marp-presenter-container .bespoke-marp-presenter-note-container .bespoke-marp-note::-webkit-scrollbar{width:6px}body[data-bespoke-view=presenter] .bespoke-marp-presenter-container .bespoke-marp-presenter-note-container .bespoke-marp-note::-webkit-scrollbar-track{background:transparent}body[data-bespoke-view=presenter] .bespoke-marp-presenter-container .bespoke-marp-presenter-note-container .bespoke-marp-note::-webkit-scrollbar-thumb{background:hsla(0,0%,93%,.5);border-radius:6px}body[data-bespoke-view=presenter] .bespoke-marp-presenter-container .bespoke-marp-presenter-note-container .bespoke-marp-note:empty{pointer-events:none}body[data-bespoke-view=presenter] .bespoke-marp-presenter-container .bespoke-marp-presenter-note-container .bespoke-marp-note.active{display:block}body[data-bespoke-view=presenter] .bespoke-marp-presenter-container .bespoke-marp-presenter-note-container .bespoke-marp-note p:first-child{margin-top:0}body[data-bespoke-view=presenter] .bespoke-marp-presenter-container .bespoke-marp-presenter-note-container .bespoke-marp-note p:last-child{margin-bottom:0}body[data-bespoke-view=presenter] .bespoke-marp-presenter-container .bespoke-marp-presenter-info-container{align-items:center;box-sizing:border-box;color:#eee;display:flex;flex-wrap:nowrap;grid-area:info;justify-content:center;overflow:hidden;padding:0 10px}body[data-bespoke-view=presenter] .bespoke-marp-presenter-container .bespoke-marp-presenter-info-container .bespoke-marp-presenter-info-page,body[data-bespoke-view=presenter] .bespoke-marp-presenter-container .bespoke-marp-presenter-info-container .bespoke-marp-presenter-info-time,body[data-bespoke-view=presenter] .bespoke-marp-presenter-container .bespoke-marp-presenter-info-container .bespoke-marp-presenter-info-timer{box-sizing:border-box;display:block;padding:0 10px;white-space:nowrap;width:100%}body[data-bespoke-view=presenter] .bespoke-marp-presenter-container .bespoke-marp-presenter-info-container button{height:1.5em;line-height:1.5em;width:1.5em}body[data-bespoke-view=presenter] .bespoke-marp-presenter-container .bespoke-marp-presenter-info-container .bespoke-marp-presenter-info-page{order:2;text-align:center}body[data-bespoke-view=presenter] .bespoke-marp-presenter-container .bespoke-marp-presenter-info-container .bespoke-marp-presenter-info-page .bespoke-marp-presenter-info-page-text{display:inline-block;min-width:120px;text-align:center}body[data-bespoke-view=presenter] .bespoke-marp-presenter-container .bespoke-marp-presenter-info-container .bespoke-marp-presenter-info-time{color:#999;order:1;text-align:left}body[data-bespoke-view=presenter] .bespoke-marp-presenter-container .bespoke-marp-presenter-info-container .bespoke-marp-presenter-info-timer{color:#999;order:3;text-align:right}body[data-bespoke-view=presenter] .bespoke-marp-presenter-container .bespoke-marp-presenter-info-container .bespoke-marp-presenter-info-timer:hover{cursor:pointer}}@media print{.bespoke-marp-presenter-info-container,.bespoke-marp-presenter-next-container,.bespoke-marp-presenter-note-container{display:none}}</style><style>div#\:\$p>svg>foreignObject>section{width:1280px;height:720px;box-sizing:border-box;overflow:hidden;position:relative;scroll-snap-align:center center}div#\:\$p>svg>foreignObject>section:after{bottom:0;content:attr(data-marpit-pagination);padding:inherit;pointer-events:none;position:absolute;right:0}div#\:\$p>svg>foreignObject>section:not([data-marpit-pagination]):after{display:none}/* Normalization */div#\:\$p>svg>foreignObject>section :is(h1,marp-h1){font-size:2em;margin:0.67em 0}div#\:\$p>svg>foreignObject>section video::-webkit-media-controls{will-change:transform}@page{size:1280px 720px;margin:0}@media print{body,html{background-color:#fff;margin:0;page-break-inside:avoid;break-inside:avoid-page}div#\:\$p>svg>foreignObject>section{page-break-before:always;break-before:page}div#\:\$p>svg>foreignObject>section,div#\:\$p>svg>foreignObject>section *{-webkit-print-color-adjust:exact!important;animation-delay:0s!important;animation-duration:0s!important;color-adjust:exact!important;transition:none!important}div#\:\$p>svg[data-marpit-svg]{display:block;height:100vh;width:100vw}}
|
|
/*!
|
|
* Marp default theme.
|
|
*
|
|
* @theme default
|
|
* @author Yuki Hattori
|
|
*
|
|
* @auto-scaling true
|
|
* @size 16:9 1280px 720px
|
|
* @size 4:3 960px 720px
|
|
*/div#\:\$p>svg>foreignObject>section,div#\:\$p>svg>foreignObject>section [data-theme=light]{--color-prettylights-syntax-comment:#6e7781;--color-prettylights-syntax-constant:#0550ae;--color-prettylights-syntax-entity:#6639ba;--color-prettylights-syntax-storage-modifier-import:#24292f;--color-prettylights-syntax-entity-tag:#116329;--color-prettylights-syntax-keyword:#cf222e;--color-prettylights-syntax-string:#0a3069;--color-prettylights-syntax-variable:#953800;--color-prettylights-syntax-brackethighlighter-unmatched:#82071e;--color-prettylights-syntax-invalid-illegal-text:#f6f8fa;--color-prettylights-syntax-invalid-illegal-bg:#82071e;--color-prettylights-syntax-carriage-return-text:#f6f8fa;--color-prettylights-syntax-carriage-return-bg:#cf222e;--color-prettylights-syntax-string-regexp:#116329;--color-prettylights-syntax-markup-list:#3b2300;--color-prettylights-syntax-markup-heading:#0550ae;--color-prettylights-syntax-markup-italic:#24292f;--color-prettylights-syntax-markup-bold:#24292f;--color-prettylights-syntax-markup-deleted-text:#82071e;--color-prettylights-syntax-markup-deleted-bg:#ffebe9;--color-prettylights-syntax-markup-inserted-text:#116329;--color-prettylights-syntax-markup-inserted-bg:#dafbe1;--color-prettylights-syntax-markup-changed-text:#953800;--color-prettylights-syntax-markup-changed-bg:#ffd8b5;--color-prettylights-syntax-markup-ignored-text:#eaeef2;--color-prettylights-syntax-markup-ignored-bg:#0550ae;--color-prettylights-syntax-meta-diff-range:#8250df;--color-prettylights-syntax-brackethighlighter-angle:#57606a;--color-prettylights-syntax-sublimelinter-gutter-mark:#8c959f;--color-prettylights-syntax-constant-other-reference-link:#0a3069;--color-fg-default:#1f2328;--color-fg-muted:#656d76;--color-fg-subtle:#6e7781;--color-canvas-default:#fff;--color-canvas-subtle:#f6f8fa;--color-border-default:#d0d7de;--color-border-muted:#d8dee4;--color-neutral-muted:rgba(175,184,193,.2);--color-accent-fg:#0969da;--color-accent-emphasis:#0969da;--color-attention-subtle:#fff8c5;--color-danger-fg:#d1242f;color-scheme:light}div#\:\$p>svg>foreignObject>section [data-theme=dark],div#\:\$p>svg>foreignObject>section:where(.invert){--color-prettylights-syntax-comment:#8b949e;--color-prettylights-syntax-constant:#79c0ff;--color-prettylights-syntax-entity:#d2a8ff;--color-prettylights-syntax-storage-modifier-import:#c9d1d9;--color-prettylights-syntax-entity-tag:#7ee787;--color-prettylights-syntax-keyword:#ff7b72;--color-prettylights-syntax-string:#a5d6ff;--color-prettylights-syntax-variable:#ffa657;--color-prettylights-syntax-brackethighlighter-unmatched:#f85149;--color-prettylights-syntax-invalid-illegal-text:#f0f6fc;--color-prettylights-syntax-invalid-illegal-bg:#8e1519;--color-prettylights-syntax-carriage-return-text:#f0f6fc;--color-prettylights-syntax-carriage-return-bg:#b62324;--color-prettylights-syntax-string-regexp:#7ee787;--color-prettylights-syntax-markup-list:#f2cc60;--color-prettylights-syntax-markup-heading:#1f6feb;--color-prettylights-syntax-markup-italic:#c9d1d9;--color-prettylights-syntax-markup-bold:#c9d1d9;--color-prettylights-syntax-markup-deleted-text:#ffdcd7;--color-prettylights-syntax-markup-deleted-bg:#67060c;--color-prettylights-syntax-markup-inserted-text:#aff5b4;--color-prettylights-syntax-markup-inserted-bg:#033a16;--color-prettylights-syntax-markup-changed-text:#ffdfb6;--color-prettylights-syntax-markup-changed-bg:#5a1e02;--color-prettylights-syntax-markup-ignored-text:#c9d1d9;--color-prettylights-syntax-markup-ignored-bg:#1158c7;--color-prettylights-syntax-meta-diff-range:#d2a8ff;--color-prettylights-syntax-brackethighlighter-angle:#8b949e;--color-prettylights-syntax-sublimelinter-gutter-mark:#484f58;--color-prettylights-syntax-constant-other-reference-link:#a5d6ff;--color-fg-default:#e6edf3;--color-fg-muted:#7d8590;--color-fg-subtle:#6e7681;--color-canvas-default:#0d1117;--color-canvas-subtle:#161b22;--color-border-default:#30363d;--color-border-muted:#21262d;--color-neutral-muted:hsla(215,8%,47%,.4);--color-accent-fg:#2f81f7;--color-accent-emphasis:#1f6feb;--color-attention-subtle:rgba(187,128,9,.15);--color-danger-fg:#f85149;color-scheme:dark}div#\:\$p>svg>foreignObject>section{-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%;word-wrap:break-word;background-color:var(--color-canvas-default);color:var(--color-fg-default);font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Noto Sans,Helvetica,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji;font-size:16px;line-height:1.5;margin:0}div#\:\$p>svg>foreignObject>section{--marpit-root-font-size:16px}div#\:\$p>svg>foreignObject>section :is(h1,marp-h1):hover .anchor .octicon-link:before,div#\:\$p>svg>foreignObject>section :is(h2,marp-h2):hover .anchor .octicon-link:before,div#\:\$p>svg>foreignObject>section :is(h3,marp-h3):hover .anchor .octicon-link:before,div#\:\$p>svg>foreignObject>section :is(h4,marp-h4):hover .anchor .octicon-link:before,div#\:\$p>svg>foreignObject>section :is(h5,marp-h5):hover .anchor .octicon-link:before,div#\:\$p>svg>foreignObject>section :is(h6,marp-h6):hover .anchor .octicon-link:before{background-color:currentColor;content:" ";display:inline-block;height:16px;-webkit-mask-image:url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 0 0 1.06 1.06l1.25-1.25a2 2 0 1 1 2.83 2.83l-2.5 2.5a2 2 0 0 1-2.83 0 .75.75 0 0 0-1.06 1.06 3.5 3.5 0 0 0 4.95 0l2.5-2.5a3.5 3.5 0 0 0-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 0 1 0-2.83l2.5-2.5a2 2 0 0 1 2.83 0 .75.75 0 0 0 1.06-1.06 3.5 3.5 0 0 0-4.95 0l-2.5 2.5a3.5 3.5 0 0 0 4.95 4.95l1.25-1.25a.75.75 0 0 0-1.06-1.06l-1.25 1.25a2 2 0 0 1-2.83 0z"/></svg>');mask-image:url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M7.775 3.275a.75.75 0 0 0 1.06 1.06l1.25-1.25a2 2 0 1 1 2.83 2.83l-2.5 2.5a2 2 0 0 1-2.83 0 .75.75 0 0 0-1.06 1.06 3.5 3.5 0 0 0 4.95 0l2.5-2.5a3.5 3.5 0 0 0-4.95-4.95l-1.25 1.25zm-4.69 9.64a2 2 0 0 1 0-2.83l2.5-2.5a2 2 0 0 1 2.83 0 .75.75 0 0 0 1.06-1.06 3.5 3.5 0 0 0-4.95 0l-2.5 2.5a3.5 3.5 0 0 0 4.95 4.95l1.25-1.25a.75.75 0 0 0-1.06-1.06l-1.25 1.25a2 2 0 0 1-2.83 0z"/></svg>');width:16px}div#\:\$p>svg>foreignObject>section details,div#\:\$p>svg>foreignObject>section figcaption,div#\:\$p>svg>foreignObject>section figure{display:block}div#\:\$p>svg>foreignObject>section summary{display:list-item}div#\:\$p>svg>foreignObject>section [hidden]{display:none!important}div#\:\$p>svg>foreignObject>section a{background-color:transparent;color:var(--color-accent-fg);text-decoration:none}div#\:\$p>svg>foreignObject>section abbr[title]{border-bottom:none;-webkit-text-decoration:underline dotted;text-decoration:underline dotted}div#\:\$p>svg>foreignObject>section b,div#\:\$p>svg>foreignObject>section strong{font-weight:var(--base-text-weight-semibold,600)}div#\:\$p>svg>foreignObject>section dfn{font-style:italic}div#\:\$p>svg>foreignObject>section :is(h1,marp-h1){border-bottom:1px solid var(--color-border-muted);font-size:2em;font-weight:var(--base-text-weight-semibold,600);margin:.67em 0;padding-bottom:.3em}div#\:\$p>svg>foreignObject>section mark{background-color:var(--color-attention-subtle);color:var(--color-fg-default)}div#\:\$p>svg>foreignObject>section small{font-size:90%}div#\:\$p>svg>foreignObject>section sub,div#\:\$p>svg>foreignObject>section sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}div#\:\$p>svg>foreignObject>section sub{bottom:-.25em}div#\:\$p>svg>foreignObject>section sup{top:-.5em}div#\:\$p>svg>foreignObject>section img{background-color:var(--color-canvas-default);border-style:none;box-sizing:content-box;max-width:100%}div#\:\$p>svg>foreignObject>section :is(pre,marp-pre),div#\:\$p>svg>foreignObject>section code,div#\:\$p>svg>foreignObject>section kbd,div#\:\$p>svg>foreignObject>section samp{font-family:monospace;font-size:1em}div#\:\$p>svg>foreignObject>section figure{margin:1em 40px}div#\:\$p>svg>foreignObject>section hr{background:transparent;background-color:var(--color-border-default);border:0;box-sizing:content-box;height:.25em;margin:24px 0;overflow:hidden;padding:0}div#\:\$p>svg>foreignObject>section input{font:inherit;font-family:inherit;font-size:inherit;line-height:inherit;margin:0;overflow:visible}div#\:\$p>svg>foreignObject>section [type=button],div#\:\$p>svg>foreignObject>section [type=reset],div#\:\$p>svg>foreignObject>section [type=submit]{-webkit-appearance:button}div#\:\$p>svg>foreignObject>section [type=checkbox],div#\:\$p>svg>foreignObject>section [type=radio]{box-sizing:border-box;padding:0}div#\:\$p>svg>foreignObject>section [type=number]::-webkit-inner-spin-button,div#\:\$p>svg>foreignObject>section [type=number]::-webkit-outer-spin-button{height:auto}div#\:\$p>svg>foreignObject>section [type=search]::-webkit-search-cancel-button,div#\:\$p>svg>foreignObject>section [type=search]::-webkit-search-decoration{-webkit-appearance:none}div#\:\$p>svg>foreignObject>section ::-webkit-input-placeholder{color:inherit;opacity:.54}div#\:\$p>svg>foreignObject>section ::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}div#\:\$p>svg>foreignObject>section a:hover{text-decoration:underline}div#\:\$p>svg>foreignObject>section ::-moz-placeholder{color:var(--color-fg-subtle);opacity:1}div#\:\$p>svg>foreignObject>section ::placeholder{color:var(--color-fg-subtle);opacity:1}div#\:\$p>svg>foreignObject>section hr:after,div#\:\$p>svg>foreignObject>section hr:before{content:"";display:table}div#\:\$p>svg>foreignObject>section hr:after{clear:both}div#\:\$p>svg>foreignObject>section table{border-collapse:collapse;border-spacing:0;display:block;max-width:100%;overflow:auto;width:-moz-max-content;width:max-content}div#\:\$p>svg>foreignObject>section td,div#\:\$p>svg>foreignObject>section th{padding:0}div#\:\$p>svg>foreignObject>section details summary{cursor:pointer}div#\:\$p>svg>foreignObject>section details:not([open])>:not(summary){display:none!important}div#\:\$p>svg>foreignObject>section [role=button]:focus,div#\:\$p>svg>foreignObject>section a:focus,div#\:\$p>svg>foreignObject>section input[type=checkbox]:focus,div#\:\$p>svg>foreignObject>section input[type=radio]:focus{box-shadow:none;outline:2px solid var(--color-accent-fg);outline-offset:-2px}div#\:\$p>svg>foreignObject>section [role=button]:focus:not(:focus-visible),div#\:\$p>svg>foreignObject>section a:focus:not(:focus-visible),div#\:\$p>svg>foreignObject>section input[type=checkbox]:focus:not(:focus-visible),div#\:\$p>svg>foreignObject>section input[type=radio]:focus:not(:focus-visible){outline:1px solid transparent}div#\:\$p>svg>foreignObject>section [role=button]:focus-visible,div#\:\$p>svg>foreignObject>section a:focus-visible,div#\:\$p>svg>foreignObject>section input[type=checkbox]:focus-visible,div#\:\$p>svg>foreignObject>section input[type=radio]:focus-visible{box-shadow:none;outline:2px solid var(--color-accent-fg);outline-offset:-2px}div#\:\$p>svg>foreignObject>section a:not([class]):focus,div#\:\$p>svg>foreignObject>section a:not([class]):focus-visible,div#\:\$p>svg>foreignObject>section input[type=checkbox]:focus,div#\:\$p>svg>foreignObject>section input[type=checkbox]:focus-visible,div#\:\$p>svg>foreignObject>section input[type=radio]:focus,div#\:\$p>svg>foreignObject>section input[type=radio]:focus-visible{outline-offset:0}div#\:\$p>svg>foreignObject>section kbd{background-color:var(--color-canvas-subtle);border-bottom-color:var(--color-neutral-muted);border:1px solid var(--color-neutral-muted);border-radius:6px;box-shadow:inset 0 -1px 0 var(--color-neutral-muted);color:var(--color-fg-default);display:inline-block;font:11px ui-monospace,SFMono-Regular,SF Mono,Menlo,Consolas,Liberation Mono,monospace;line-height:10px;padding:3px 5px;vertical-align:middle}div#\:\$p>svg>foreignObject>section :is(h1,marp-h1),div#\:\$p>svg>foreignObject>section :is(h2,marp-h2),div#\:\$p>svg>foreignObject>section :is(h3,marp-h3),div#\:\$p>svg>foreignObject>section :is(h4,marp-h4),div#\:\$p>svg>foreignObject>section :is(h5,marp-h5),div#\:\$p>svg>foreignObject>section :is(h6,marp-h6){font-weight:var(--base-text-weight-semibold,600);line-height:1.25;margin-bottom:16px;margin-top:24px}div#\:\$p>svg>foreignObject>section :is(h2,marp-h2){border-bottom:1px solid var(--color-border-muted);font-size:1.5em;padding-bottom:.3em}div#\:\$p>svg>foreignObject>section :is(h2,marp-h2),div#\:\$p>svg>foreignObject>section :is(h3,marp-h3){font-weight:var(--base-text-weight-semibold,600)}div#\:\$p>svg>foreignObject>section :is(h3,marp-h3){font-size:1.25em}div#\:\$p>svg>foreignObject>section :is(h4,marp-h4){font-size:1em}div#\:\$p>svg>foreignObject>section :is(h4,marp-h4),div#\:\$p>svg>foreignObject>section :is(h5,marp-h5){font-weight:var(--base-text-weight-semibold,600)}div#\:\$p>svg>foreignObject>section :is(h5,marp-h5){font-size:.875em}div#\:\$p>svg>foreignObject>section :is(h6,marp-h6){color:var(--color-fg-muted);font-size:.85em;font-weight:var(--base-text-weight-semibold,600)}div#\:\$p>svg>foreignObject>section p{margin-bottom:10px;margin-top:0}div#\:\$p>svg>foreignObject>section blockquote{border-left:.25em solid var(--color-border-default);color:var(--color-fg-muted);margin:0;padding:0 1em}div#\:\$p>svg>foreignObject>section ol,div#\:\$p>svg>foreignObject>section ul{margin-bottom:0;margin-top:0;padding-left:2em}div#\:\$p>svg>foreignObject>section ol ol,div#\:\$p>svg>foreignObject>section ul ol{list-style-type:lower-roman}div#\:\$p>svg>foreignObject>section ol ol ol,div#\:\$p>svg>foreignObject>section ol ul ol,div#\:\$p>svg>foreignObject>section ul ol ol,div#\:\$p>svg>foreignObject>section ul ul ol{list-style-type:lower-alpha}div#\:\$p>svg>foreignObject>section dd{margin-left:0}div#\:\$p>svg>foreignObject>section :is(pre,marp-pre),div#\:\$p>svg>foreignObject>section code,div#\:\$p>svg>foreignObject>section samp,div#\:\$p>svg>foreignObject>section tt{font-family:ui-monospace,SFMono-Regular,SF Mono,Menlo,Consolas,Liberation Mono,monospace;font-size:12px}div#\:\$p>svg>foreignObject>section :is(pre,marp-pre){word-wrap:normal;margin-bottom:0;margin-top:0}div#\:\$p>svg>foreignObject>section .octicon{fill:currentColor;display:inline-block;overflow:visible!important;vertical-align:text-bottom}div#\:\$p>svg>foreignObject>section input::-webkit-inner-spin-button,div#\:\$p>svg>foreignObject>section input::-webkit-outer-spin-button{-webkit-appearance:none;appearance:none;margin:0}div#\:\$p>svg>foreignObject>section:after,div#\:\$p>svg>foreignObject>section:before{
|
|
/* content:""; */display:table}div#\:\$p>svg>foreignObject>section:after{clear:both}div#\:\$p>svg>foreignObject>section>:first-child{margin-top:0!important}div#\:\$p>svg>foreignObject>section>:last-child{margin-bottom:0!important}div#\:\$p>svg>foreignObject>section a:not([href]){color:inherit;text-decoration:none}div#\:\$p>svg>foreignObject>section .absent{color:var(--color-danger-fg)}div#\:\$p>svg>foreignObject>section .anchor{float:left;line-height:1;margin-left:-20px;padding-right:4px}div#\:\$p>svg>foreignObject>section .anchor:focus{outline:none}div#\:\$p>svg>foreignObject>section :is(pre,marp-pre),div#\:\$p>svg>foreignObject>section blockquote,div#\:\$p>svg>foreignObject>section details,div#\:\$p>svg>foreignObject>section dl,div#\:\$p>svg>foreignObject>section ol,div#\:\$p>svg>foreignObject>section p,div#\:\$p>svg>foreignObject>section table,div#\:\$p>svg>foreignObject>section ul{margin-bottom:16px;margin-top:0}div#\:\$p>svg>foreignObject>section blockquote>:first-child{margin-top:0}div#\:\$p>svg>foreignObject>section blockquote>:last-child{margin-bottom:0}div#\:\$p>svg>foreignObject>section :is(h1,marp-h1) .octicon-link,div#\:\$p>svg>foreignObject>section :is(h2,marp-h2) .octicon-link,div#\:\$p>svg>foreignObject>section :is(h3,marp-h3) .octicon-link,div#\:\$p>svg>foreignObject>section :is(h4,marp-h4) .octicon-link,div#\:\$p>svg>foreignObject>section :is(h5,marp-h5) .octicon-link,div#\:\$p>svg>foreignObject>section :is(h6,marp-h6) .octicon-link{color:var(--color-fg-default);vertical-align:middle;visibility:hidden}div#\:\$p>svg>foreignObject>section :is(h1,marp-h1):hover .anchor,div#\:\$p>svg>foreignObject>section :is(h2,marp-h2):hover .anchor,div#\:\$p>svg>foreignObject>section :is(h3,marp-h3):hover .anchor,div#\:\$p>svg>foreignObject>section :is(h4,marp-h4):hover .anchor,div#\:\$p>svg>foreignObject>section :is(h5,marp-h5):hover .anchor,div#\:\$p>svg>foreignObject>section :is(h6,marp-h6):hover .anchor{text-decoration:none}div#\:\$p>svg>foreignObject>section :is(h1,marp-h1):hover .anchor .octicon-link,div#\:\$p>svg>foreignObject>section :is(h2,marp-h2):hover .anchor .octicon-link,div#\:\$p>svg>foreignObject>section :is(h3,marp-h3):hover .anchor .octicon-link,div#\:\$p>svg>foreignObject>section :is(h4,marp-h4):hover .anchor .octicon-link,div#\:\$p>svg>foreignObject>section :is(h5,marp-h5):hover .anchor .octicon-link,div#\:\$p>svg>foreignObject>section :is(h6,marp-h6):hover .anchor .octicon-link{visibility:visible}div#\:\$p>svg>foreignObject>section :is(h1,marp-h1) code,div#\:\$p>svg>foreignObject>section :is(h1,marp-h1) tt,div#\:\$p>svg>foreignObject>section :is(h2,marp-h2) code,div#\:\$p>svg>foreignObject>section :is(h2,marp-h2) tt,div#\:\$p>svg>foreignObject>section :is(h3,marp-h3) code,div#\:\$p>svg>foreignObject>section :is(h3,marp-h3) tt,div#\:\$p>svg>foreignObject>section :is(h4,marp-h4) code,div#\:\$p>svg>foreignObject>section :is(h4,marp-h4) tt,div#\:\$p>svg>foreignObject>section :is(h5,marp-h5) code,div#\:\$p>svg>foreignObject>section :is(h5,marp-h5) tt,div#\:\$p>svg>foreignObject>section :is(h6,marp-h6) code,div#\:\$p>svg>foreignObject>section :is(h6,marp-h6) tt{font-size:inherit;padding:0 .2em}div#\:\$p>svg>foreignObject>section summary :is(h1,marp-h1),div#\:\$p>svg>foreignObject>section summary :is(h2,marp-h2),div#\:\$p>svg>foreignObject>section summary :is(h3,marp-h3),div#\:\$p>svg>foreignObject>section summary :is(h4,marp-h4),div#\:\$p>svg>foreignObject>section summary :is(h5,marp-h5),div#\:\$p>svg>foreignObject>section summary :is(h6,marp-h6){display:inline-block}div#\:\$p>svg>foreignObject>section summary :is(h1,marp-h1) .anchor,div#\:\$p>svg>foreignObject>section summary :is(h2,marp-h2) .anchor,div#\:\$p>svg>foreignObject>section summary :is(h3,marp-h3) .anchor,div#\:\$p>svg>foreignObject>section summary :is(h4,marp-h4) .anchor,div#\:\$p>svg>foreignObject>section summary :is(h5,marp-h5) .anchor,div#\:\$p>svg>foreignObject>section summary :is(h6,marp-h6) .anchor{margin-left:-40px}div#\:\$p>svg>foreignObject>section summary :is(h1,marp-h1),div#\:\$p>svg>foreignObject>section summary :is(h2,marp-h2){border-bottom:0;padding-bottom:0}div#\:\$p>svg>foreignObject>section ol.no-list,div#\:\$p>svg>foreignObject>section ul.no-list{list-style-type:none;padding:0}div#\:\$p>svg>foreignObject>section ol[type="a s"]{list-style-type:lower-alpha}div#\:\$p>svg>foreignObject>section ol[type="A s"]{list-style-type:upper-alpha}div#\:\$p>svg>foreignObject>section ol[type="i s"]{list-style-type:lower-roman}div#\:\$p>svg>foreignObject>section ol[type="I s"]{list-style-type:upper-roman}div#\:\$p>svg>foreignObject>section div>ol:not([type]),div#\:\$p>svg>foreignObject>section ol[type="1"]{list-style-type:decimal}div#\:\$p>svg>foreignObject>section ol ol,div#\:\$p>svg>foreignObject>section ol ul,div#\:\$p>svg>foreignObject>section ul ol,div#\:\$p>svg>foreignObject>section ul ul{margin-bottom:0;margin-top:0}div#\:\$p>svg>foreignObject>section li>p{margin-top:16px}div#\:\$p>svg>foreignObject>section li+li{margin-top:.25em}div#\:\$p>svg>foreignObject>section dl{padding:0}div#\:\$p>svg>foreignObject>section dl dt{font-size:1em;font-style:italic;font-weight:var(--base-text-weight-semibold,600);margin-top:16px;padding:0}div#\:\$p>svg>foreignObject>section dl dd{margin-bottom:16px;padding:0 16px}div#\:\$p>svg>foreignObject>section table th{font-weight:var(--base-text-weight-semibold,600)}div#\:\$p>svg>foreignObject>section table td,div#\:\$p>svg>foreignObject>section table th{border:1px solid var(--color-border-default);padding:6px 13px}div#\:\$p>svg>foreignObject>section table td>:last-child{margin-bottom:0}div#\:\$p>svg>foreignObject>section table tr{background-color:var(--color-canvas-default);border-top:1px solid var(--color-border-muted)}div#\:\$p>svg>foreignObject>section table tr:nth-child(2n){background-color:var(--color-canvas-subtle)}div#\:\$p>svg>foreignObject>section table img{background-color:transparent}div#\:\$p>svg>foreignObject>section img[align=right]{padding-left:20px}div#\:\$p>svg>foreignObject>section img[align=left]{padding-right:20px}div#\:\$p>svg>foreignObject>section .emoji{background-color:transparent;max-width:none;vertical-align:text-top}div#\:\$p>svg>foreignObject>section :is(span,marp-span).frame,div#\:\$p>svg>foreignObject>section :is(span,marp-span).frame>:is(span,marp-span){display:block;overflow:hidden}div#\:\$p>svg>foreignObject>section :is(span,marp-span).frame>:is(span,marp-span){border:1px solid var(--color-border-default);float:left;margin:13px 0 0;padding:7px;width:auto}div#\:\$p>svg>foreignObject>section :is(span,marp-span).frame :is(span,marp-span) img{display:block;float:left}div#\:\$p>svg>foreignObject>section :is(span,marp-span).frame :is(span,marp-span) :is(span,marp-span){clear:both;color:var(--color-fg-default);display:block;padding:5px 0 0}div#\:\$p>svg>foreignObject>section :is(span,marp-span).align-center{clear:both;display:block;overflow:hidden}div#\:\$p>svg>foreignObject>section :is(span,marp-span).align-center>:is(span,marp-span){display:block;margin:13px auto 0;overflow:hidden;text-align:center}div#\:\$p>svg>foreignObject>section :is(span,marp-span).align-center :is(span,marp-span) img{margin:0 auto;text-align:center}div#\:\$p>svg>foreignObject>section :is(span,marp-span).align-right{clear:both;display:block;overflow:hidden}div#\:\$p>svg>foreignObject>section :is(span,marp-span).align-right>:is(span,marp-span){display:block;margin:13px 0 0;overflow:hidden;text-align:right}div#\:\$p>svg>foreignObject>section :is(span,marp-span).align-right :is(span,marp-span) img{margin:0;text-align:right}div#\:\$p>svg>foreignObject>section :is(span,marp-span).float-left{display:block;float:left;margin-right:13px;overflow:hidden}div#\:\$p>svg>foreignObject>section :is(span,marp-span).float-left :is(span,marp-span){margin:13px 0 0}div#\:\$p>svg>foreignObject>section :is(span,marp-span).float-right{display:block;float:right;margin-left:13px;overflow:hidden}div#\:\$p>svg>foreignObject>section :is(span,marp-span).float-right>:is(span,marp-span){display:block;margin:13px auto 0;overflow:hidden;text-align:right}div#\:\$p>svg>foreignObject>section code,div#\:\$p>svg>foreignObject>section tt{background-color:var(--color-neutral-muted);border-radius:6px;font-size:85%;margin:0;padding:.2em .4em;white-space:break-spaces}div#\:\$p>svg>foreignObject>section code br,div#\:\$p>svg>foreignObject>section tt br{display:none}div#\:\$p>svg>foreignObject>section del code{text-decoration:inherit}div#\:\$p>svg>foreignObject>section samp{font-size:85%}div#\:\$p>svg>foreignObject>section :is(pre,marp-pre) code{font-size:100%}div#\:\$p>svg>foreignObject>section :is(pre,marp-pre)>code{background:transparent;border:0;margin:0;padding:0;white-space:pre;word-break:normal}div#\:\$p>svg>foreignObject>section .highlight{margin-bottom:16px}div#\:\$p>svg>foreignObject>section .highlight :is(pre,marp-pre){margin-bottom:0;word-break:normal}div#\:\$p>svg>foreignObject>section :is(pre,marp-pre){background-color:var(--color-canvas-subtle);border-radius:6px;color:var(--color-fg-default);font-size:85%;line-height:1.45;overflow:auto;padding:16px}div#\:\$p>svg>foreignObject>section :is(pre,marp-pre) code,div#\:\$p>svg>foreignObject>section :is(pre,marp-pre) tt{word-wrap:normal;background-color:transparent;border:0;display:inline;line-height:inherit;margin:0;max-width:auto;overflow:visible;padding:0}div#\:\$p>svg>foreignObject>section .csv-data td,div#\:\$p>svg>foreignObject>section .csv-data th{font-size:12px;line-height:1;overflow:hidden;padding:5px;text-align:left;white-space:nowrap}div#\:\$p>svg>foreignObject>section .csv-data .blob-num{background:var(--color-canvas-default);border:0;padding:10px 8px 9px;text-align:right}div#\:\$p>svg>foreignObject>section .csv-data tr{border-top:0}div#\:\$p>svg>foreignObject>section .csv-data th{background:var(--color-canvas-subtle);border-top:0;font-weight:var(--base-text-weight-semibold,600)}div#\:\$p>svg>foreignObject>section [data-footnote-ref]:before{content:"["}div#\:\$p>svg>foreignObject>section [data-footnote-ref]:after{content:"]"}div#\:\$p>svg>foreignObject>section .footnotes{border-top:1px solid var(--color-border-default);color:var(--color-fg-muted);font-size:12px}div#\:\$p>svg>foreignObject>section div#\:\$p>svg>foreignObject>section section.footnotes{--marpit-root-font-size:12px}div#\:\$p>svg>foreignObject>section .footnotes ol{padding-left:16px}div#\:\$p>svg>foreignObject>section .footnotes ol ul{display:inline-block;margin-top:16px;padding-left:16px}div#\:\$p>svg>foreignObject>section .footnotes li{position:relative}div#\:\$p>svg>foreignObject>section .footnotes li:target:before{border:2px solid var(--color-accent-emphasis);border-radius:6px;bottom:-8px;content:"";left:-24px;pointer-events:none;position:absolute;right:-8px;top:-8px}div#\:\$p>svg>foreignObject>section .footnotes li:target{color:var(--color-fg-default)}div#\:\$p>svg>foreignObject>section .footnotes .data-footnote-backref g-emoji{font-family:monospace}div#\:\$p>svg>foreignObject>section .pl-c{color:var(--color-prettylights-syntax-comment)}div#\:\$p>svg>foreignObject>section .pl-c1,div#\:\$p>svg>foreignObject>section .pl-s .pl-v{color:var(--color-prettylights-syntax-constant)}div#\:\$p>svg>foreignObject>section .pl-e,div#\:\$p>svg>foreignObject>section .pl-en{color:var(--color-prettylights-syntax-entity)}div#\:\$p>svg>foreignObject>section .pl-s .pl-s1,div#\:\$p>svg>foreignObject>section .pl-smi{color:var(--color-prettylights-syntax-storage-modifier-import)}div#\:\$p>svg>foreignObject>section .pl-ent{color:var(--color-prettylights-syntax-entity-tag)}div#\:\$p>svg>foreignObject>section .pl-k{color:var(--color-prettylights-syntax-keyword)}div#\:\$p>svg>foreignObject>section .pl-pds,div#\:\$p>svg>foreignObject>section .pl-s,div#\:\$p>svg>foreignObject>section .pl-s .pl-pse .pl-s1,div#\:\$p>svg>foreignObject>section .pl-sr,div#\:\$p>svg>foreignObject>section .pl-sr .pl-cce,div#\:\$p>svg>foreignObject>section .pl-sr .pl-sra,div#\:\$p>svg>foreignObject>section .pl-sr .pl-sre{color:var(--color-prettylights-syntax-string)}div#\:\$p>svg>foreignObject>section .pl-smw,div#\:\$p>svg>foreignObject>section .pl-v{color:var(--color-prettylights-syntax-variable)}div#\:\$p>svg>foreignObject>section .pl-bu{color:var(--color-prettylights-syntax-brackethighlighter-unmatched)}div#\:\$p>svg>foreignObject>section .pl-ii{background-color:var(--color-prettylights-syntax-invalid-illegal-bg);color:var(--color-prettylights-syntax-invalid-illegal-text)}div#\:\$p>svg>foreignObject>section .pl-c2{background-color:var(--color-prettylights-syntax-carriage-return-bg);color:var(--color-prettylights-syntax-carriage-return-text)}div#\:\$p>svg>foreignObject>section .pl-sr .pl-cce{color:var(--color-prettylights-syntax-string-regexp);font-weight:700}div#\:\$p>svg>foreignObject>section .pl-ml{color:var(--color-prettylights-syntax-markup-list)}div#\:\$p>svg>foreignObject>section .pl-mh,div#\:\$p>svg>foreignObject>section .pl-mh .pl-en,div#\:\$p>svg>foreignObject>section .pl-ms{color:var(--color-prettylights-syntax-markup-heading);font-weight:700}div#\:\$p>svg>foreignObject>section .pl-mi{color:var(--color-prettylights-syntax-markup-italic);font-style:italic}div#\:\$p>svg>foreignObject>section .pl-mb{color:var(--color-prettylights-syntax-markup-bold);font-weight:700}div#\:\$p>svg>foreignObject>section .pl-md{background-color:var(--color-prettylights-syntax-markup-deleted-bg);color:var(--color-prettylights-syntax-markup-deleted-text)}div#\:\$p>svg>foreignObject>section .pl-mi1{background-color:var(--color-prettylights-syntax-markup-inserted-bg);color:var(--color-prettylights-syntax-markup-inserted-text)}div#\:\$p>svg>foreignObject>section .pl-mc{background-color:var(--color-prettylights-syntax-markup-changed-bg);color:var(--color-prettylights-syntax-markup-changed-text)}div#\:\$p>svg>foreignObject>section .pl-mi2{background-color:var(--color-prettylights-syntax-markup-ignored-bg);color:var(--color-prettylights-syntax-markup-ignored-text)}div#\:\$p>svg>foreignObject>section .pl-mdr{color:var(--color-prettylights-syntax-meta-diff-range);font-weight:700}div#\:\$p>svg>foreignObject>section .pl-ba{color:var(--color-prettylights-syntax-brackethighlighter-angle)}div#\:\$p>svg>foreignObject>section .pl-sg{color:var(--color-prettylights-syntax-sublimelinter-gutter-mark)}div#\:\$p>svg>foreignObject>section .pl-corl{color:var(--color-prettylights-syntax-constant-other-reference-link);text-decoration:underline}div#\:\$p>svg>foreignObject>section g-emoji{display:inline-block;font-family:Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol;font-size:1em;font-style:normal!important;font-weight:var(--base-text-weight-normal,400);line-height:1;min-width:1ch;vertical-align:-.075em}div#\:\$p>svg>foreignObject>section g-emoji img{height:1em;width:1em}div#\:\$p>svg>foreignObject>section .task-list-item{list-style-type:none}div#\:\$p>svg>foreignObject>section .task-list-item label{font-weight:var(--base-text-weight-normal,400)}div#\:\$p>svg>foreignObject>section .task-list-item.enabled label{cursor:pointer}div#\:\$p>svg>foreignObject>section .task-list-item+.task-list-item{margin-top:4px}div#\:\$p>svg>foreignObject>section .task-list-item .handle{display:none}div#\:\$p>svg>foreignObject>section .task-list-item-checkbox{margin:0 .2em .25em -1.4em;vertical-align:middle}div#\:\$p>svg>foreignObject>section .contains-task-list:dir(rtl) .task-list-item-checkbox{margin:0 -1.6em .25em .2em}div#\:\$p>svg>foreignObject>section .contains-task-list{position:relative}div#\:\$p>svg>foreignObject>section .contains-task-list:focus-within .task-list-item-convert-container,div#\:\$p>svg>foreignObject>section .contains-task-list:hover .task-list-item-convert-container{clip:auto;display:block;height:24px;overflow:visible;width:auto}div#\:\$p>svg>foreignObject>section ::-webkit-calendar-picker-indicator{filter:invert(50%)}div#\:\$p>svg>foreignObject>section :is(h1,marp-h1){color:var(--h1-color);font-size:1.6em}div#\:\$p>svg>foreignObject>section :is(h1,marp-h1),div#\:\$p>svg>foreignObject>section :is(h2,marp-h2){border-bottom:none}div#\:\$p>svg>foreignObject>section :is(h2,marp-h2){font-size:1.3em}div#\:\$p>svg>foreignObject>section :is(h3,marp-h3){font-size:1.1em}div#\:\$p>svg>foreignObject>section :is(h4,marp-h4){font-size:1.05em}div#\:\$p>svg>foreignObject>section :is(h5,marp-h5){font-size:1em}div#\:\$p>svg>foreignObject>section :is(h6,marp-h6){font-size:.9em}div#\:\$p>svg>foreignObject>section :is(h1,marp-h1) strong,div#\:\$p>svg>foreignObject>section :is(h2,marp-h2) strong,div#\:\$p>svg>foreignObject>section :is(h3,marp-h3) strong,div#\:\$p>svg>foreignObject>section :is(h4,marp-h4) strong,div#\:\$p>svg>foreignObject>section :is(h5,marp-h5) strong,div#\:\$p>svg>foreignObject>section :is(h6,marp-h6) strong{color:var(--heading-strong-color);font-weight:inherit}div#\:\$p>svg>foreignObject>section :is(h1,marp-h1)::part(auto-scaling),div#\:\$p>svg>foreignObject>section :is(h2,marp-h2)::part(auto-scaling),div#\:\$p>svg>foreignObject>section :is(h3,marp-h3)::part(auto-scaling),div#\:\$p>svg>foreignObject>section :is(h4,marp-h4)::part(auto-scaling),div#\:\$p>svg>foreignObject>section :is(h5,marp-h5)::part(auto-scaling),div#\:\$p>svg>foreignObject>section :is(h6,marp-h6)::part(auto-scaling){max-height:563px}div#\:\$p>svg>foreignObject>section hr{height:0;padding-top:.25em}div#\:\$p>svg>foreignObject>section :is(pre,marp-pre){border:1px solid var(--color-border-default);line-height:1.15;overflow:visible}div#\:\$p>svg>foreignObject>section :is(pre,marp-pre)::part(auto-scaling){max-height:529px}div#\:\$p>svg>foreignObject>section :is(pre,marp-pre) :where(.hljs){color:var(--color-prettylights-syntax-storage-modifier-import)}div#\:\$p>svg>foreignObject>section :is(pre,marp-pre) :where(.hljs-doctag),div#\:\$p>svg>foreignObject>section :is(pre,marp-pre) :where(.hljs-keyword),div#\:\$p>svg>foreignObject>section :is(pre,marp-pre) :where(.hljs-meta .hljs-keyword),div#\:\$p>svg>foreignObject>section :is(pre,marp-pre) :where(.hljs-template-tag),div#\:\$p>svg>foreignObject>section :is(pre,marp-pre) :where(.hljs-template-variable),div#\:\$p>svg>foreignObject>section :is(pre,marp-pre) :where(.hljs-type),div#\:\$p>svg>foreignObject>section :is(pre,marp-pre) :where(.hljs-variable.language_){color:var(--color-prettylights-syntax-keyword)}div#\:\$p>svg>foreignObject>section :is(pre,marp-pre) :where(.hljs-title),div#\:\$p>svg>foreignObject>section :is(pre,marp-pre) :where(.hljs-title.class_),div#\:\$p>svg>foreignObject>section :is(pre,marp-pre) :where(.hljs-title.class_.inherited__),div#\:\$p>svg>foreignObject>section :is(pre,marp-pre) :where(.hljs-title.function_){color:var(--color-prettylights-syntax-entity)}div#\:\$p>svg>foreignObject>section :is(pre,marp-pre) :where(.hljs-attr),div#\:\$p>svg>foreignObject>section :is(pre,marp-pre) :where(.hljs-attribute),div#\:\$p>svg>foreignObject>section :is(pre,marp-pre) :where(.hljs-literal),div#\:\$p>svg>foreignObject>section :is(pre,marp-pre) :where(.hljs-meta),div#\:\$p>svg>foreignObject>section :is(pre,marp-pre) :where(.hljs-number),div#\:\$p>svg>foreignObject>section :is(pre,marp-pre) :where(.hljs-operator),div#\:\$p>svg>foreignObject>section :is(pre,marp-pre) :where(.hljs-selector-attr),div#\:\$p>svg>foreignObject>section :is(pre,marp-pre) :where(.hljs-selector-class),div#\:\$p>svg>foreignObject>section :is(pre,marp-pre) :where(.hljs-selector-id),div#\:\$p>svg>foreignObject>section :is(pre,marp-pre) :where(.hljs-variable){color:var(--color-prettylights-syntax-constant)}div#\:\$p>svg>foreignObject>section :is(pre,marp-pre) :where(.hljs-meta .hljs-string),div#\:\$p>svg>foreignObject>section :is(pre,marp-pre) :where(.hljs-regexp),div#\:\$p>svg>foreignObject>section :is(pre,marp-pre) :where(.hljs-string){color:var(--color-prettylights-syntax-string)}div#\:\$p>svg>foreignObject>section :is(pre,marp-pre) :where(.hljs-built_in),div#\:\$p>svg>foreignObject>section :is(pre,marp-pre) :where(.hljs-symbol){color:var(--color-prettylights-syntax-variable)}div#\:\$p>svg>foreignObject>section :is(pre,marp-pre) :where(.hljs-code),div#\:\$p>svg>foreignObject>section :is(pre,marp-pre) :where(.hljs-comment),div#\:\$p>svg>foreignObject>section :is(pre,marp-pre) :where(.hljs-formula){color:var(--color-prettylights-syntax-comment)}div#\:\$p>svg>foreignObject>section :is(pre,marp-pre) :where(.hljs-name),div#\:\$p>svg>foreignObject>section :is(pre,marp-pre) :where(.hljs-quote),div#\:\$p>svg>foreignObject>section :is(pre,marp-pre) :where(.hljs-selector-pseudo),div#\:\$p>svg>foreignObject>section :is(pre,marp-pre) :where(.hljs-selector-tag){color:var(--color-prettylights-syntax-entity-tag)}div#\:\$p>svg>foreignObject>section :is(pre,marp-pre) :where(.hljs-subst){color:var(--color-prettylights-syntax-storage-modifier-import)}div#\:\$p>svg>foreignObject>section :is(pre,marp-pre) :where(.hljs-section){color:var(--color-prettylights-syntax-markup-heading);font-weight:700}div#\:\$p>svg>foreignObject>section :is(pre,marp-pre) :where(.hljs-bullet){color:var(--color-prettylights-syntax-markup-list)}div#\:\$p>svg>foreignObject>section :is(pre,marp-pre) :where(.hljs-emphasis){color:var(--color-prettylights-syntax-markup-italic);font-style:italic}div#\:\$p>svg>foreignObject>section :is(pre,marp-pre) :where(.hljs-strong){color:var(--color-prettylights-syntax-markup-bold);font-weight:700}div#\:\$p>svg>foreignObject>section :is(pre,marp-pre) :where(.hljs-addition){background-color:var(--color-prettylights-syntax-markup-inserted-bg);color:var(--color-prettylights-syntax-markup-inserted-text)}div#\:\$p>svg>foreignObject>section :is(pre,marp-pre) :where(.hljs-deletion){background-color:var(--color-prettylights-syntax-markup-deleted-bg);color:var(--color-prettylights-syntax-markup-deleted-text)}div#\:\$p>svg>foreignObject>section footer,div#\:\$p>svg>foreignObject>section header{color:var(--header-footer-color);font-size:18px;left:30px;margin:0;position:absolute}div#\:\$p>svg>foreignObject>section header{top:21px}div#\:\$p>svg>foreignObject>section footer{bottom:21px}div#\:\$p>svg>foreignObject>section{--h1-color:#246;--header-footer-color:hsla(0,0%,40%,.75);--heading-strong-color:#48c;--paginate-color:#777;align-items:stretch;display:flex;flex-flow:column nowrap;font-size:29px;height:720px;justify-content:center;padding:78.5px;width:1280px}div#\:\$p>svg>foreignObject>section{--marpit-root-font-size:29px}div#\:\$p>svg>foreignObject>section:where(.invert){--h1-color:#cee7ff;--header-footer-color:hsla(0,0%,60%,.75);--heading-strong-color:#7bf;--paginate-color:#999}div#\:\$p>svg>foreignObject>section>:last-child,div#\:\$p>svg>foreignObject>section[data-footer]>:nth-last-child(2){margin-bottom:0}div#\:\$p>svg>foreignObject>section>:first-child,div#\:\$p>svg>foreignObject>section>header:first-child+*{margin-top:0}div#\:\$p>svg>foreignObject>section:after{bottom:21px;color:var(--paginate-color);font-size:24px;padding:0;position:absolute;right:30px}div#\:\$p>svg>foreignObject>section:after{--marpit-root-font-size:24px}div#\:\$p>svg>foreignObject>section[data-color] :is(h1,marp-h1),div#\:\$p>svg>foreignObject>section[data-color] :is(h2,marp-h2),div#\:\$p>svg>foreignObject>section[data-color] :is(h3,marp-h3),div#\:\$p>svg>foreignObject>section[data-color] :is(h4,marp-h4),div#\:\$p>svg>foreignObject>section[data-color] :is(h5,marp-h5),div#\:\$p>svg>foreignObject>section[data-color] :is(h6,marp-h6){color:currentcolor}div#\:\$p>svg>foreignObject>:where(section){container-type:size}div#\:\$p>svg>foreignObject>section mjx-container[jax=SVG]{direction:ltr}div#\:\$p>svg>foreignObject>section mjx-container[jax=SVG]>svg{overflow:visible;min-height:1px;min-width:1px}div#\:\$p>svg>foreignObject>section mjx-container[jax=SVG]>svg a{fill:blue;stroke:blue}div#\:\$p>svg>foreignObject>section mjx-container[jax=SVG][display=true]{display:block;text-align:center;margin:1em 0}div#\:\$p>svg>foreignObject>section mjx-container[jax=SVG][display=true][width=full]{display:flex}div#\:\$p>svg>foreignObject>section mjx-container[jax=SVG][justify=left]{text-align:left}div#\:\$p>svg>foreignObject>section mjx-container[jax=SVG][justify=right]{text-align:right}div#\:\$p>svg>foreignObject>section g[data-mml-node=merror]>g{fill:red;stroke:red}div#\:\$p>svg>foreignObject>section g[data-mml-node=merror]>rect[data-background]{fill:yellow;stroke:none}div#\:\$p>svg>foreignObject>section g[data-mml-node=mtable]>line[data-line],div#\:\$p>svg>foreignObject>section svg[data-table]>g>line[data-line]{stroke-width:70px;fill:none}div#\:\$p>svg>foreignObject>section g[data-mml-node=mtable]>rect[data-frame],div#\:\$p>svg>foreignObject>section svg[data-table]>g>rect[data-frame]{stroke-width:70px;fill:none}div#\:\$p>svg>foreignObject>section g[data-mml-node=mtable]>.mjx-dashed,div#\:\$p>svg>foreignObject>section svg[data-table]>g>.mjx-dashed{stroke-dasharray:140}div#\:\$p>svg>foreignObject>section g[data-mml-node=mtable]>.mjx-dotted,div#\:\$p>svg>foreignObject>section svg[data-table]>g>.mjx-dotted{stroke-linecap:round;stroke-dasharray:0,140}div#\:\$p>svg>foreignObject>section g[data-mml-node=mtable]>g>svg{overflow:visible}div#\:\$p>svg>foreignObject>section [jax=SVG] mjx-tool{display:inline-block;position:relative;width:0;height:0}div#\:\$p>svg>foreignObject>section [jax=SVG] mjx-tool>mjx-tip{position:absolute;top:0;left:0}div#\:\$p>svg>foreignObject>section mjx-tool>mjx-tip{display:inline-block;padding:.2em;border:1px solid #888;font-size:70%;background-color:#F8F8F8;color:black;box-shadow:2px 2px 5px #AAAAAA}div#\:\$p>svg>foreignObject>section g[data-mml-node=maction][data-toggle]{cursor:pointer}div#\:\$p>svg>foreignObject>section mjx-status{display:block;position:fixed;left:1em;bottom:1em;min-width:25%;padding:.2em .4em;border:1px solid #888;font-size:90%;background-color:#F8F8F8;color:black}div#\:\$p>svg>foreignObject>section foreignObject[data-mjx-xml]{font-family:initial;line-height:normal;overflow:visible}div#\:\$p>svg>foreignObject>section mjx-container[jax=SVG] path[data-c],div#\:\$p>svg>foreignObject>section mjx-container[jax=SVG] use[data-c]{stroke-width:3}@media print{div#\:\$p>svg>foreignObject>section mjx-container[jax=SVG] path[data-c],div#\:\$p>svg>foreignObject>section mjx-container[jax=SVG] use[data-c]{stroke-width:0}}div#\:\$p>svg>foreignObject>section img[data-marp-twemoji]{background:transparent;height:1em;margin:0 .05em 0 .1em;vertical-align:-.1em;width:1em}
|
|
/* buutti.css */
|
|
/* @theme buutti */div#\:\$p>svg>foreignObject>section .columns{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:calc(var(--marpit-root-font-size, 1rem) * 1)}div#\:\$p>svg>foreignObject>section .columns12{display:grid;grid-template-columns:1fr 2fr;gap:calc(var(--marpit-root-font-size, 1rem) * 1)}div#\:\$p>svg>foreignObject>section .columns21{display:grid;grid-template-columns:2fr 1fr;gap:calc(var(--marpit-root-font-size, 1rem) * 1)}div#\:\$p>svg>foreignObject>section .columns32{display:grid;grid-template-columns:3fr 2fr;gap:calc(var(--marpit-root-font-size, 1rem) * 1)}div#\:\$p>svg>foreignObject>section .columns23{display:grid;grid-template-columns:2fr 3fr;gap:calc(var(--marpit-root-font-size, 1rem) * 1)}div#\:\$p>svg>foreignObject>section .columns111{display:grid;grid-template-columns:1fr 1fr 1fr;gap:calc(var(--marpit-root-font-size, 1rem) * 1)}div#\:\$p>svg>foreignObject>section .centered{display:flex;flex-direction:column;justify-content:center;text-align:center}div#\:\$p>svg>foreignObject>section .tableborderless td,div#\:\$p>svg>foreignObject>section th{border:none!important;border-collapse:collapse}div#\:\$p>svg>foreignObject>section.extra{background-color:#5d275d;background-image:linear-gradient(to bottom,#401a40,#1d0c1d);color:white}div#\:\$p>svg>foreignObject>section.extra a{color:rgb(145,255,209)}div#\:\$p>svg>foreignObject>section.exercise{background-color:#29366f;background-image:linear-gradient(to bottom,#20636a,#173742);color:white}div#\:\$p>svg>foreignObject>section.exercise a{color:rgb(211,173,255)}
|
|
|
|
/* @theme uaut9f7pkna0co5lb2ryww7srs2g53vpacs6y1s30riqh */div#\:\$p>svg>foreignObject>section[data-marpit-advanced-background=background]{columns:initial!important;display:block!important;padding:0!important}div#\:\$p>svg>foreignObject>section[data-marpit-advanced-background=background]:after,div#\:\$p>svg>foreignObject>section[data-marpit-advanced-background=background]:before,div#\:\$p>svg>foreignObject>section[data-marpit-advanced-background=content]:after,div#\:\$p>svg>foreignObject>section[data-marpit-advanced-background=content]:before{display:none!important}div#\:\$p>svg>foreignObject>section[data-marpit-advanced-background=background]>div[data-marpit-advanced-background-container]{all:initial;display:flex;flex-direction:row;height:100%;overflow:hidden;width:100%}div#\:\$p>svg>foreignObject>section[data-marpit-advanced-background=background]>div[data-marpit-advanced-background-container][data-marpit-advanced-background-direction=vertical]{flex-direction:column}div#\:\$p>svg>foreignObject>section[data-marpit-advanced-background=background][data-marpit-advanced-background-split]>div[data-marpit-advanced-background-container]{width:var(--marpit-advanced-background-split,50%)}div#\:\$p>svg>foreignObject>section[data-marpit-advanced-background=background][data-marpit-advanced-background-split=right]>div[data-marpit-advanced-background-container]{margin-left:calc(100% - var(--marpit-advanced-background-split, 50%))}div#\:\$p>svg>foreignObject>section[data-marpit-advanced-background=background]>div[data-marpit-advanced-background-container]>figure{all:initial;background-position:center;background-repeat:no-repeat;background-size:cover;flex:auto;margin:0}div#\:\$p>svg>foreignObject>section[data-marpit-advanced-background=background]>div[data-marpit-advanced-background-container]>figure>figcaption{position:absolute;border:0;clip:rect(0,0,0,0);height:1px;margin:-1px;overflow:hidden;padding:0;white-space:nowrap;width:1px}div#\:\$p>svg>foreignObject>section[data-marpit-advanced-background=content],div#\:\$p>svg>foreignObject>section[data-marpit-advanced-background=pseudo]{background:transparent!important}div#\:\$p>svg>foreignObject>section[data-marpit-advanced-background=pseudo],div#\:\$p>svg[data-marpit-svg]>foreignObject[data-marpit-advanced-background=pseudo]{pointer-events:none!important}div#\:\$p>svg>foreignObject>section[data-marpit-advanced-background-split]{width:100%;height:100%}</style></head><body><div class="bespoke-marp-osc"><button data-bespoke-marp-osc="prev" tabindex="-1" title="Previous slide">Previous slide</button><span data-bespoke-marp-osc="page"></span><button data-bespoke-marp-osc="next" tabindex="-1" title="Next slide">Next slide</button><button data-bespoke-marp-osc="fullscreen" tabindex="-1" title="Toggle fullscreen (f)">Toggle fullscreen</button><button data-bespoke-marp-osc="presenter" tabindex="-1" title="Open presenter view (p)">Open presenter view</button></div><div id=":$p"><svg data-marpit-svg="" viewBox="0 0 1280 720"><foreignObject width="1280" height="720"><section id="1" data-paginate="true" data-class="invert" data-heading-divider="5" data-theme="uaut9f7pkna0co5lb2ryww7srs2g53vpacs6y1s30riqh" lang="en-US" class="invert" data-marpit-pagination="1" style="--paginate:true;--class:invert;--heading-divider:5;--theme:uaut9f7pkna0co5lb2ryww7srs2g53vpacs6y1s30riqh;" data-marpit-pagination-total="59">
|
|
<h1 id="testing">Testing</h1>
|
|
</section>
|
|
</foreignObject></svg><svg data-marpit-svg="" viewBox="0 0 1280 720"><foreignObject width="1280" height="720"><section id="2" data-paginate="true" data-class="invert" data-heading-divider="5" data-theme="uaut9f7pkna0co5lb2ryww7srs2g53vpacs6y1s30riqh" lang="en-US" class="invert" data-marpit-pagination="2" style="--paginate:true;--class:invert;--heading-divider:5;--theme:uaut9f7pkna0co5lb2ryww7srs2g53vpacs6y1s30riqh;" data-marpit-pagination-total="59">
|
|
<h2 id="contents">Contents</h2>
|
|
<ul>
|
|
<li><a href="#introduction-to-testing">Introduction to testing</a></li>
|
|
<li><a href="#c-testing-frameworks">C# Testing Frameworks</a></li>
|
|
<li><a href="#an-extensive-unit-test-example">An extensive unit test example</a></li>
|
|
<li><a href="#writing-testable-code">Writing testable code</a></li>
|
|
<li><a href="#testing-with-postman">Testing with Postman</a></li>
|
|
<li><a href="#cicdct">CI/CD/CT</a></li>
|
|
</ul>
|
|
</section>
|
|
</foreignObject></svg><svg data-marpit-svg="" viewBox="0 0 1280 720"><foreignObject width="1280" height="720"><section id="3" data-paginate="true" data-class="invert" data-heading-divider="5" data-theme="uaut9f7pkna0co5lb2ryww7srs2g53vpacs6y1s30riqh" lang="en-US" class="invert" data-marpit-pagination="3" style="--paginate:true;--class:invert;--heading-divider:5;--theme:uaut9f7pkna0co5lb2ryww7srs2g53vpacs6y1s30riqh;" data-marpit-pagination-total="59">
|
|
<h2 id="introduction-to-testing">Introduction to testing</h2>
|
|
</section>
|
|
</foreignObject></svg><svg data-marpit-svg="" viewBox="0 0 1280 720"><foreignObject width="1280" height="720"><section id="4" data-marpit-fragments="8" data-paginate="true" data-class="invert" data-heading-divider="5" data-theme="uaut9f7pkna0co5lb2ryww7srs2g53vpacs6y1s30riqh" lang="en-US" class="invert" data-marpit-pagination="4" style="--paginate:true;--class:invert;--heading-divider:5;--theme:uaut9f7pkna0co5lb2ryww7srs2g53vpacs6y1s30riqh;" data-marpit-pagination-total="59">
|
|
<h3 id="what-is-testing">What is testing?</h3>
|
|
<ul>
|
|
<li data-marpit-fragment="1">Testing is much more than just trying out manually if the program works!</li>
|
|
<li data-marpit-fragment="2"><a href="https://en.wikipedia.org/wiki/Manual_testing">Manual testing</a>: acting as an end user of the application</li>
|
|
<li data-marpit-fragment="3"><a href="https://en.wikipedia.org/wiki/Test_automation">Automated testing</a>: using software to emulate the end user, or testing the functionality of the code itself</li>
|
|
<li data-marpit-fragment="4">Proving that the program is working as intended during development on a...
|
|
<ul>
|
|
<li data-marpit-fragment="5">...<em><strong>micro level</strong></em>: individual pieces of code, such as methods</li>
|
|
<li data-marpit-fragment="6">...<em><strong>macro level</strong></em>: larger functional components, such as services</li>
|
|
</ul>
|
|
</li>
|
|
<li data-marpit-fragment="7"><em>All possible paths in a piece of code should be tested</em>
|
|
<ul>
|
|
<li data-marpit-fragment="8">i.e., <a href="https://en.wikipedia.org/wiki/Code_coverage">test coverage</a> should be close to 100%</li>
|
|
</ul>
|
|
</li>
|
|
</ul>
|
|
</section>
|
|
</foreignObject></svg><svg data-marpit-svg="" viewBox="0 0 1280 720"><foreignObject width="1280" height="720"><section id="5" data-marpit-fragments="8" data-paginate="true" data-class="invert" data-heading-divider="5" data-theme="uaut9f7pkna0co5lb2ryww7srs2g53vpacs6y1s30riqh" lang="en-US" class="invert" data-marpit-pagination="5" style="--paginate:true;--class:invert;--heading-divider:5;--theme:uaut9f7pkna0co5lb2ryww7srs2g53vpacs6y1s30riqh;" data-marpit-pagination-total="59">
|
|
<h3 id="automated-testing-why">Automated testing: Why?</h3>
|
|
<ul>
|
|
<li data-marpit-fragment="1">Why do automated testing?
|
|
<ul>
|
|
<li data-marpit-fragment="2">Sets ground truths that can be relied on during development:
|
|
<ul>
|
|
<li data-marpit-fragment="3"><em>"With this kind of input, this should happen"</em></li>
|
|
<li data-marpit-fragment="4">(No need to take the programmers word for it)</li>
|
|
</ul>
|
|
</li>
|
|
<li data-marpit-fragment="5">Ensures code functionality before moving on
|
|
<ul>
|
|
<li data-marpit-fragment="6">Ideally finding bugs before starting to debug</li>
|
|
</ul>
|
|
</li>
|
|
<li data-marpit-fragment="7">Guides developers to create more testable and thus more readable, interchangeable and overall better code</li>
|
|
<li data-marpit-fragment="8"><a href="https://en.wikipedia.org/wiki/Test-driven_development">Test-driven development (TDD)</a> is based on writing tests first and code later!</li>
|
|
</ul>
|
|
</li>
|
|
</ul>
|
|
</section>
|
|
</foreignObject></svg><svg data-marpit-svg="" viewBox="0 0 1280 720"><foreignObject width="1280" height="720"><section id="6" data-marpit-fragments="10" data-paginate="true" data-class="invert" data-heading-divider="5" data-theme="uaut9f7pkna0co5lb2ryww7srs2g53vpacs6y1s30riqh" lang="en-US" class="invert" data-marpit-pagination="6" style="--paginate:true;--class:invert;--heading-divider:5;--theme:uaut9f7pkna0co5lb2ryww7srs2g53vpacs6y1s30riqh;" data-marpit-pagination-total="59">
|
|
<h3 id="types-of-automated-testing">Types of automated testing</h3>
|
|
<ul>
|
|
<li data-marpit-fragment="1">Unit testing
|
|
<ul>
|
|
<li data-marpit-fragment="2">Testing individual units (methods, classes) within a project</li>
|
|
<li data-marpit-fragment="3"><em>"input x produces output y"</em></li>
|
|
<li data-marpit-fragment="4">Each unit has to be entirely independent and <em><strong>deterministic</strong></em> for it to be unit testable!
|
|
<ul>
|
|
<li data-marpit-fragment="5">Deterministic: Always the same result with the same input</li>
|
|
</ul>
|
|
</li>
|
|
</ul>
|
|
</li>
|
|
<li data-marpit-fragment="6">Integration testing
|
|
<ul>
|
|
<li data-marpit-fragment="7">Testing complete modules and the interaction between them within a project</li>
|
|
<li data-marpit-fragment="8">Closer to real life scenarios</li>
|
|
</ul>
|
|
</li>
|
|
<li data-marpit-fragment="9">End-to-end (E2E) testing
|
|
<ul>
|
|
<li data-marpit-fragment="10">Testing the whole program by emulating the end user's behaviour</li>
|
|
</ul>
|
|
</li>
|
|
</ul>
|
|
</section>
|
|
</foreignObject></svg><svg data-marpit-svg="" viewBox="0 0 1280 720"><foreignObject width="1280" height="720"><section id="7" data-marpit-fragments="1" data-paginate="true" data-class="invert" data-heading-divider="5" data-theme="uaut9f7pkna0co5lb2ryww7srs2g53vpacs6y1s30riqh" lang="en-US" class="invert" data-marpit-pagination="7" style="--paginate:true;--class:invert;--heading-divider:5;--theme:uaut9f7pkna0co5lb2ryww7srs2g53vpacs6y1s30riqh;" data-marpit-pagination-total="59">
|
|
<h3 id="test-type-comparison">Test type comparison</h3>
|
|
<ul>
|
|
<li data-marpit-fragment="1">Unit tests are faster to write, E2E tests slowest</li>
|
|
</ul>
|
|
<div class='centered'>
|
|
<p><img src="https://microsoft.github.io/code-with-engineering-playbook/automated-testing/cdc-testing/images/testing-pyramid.png" alt="" style="width:800px;" /></p>
|
|
</div>
|
|
</section>
|
|
</foreignObject></svg><svg data-marpit-svg="" viewBox="0 0 1280 720"><foreignObject width="1280" height="720"><section id="8" data-marpit-fragments="6" data-paginate="true" data-class="invert" data-heading-divider="5" data-theme="uaut9f7pkna0co5lb2ryww7srs2g53vpacs6y1s30riqh" lang="en-US" class="invert" data-marpit-pagination="8" style="--paginate:true;--class:invert;--heading-divider:5;--theme:uaut9f7pkna0co5lb2ryww7srs2g53vpacs6y1s30riqh;" data-marpit-pagination-total="59">
|
|
<h2 id="c-testing-frameworks">C# Testing Frameworks</h2>
|
|
<ul>
|
|
<li data-marpit-fragment="1">Three main testing frameworks:
|
|
<ul>
|
|
<li data-marpit-fragment="2"><code>MSTest</code></li>
|
|
<li data-marpit-fragment="3"><code>NUnit</code></li>
|
|
<li data-marpit-fragment="4"><code>XUnit</code></li>
|
|
</ul>
|
|
</li>
|
|
<li data-marpit-fragment="5">You can install these using NuGet, or in the newer versions of Visual Studio you can create a new testing project</li>
|
|
<li data-marpit-fragment="6">NUnit is the most popular of the three, and will be used in this lecture</li>
|
|
</ul>
|
|
</section>
|
|
</foreignObject></svg><svg data-marpit-svg="" viewBox="0 0 1280 720"><foreignObject width="1280" height="720"><section id="9" data-marpit-fragments="4" data-paginate="true" data-class="invert" data-heading-divider="5" data-theme="uaut9f7pkna0co5lb2ryww7srs2g53vpacs6y1s30riqh" lang="en-US" class="invert" data-marpit-pagination="9" style="--paginate:true;--class:invert;--heading-divider:5;--theme:uaut9f7pkna0co5lb2ryww7srs2g53vpacs6y1s30riqh;" data-marpit-pagination-total="59">
|
|
<h3 id="mstest">MSTest</h3>
|
|
<ul>
|
|
<li data-marpit-fragment="1">Built-in to most versions of Visual Studio since 2005</li>
|
|
<li data-marpit-fragment="2">Simple to use, easy structure
|
|
<ul>
|
|
<li data-marpit-fragment="3"><code>TestClass</code> for denoting classes containing unit tests</li>
|
|
<li data-marpit-fragment="4"><code>TestMethod</code> for denoting unit test methods</li>
|
|
</ul>
|
|
</li>
|
|
</ul>
|
|
</section>
|
|
</foreignObject></svg><svg data-marpit-svg="" viewBox="0 0 1280 720"><foreignObject width="1280" height="720"><section id="10" data-marpit-fragments="6" data-paginate="true" data-class="invert" data-heading-divider="5" data-theme="uaut9f7pkna0co5lb2ryww7srs2g53vpacs6y1s30riqh" lang="en-US" class="invert" data-marpit-pagination="10" style="--paginate:true;--class:invert;--heading-divider:5;--theme:uaut9f7pkna0co5lb2ryww7srs2g53vpacs6y1s30riqh;" data-marpit-pagination-total="59">
|
|
<h3 id="nunit">NUnit</h3>
|
|
<ul>
|
|
<li data-marpit-fragment="1">Most popular C# unit testing framework</li>
|
|
<li data-marpit-fragment="2">Very similar structure to MSTest tests
|
|
<ul>
|
|
<li data-marpit-fragment="3">Instead of <code>TestClass</code>, we use <code>TestFixture</code></li>
|
|
<li data-marpit-fragment="4">Instead of <code>TestMethod</code>, we use <code>Test</code> / <code>TestCase</code></li>
|
|
</ul>
|
|
</li>
|
|
<li data-marpit-fragment="5">Has some advantages to MSTest or XUnit tests, for example:
|
|
<ul>
|
|
<li data-marpit-fragment="6">Ability to test a single test method using multiple different arguments</li>
|
|
</ul>
|
|
</li>
|
|
</ul>
|
|
</section>
|
|
</foreignObject></svg><svg data-marpit-svg="" viewBox="0 0 1280 720"><foreignObject width="1280" height="720"><section id="11" data-marpit-fragments="5" data-paginate="true" data-class="invert" data-heading-divider="5" data-theme="uaut9f7pkna0co5lb2ryww7srs2g53vpacs6y1s30riqh" lang="en-US" class="invert" data-marpit-pagination="11" style="--paginate:true;--class:invert;--heading-divider:5;--theme:uaut9f7pkna0co5lb2ryww7srs2g53vpacs6y1s30riqh;" data-marpit-pagination-total="59">
|
|
<h3 id="xunit">XUnit</h3>
|
|
<ul>
|
|
<li data-marpit-fragment="1">Free, open-source unit testing tool for .NET framework and .NET core</li>
|
|
<li data-marpit-fragment="2">Made by the creator of NUnit v2</li>
|
|
<li data-marpit-fragment="3">You can test C#, F#, VB.NET and other .NET languages</li>
|
|
<li data-marpit-fragment="4">Part of the .NET Foundation</li>
|
|
<li data-marpit-fragment="5">Unique and different style</li>
|
|
</ul>
|
|
</section>
|
|
</foreignObject></svg><svg data-marpit-svg="" viewBox="0 0 1280 720"><foreignObject width="1280" height="720"><section id="12" data-marpit-fragments="2" data-paginate="true" data-class="invert" data-heading-divider="5" data-theme="uaut9f7pkna0co5lb2ryww7srs2g53vpacs6y1s30riqh" lang="en-US" class="invert" data-marpit-pagination="12" style="--paginate:true;--class:invert;--heading-divider:5;--theme:uaut9f7pkna0co5lb2ryww7srs2g53vpacs6y1s30riqh;" data-marpit-pagination-total="59">
|
|
<h3 id="note-about-test-case-naming">Note about test case naming</h3>
|
|
<ul>
|
|
<li data-marpit-fragment="1">In C# unit testing, the most commonly followed naming convention for test cases/methods is as follows:<pre is="marp-pre" data-auto-scaling="downscale-only"><code class="language-csharp">FunctionNameThatIsTested_Condition_ShouldDoX
|
|
</code></pre>
|
|
</li>
|
|
<li data-marpit-fragment="2">For example:<pre is="marp-pre" data-auto-scaling="downscale-only"><code class="language-csharp">Fibonacci_WhenNIs0_ShouldReturn0
|
|
</code></pre>
|
|
</li>
|
|
</ul>
|
|
</section>
|
|
</foreignObject></svg><svg data-marpit-svg="" viewBox="0 0 1280 720"><foreignObject width="1280" height="720"><section id="13" data-paginate="true" data-class="invert" data-heading-divider="5" data-theme="uaut9f7pkna0co5lb2ryww7srs2g53vpacs6y1s30riqh" lang="en-US" class="invert" data-marpit-pagination="13" style="--paginate:true;--class:invert;--heading-divider:5;--theme:uaut9f7pkna0co5lb2ryww7srs2g53vpacs6y1s30riqh;" data-marpit-pagination-total="59">
|
|
<h2 id="an-extensive-unit-test-example">An extensive unit test Example</h2>
|
|
</section>
|
|
</foreignObject></svg><svg data-marpit-svg="" viewBox="0 0 1280 720"><foreignObject width="1280" height="720"><section id="14" data-marpit-fragments="2" data-paginate="true" data-class="invert" data-heading-divider="5" data-theme="uaut9f7pkna0co5lb2ryww7srs2g53vpacs6y1s30riqh" lang="en-US" class="invert" data-marpit-pagination="14" style="--paginate:true;--class:invert;--heading-divider:5;--theme:uaut9f7pkna0co5lb2ryww7srs2g53vpacs6y1s30riqh;" data-marpit-pagination-total="59">
|
|
<h3 id="creating-a-solution--sample-project">Creating a solution & sample project</h3>
|
|
<p>First, we need a solution and a sample project.</p>
|
|
<div class='columns23' markdown='1'>
|
|
<div markdown='1'>
|
|
<ol>
|
|
<li data-marpit-fragment="1">In Visual Studio, create a <em><strong>Class library</strong></em> that targets <em><strong>.NET Standard</strong></em>.</li>
|
|
<li data-marpit-fragment="2">Name the project <code>Fibonacci.Library</code> and the solution <code>Fibonacci</code>. Remember to uncheck <em>Place solution and project in the same directory</em> if it isn't unchecked already.</li>
|
|
</ol>
|
|
</div>
|
|
<div markdown='1'>
|
|
<p><img src="imgs/9-testing_1.png" alt="" /></p>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</foreignObject></svg><svg data-marpit-svg="" viewBox="0 0 1280 720"><foreignObject width="1280" height="720"><section id="15" data-marpit-fragments="4" data-paginate="true" data-class="invert" data-heading-divider="5" data-theme="uaut9f7pkna0co5lb2ryww7srs2g53vpacs6y1s30riqh" lang="en-US" class="invert" data-marpit-pagination="15" style="--paginate:true;--class:invert;--heading-divider:5;--theme:uaut9f7pkna0co5lb2ryww7srs2g53vpacs6y1s30riqh;" data-marpit-pagination-total="59">
|
|
<h3 id="adding-the-function-to-be-tested">Adding the function to be tested</h3>
|
|
<div class='columns' markdown='1'>
|
|
<div markdown='1'>
|
|
<ol start="3">
|
|
<li data-marpit-fragment="1">Rename the default <code>Class1.cs</code> file to <code>Fibonacci.cs</code>.</li>
|
|
<li data-marpit-fragment="2">Allow Visual Studio to also rename the class itself to <code>Fibonacci</code>.
|
|
<ul>
|
|
<li data-marpit-fragment="3">Make the class <code>public</code> and <code>static</code>.</li>
|
|
</ul>
|
|
</li>
|
|
<li data-marpit-fragment="4">Add a <em>recursive fibonacci function</em> shown here.</li>
|
|
</ol>
|
|
</div>
|
|
<div markdown='1'>
|
|
<pre is="marp-pre" data-auto-scaling="downscale-only"><code class="language-csharp"><span class="hljs-keyword">using</span> System;
|
|
|
|
<span class="hljs-keyword">namespace</span> <span class="hljs-title">Fibonacci.Library</span>
|
|
{
|
|
<span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">class</span> <span class="hljs-title">Fibonacci</span>
|
|
{
|
|
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-built_in">long</span> <span class="hljs-title">Recursive</span>(<span class="hljs-params">
|
|
<span class="hljs-built_in">int</span> n,
|
|
<span class="hljs-built_in">long</span> previous = <span class="hljs-number">0</span>,
|
|
<span class="hljs-built_in">long</span> current = <span class="hljs-number">0</span>,
|
|
<span class="hljs-built_in">int</span> counter = <span class="hljs-number">0</span>
|
|
</span>)</span>
|
|
{
|
|
<span class="hljs-keyword">return</span> counter == n ?
|
|
previous + current :
|
|
Recursive(
|
|
n,
|
|
current,
|
|
Math.Max(previous + current, <span class="hljs-number">1</span>),
|
|
counter + <span class="hljs-number">1</span>
|
|
);
|
|
}
|
|
}
|
|
}
|
|
</code></pre>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</foreignObject></svg><svg data-marpit-svg="" viewBox="0 0 1280 720"><foreignObject width="1280" height="720"><section id="16" data-marpit-fragments="3" data-paginate="true" data-class="invert" data-heading-divider="5" data-theme="uaut9f7pkna0co5lb2ryww7srs2g53vpacs6y1s30riqh" lang="en-US" class="invert" data-marpit-pagination="16" style="--paginate:true;--class:invert;--heading-divider:5;--theme:uaut9f7pkna0co5lb2ryww7srs2g53vpacs6y1s30riqh;" data-marpit-pagination-total="59">
|
|
<h3 id="adding-a-testing-project">Adding a testing project</h3>
|
|
<ul>
|
|
<li data-marpit-fragment="1">Now we need to add a testing project to our solution.</li>
|
|
</ul>
|
|
<ol start="6">
|
|
<li data-marpit-fragment="2">Right click your solution, and add a new project. Select the NUnit project for <strong><em>.NET Core</em></strong>, and call it <code>Fibonacci.UnitTests</code>.</li>
|
|
<li data-marpit-fragment="3">Rename the created class file to <code>Fibonacci_Tests.cs</code>, and the class to <code>Fibonacci_Tests</code></li>
|
|
</ol>
|
|
</section>
|
|
</foreignObject></svg><svg data-marpit-svg="" viewBox="0 0 1280 720"><foreignObject width="1280" height="720"><section id="17" data-marpit-fragments="3" data-paginate="true" data-class="invert" data-heading-divider="5" data-theme="uaut9f7pkna0co5lb2ryww7srs2g53vpacs6y1s30riqh" lang="en-US" class="invert" data-marpit-pagination="17" style="--paginate:true;--class:invert;--heading-divider:5;--theme:uaut9f7pkna0co5lb2ryww7srs2g53vpacs6y1s30riqh;" data-marpit-pagination-total="59">
|
|
<h3 id="linking-the-testing-project">Linking the testing project</h3>
|
|
<ol start="8">
|
|
<li data-marpit-fragment="1">We then need to link the <code>Fibonacci.Library</code> project to <code>Fibonacci.UnitTests</code>.</li>
|
|
<li data-marpit-fragment="2">Right click <code>Fibonacci.UnitTests</code>, and select <em>Add > Reference...</em>, and under <em>Projects</em> select <code>Fibonacci.Library</code>. Press OK.</li>
|
|
</ol>
|
|
<ul>
|
|
<li data-marpit-fragment="3">Now we have linked <code>Fibonacci.Library</code> to <code>Fibonacci.UnitTests</code>, and we can begin testing our <code>Recursive</code> function in the <code>Fibonacci</code> class.</li>
|
|
</ul>
|
|
</section>
|
|
</foreignObject></svg><svg data-marpit-svg="" viewBox="0 0 1280 720"><foreignObject width="1280" height="720"><section id="18" data-marpit-fragments="2" data-paginate="true" data-class="invert" data-heading-divider="5" data-theme="uaut9f7pkna0co5lb2ryww7srs2g53vpacs6y1s30riqh" lang="en-US" class="invert" data-marpit-pagination="18" style="--paginate:true;--class:invert;--heading-divider:5;--theme:uaut9f7pkna0co5lb2ryww7srs2g53vpacs6y1s30riqh;" data-marpit-pagination-total="59">
|
|
<h3 id="importing-the-class">Importing the class</h3>
|
|
<ol start="10">
|
|
<li data-marpit-fragment="1">Since we have many things called "Fibonacci", we have to import our <code>Fibonacci</code> class by using an alias
|
|
<ul>
|
|
<li data-marpit-fragment="2">Put this to the top of your file <code>Fibonacci_Tests.cs</code>, underneath<br /><code>using NUnit.Framework;</code><pre is="marp-pre" data-auto-scaling="downscale-only"><code class="language-csharp"><span class="hljs-keyword">using</span> FibonacciLib = Fibonacci.Library.Fibonacci;
|
|
</code></pre>
|
|
</li>
|
|
</ul>
|
|
</li>
|
|
</ol>
|
|
</section>
|
|
</foreignObject></svg><svg data-marpit-svg="" viewBox="0 0 1280 720"><foreignObject width="1280" height="720"><section id="19" data-marpit-fragments="2" data-paginate="true" data-class="invert" data-heading-divider="5" data-theme="uaut9f7pkna0co5lb2ryww7srs2g53vpacs6y1s30riqh" lang="en-US" class="invert" data-marpit-pagination="19" style="--paginate:true;--class:invert;--heading-divider:5;--theme:uaut9f7pkna0co5lb2ryww7srs2g53vpacs6y1s30riqh;" data-marpit-pagination-total="59">
|
|
<h3 id="test-fixture-for-a-class">Test fixture for a class</h3>
|
|
<ol start="11">
|
|
<li data-marpit-fragment="1">Now, add <code>[TestFixture]</code> on top of our class, like so:</li>
|
|
</ol>
|
|
<pre is="marp-pre" data-auto-scaling="downscale-only"><code class="language-csharp"><span class="hljs-keyword">using</span> NUnit.Framework;
|
|
<span class="hljs-keyword">using</span> FibonacciLib = Fibonacci.Library.Fibonacci;
|
|
|
|
<span class="hljs-keyword">namespace</span> <span class="hljs-title">Fibonacci.UnitTests</span> {
|
|
[<span class="hljs-meta">TestFixture</span>]
|
|
<span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title">Fibonacci_Tests</span> {
|
|
</code></pre>
|
|
<ul>
|
|
<li data-marpit-fragment="2">This denotes that this class contains our unit tests.</li>
|
|
</ul>
|
|
</section>
|
|
</foreignObject></svg><svg data-marpit-svg="" viewBox="0 0 1280 720"><foreignObject width="1280" height="720"><section id="20" data-marpit-fragments="1" data-paginate="true" data-class="invert" data-heading-divider="5" data-theme="uaut9f7pkna0co5lb2ryww7srs2g53vpacs6y1s30riqh" lang="en-US" class="invert" data-marpit-pagination="20" style="--paginate:true;--class:invert;--heading-divider:5;--theme:uaut9f7pkna0co5lb2ryww7srs2g53vpacs6y1s30riqh;" data-marpit-pagination-total="59">
|
|
<h3 id="adding-the-unit-test-method">Adding the unit test method</h3>
|
|
<ol start="12">
|
|
<li data-marpit-fragment="1">Then, inside our class <code>Fibonacci_Tests</code>, let's create our first method to test the <code>Recursive</code> function, as shown here:<pre is="marp-pre" data-auto-scaling="downscale-only"><code class="language-csharp"><span class="hljs-keyword">using</span> NUnit.Framework;
|
|
<span class="hljs-keyword">using</span> FibonacciLib = Fibonacci.Library.Fibonacci;
|
|
|
|
<span class="hljs-keyword">namespace</span> <span class="hljs-title">Fibonacci.UnitTests</span> {
|
|
[<span class="hljs-meta">TestFixture</span>]
|
|
<span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title">Fibonacci_Tests</span> {
|
|
[<span class="hljs-meta">Test</span>]
|
|
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">Recursive_WhenNIs0_ShouldReturn0</span>()</span>
|
|
=> Assert.That(<span class="hljs-number">0</span>, Is.EqualTo(FibonacciLib.Recursive(n: <span class="hljs-number">0</span>)));
|
|
}
|
|
}
|
|
</code></pre>
|
|
</li>
|
|
</ol>
|
|
</section>
|
|
</foreignObject></svg><svg data-marpit-svg="" viewBox="0 0 1280 720"><foreignObject width="1280" height="720"><section id="21" data-marpit-fragments="5" data-paginate="true" data-class="invert" data-heading-divider="5" data-theme="uaut9f7pkna0co5lb2ryww7srs2g53vpacs6y1s30riqh" lang="en-US" class="invert" data-marpit-pagination="21" style="--paginate:true;--class:invert;--heading-divider:5;--theme:uaut9f7pkna0co5lb2ryww7srs2g53vpacs6y1s30riqh;" data-marpit-pagination-total="59">
|
|
<h3 id="things-to-take-into-consideration">Things to take into consideration</h3>
|
|
<ul>
|
|
<li data-marpit-fragment="1">When passing in function arguments, to avoid <em><strong>magic numbers</strong></em> and to improve code readability, use the syntax <code>argumentName: value</code>
|
|
<ul>
|
|
<li data-marpit-fragment="2">Magic numbers = using numbers as input as-is without explanation, for example without assigning to a well-named variable, or without using a descriptive type</li>
|
|
</ul>
|
|
</li>
|
|
<li data-marpit-fragment="3"><code>Assert.That</code> is one of many functions that can determine whether the test passes
|
|
<ul>
|
|
<li data-marpit-fragment="4">Its first argument is the <em><strong>expected value</strong></em>. In the second argument you give a <em><strong>constraint</strong></em> for the actual output, i.e., value is equal to, greater than, starts with, <a href="https://docs.nunit.org/articles/nunit/writing-tests/constraints/Constraints.html">etc</a>.</li>
|
|
<li data-marpit-fragment="5"><em>Do not mix the arguments up!</em> If you do, the errors you get do not make any sense.</li>
|
|
</ul>
|
|
</li>
|
|
</ul>
|
|
</section>
|
|
</foreignObject></svg><svg data-marpit-svg="" viewBox="0 0 1280 720"><foreignObject width="1280" height="720"><section id="22" data-marpit-fragments="4" data-paginate="true" data-class="invert" data-heading-divider="5" data-theme="uaut9f7pkna0co5lb2ryww7srs2g53vpacs6y1s30riqh" lang="en-US" class="invert" data-marpit-pagination="22" style="--paginate:true;--class:invert;--heading-divider:5;--theme:uaut9f7pkna0co5lb2ryww7srs2g53vpacs6y1s30riqh;" data-marpit-pagination-total="59">
|
|
<h3 id="running-tests">Running Tests</h3>
|
|
<ul>
|
|
<li data-marpit-fragment="1">
|
|
<p>Now, let's open the built-in test runner in Visual Studio to run our tests.</p>
|
|
</li>
|
|
<li data-marpit-fragment="2">
|
|
<p>On the top of Visual Studio, click <em>Test</em>, then <em>Test Explorer</em>.</p>
|
|
</li>
|
|
<li data-marpit-fragment="3">
|
|
<p>In the Test Explorer, you should see the test case we created. If not, build the solution, and make sure there are no errors.</p>
|
|
</li>
|
|
<li data-marpit-fragment="4">
|
|
<p>Either way, let's run our test case. Press <em>Run All Tests</em> in the top-left corner of Test Explorer. You should see our test case <em><strong>passing</strong></em>.</p>
|
|
</li>
|
|
</ul>
|
|
</section>
|
|
</foreignObject></svg><svg data-marpit-svg="" viewBox="0 0 1280 720"><foreignObject width="1280" height="720"><section id="23" data-marpit-fragments="4" data-paginate="true" data-class="invert" data-heading-divider="5" data-theme="uaut9f7pkna0co5lb2ryww7srs2g53vpacs6y1s30riqh" lang="en-US" class="invert" data-marpit-pagination="23" style="--paginate:true;--class:invert;--heading-divider:5;--theme:uaut9f7pkna0co5lb2ryww7srs2g53vpacs6y1s30riqh;" data-marpit-pagination-total="59">
|
|
<h3 id="adding-more-test-cases">Adding More Test Cases</h3>
|
|
<ul>
|
|
<li data-marpit-fragment="1">While there's nothing inherently wrong about our test case, it doesn't cover much of anything.</li>
|
|
<li data-marpit-fragment="2">We need to add more test cases to test different values and outputs.</li>
|
|
<li data-marpit-fragment="3">In other testing frameworks, you would do this by creating more methods.</li>
|
|
<li data-marpit-fragment="4">But with <code>NUnit</code>, we can use our <em>existing</em> method.</li>
|
|
</ul>
|
|
</section>
|
|
</foreignObject></svg><svg data-marpit-svg="" viewBox="0 0 1280 720"><foreignObject width="1280" height="720"><section id="24" data-marpit-fragments="2" data-paginate="true" data-class="invert" data-heading-divider="5" data-theme="uaut9f7pkna0co5lb2ryww7srs2g53vpacs6y1s30riqh" lang="en-US" class="invert" data-marpit-pagination="24" style="--paginate:true;--class:invert;--heading-divider:5;--theme:uaut9f7pkna0co5lb2ryww7srs2g53vpacs6y1s30riqh;" data-marpit-pagination-total="59">
|
|
<h3 id="adding-a-new-test">Adding a new test</h3>
|
|
<ol>
|
|
<li data-marpit-fragment="1">Do the following modifications to our test method:<pre is="marp-pre" data-auto-scaling="downscale-only"><code class="language-csharp">[<span class="hljs-meta">TestCase(0, 0)</span>]
|
|
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">Recursive_Always_ShouldReturnNthFibonacci</span>(<span class="hljs-params">
|
|
<span class="hljs-built_in">int</span> n,
|
|
<span class="hljs-built_in">long</span> expected
|
|
</span>)</span>
|
|
=> Assert.That(expected, Is.EqualTo(FibonacciLib.Recursive(n)));
|
|
</code></pre>
|
|
</li>
|
|
</ol>
|
|
<ul>
|
|
<li data-marpit-fragment="2">Using <code>TestCase</code> instead of <code>Test</code>, we can pass in arguments to our test case method, and we can use those passed in arguments in our actual tests.</li>
|
|
</ul>
|
|
</section>
|
|
</foreignObject></svg><svg data-marpit-svg="" viewBox="0 0 1280 720"><foreignObject width="1280" height="720"><section id="25" data-marpit-fragments="4" data-paginate="true" data-class="invert" data-heading-divider="5" data-theme="uaut9f7pkna0co5lb2ryww7srs2g53vpacs6y1s30riqh" lang="en-US" class="invert" data-marpit-pagination="25" style="--paginate:true;--class:invert;--heading-divider:5;--theme:uaut9f7pkna0co5lb2ryww7srs2g53vpacs6y1s30riqh;" data-marpit-pagination-total="59">
|
|
<h3 id="adding-more-tests">Adding more tests</h3>
|
|
<ol start="2">
|
|
<li data-marpit-fragment="1">
|
|
<p>Run the test cases again, and they should <em><strong>pass</strong></em>.</p>
|
|
<ul>
|
|
<li data-marpit-fragment="2">But as you might notice, we haven't really changed anything yet.</li>
|
|
</ul>
|
|
</li>
|
|
<li data-marpit-fragment="3">
|
|
<p>Let's add more test cases, like so:</p>
|
|
<pre is="marp-pre" data-auto-scaling="downscale-only"><code class="language-csharp">[<span class="hljs-meta">TestCase(0, 0)</span>]
|
|
[<span class="hljs-meta">TestCase(1, 1)</span>]
|
|
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">Recursive_Always_ShouldReturnNthFibonacci</span>(<span class="hljs-params">
|
|
</span></span></code></pre>
|
|
</li>
|
|
<li data-marpit-fragment="4">
|
|
<p>Now we have 2 test cases with only one test method. Run the tests again, and both cases should <em><strong>pass</strong></em>.</p>
|
|
</li>
|
|
</ol>
|
|
</section>
|
|
</foreignObject></svg><svg data-marpit-svg="" viewBox="0 0 1280 720"><foreignObject width="1280" height="720"><section id="26" data-marpit-fragments="4" data-paginate="true" data-class="invert" data-heading-divider="5" data-theme="uaut9f7pkna0co5lb2ryww7srs2g53vpacs6y1s30riqh" lang="en-US" class="invert" data-marpit-pagination="26" style="--paginate:true;--class:invert;--heading-divider:5;--theme:uaut9f7pkna0co5lb2ryww7srs2g53vpacs6y1s30riqh;" data-marpit-pagination-total="59">
|
|
<h3 id="adding-a-failing-test">Adding a failing test</h3>
|
|
<ol start="5">
|
|
<li data-marpit-fragment="1">Let's add one more TestCase, like so:<pre is="marp-pre" data-auto-scaling="downscale-only"><code class="language-csharp">[<span class="hljs-meta">TestCase(2, 1)</span>]
|
|
</code></pre>
|
|
</li>
|
|
<li data-marpit-fragment="2">Run the tests again, and you should see the test case <em><strong>fail</strong></em>.
|
|
<ul>
|
|
<li data-marpit-fragment="3">This is to be expected: the 2nd fibonacci number should be 1, but the actual value we got from our function was 2.</li>
|
|
<li data-marpit-fragment="4">This means our function does not work as expected.</li>
|
|
</ul>
|
|
</li>
|
|
</ol>
|
|
</section>
|
|
</foreignObject></svg><svg data-marpit-svg="" viewBox="0 0 1280 720"><foreignObject width="1280" height="720"><section id="27" data-marpit-fragments="4" data-paginate="true" data-class="invert" data-heading-divider="5" data-theme="uaut9f7pkna0co5lb2ryww7srs2g53vpacs6y1s30riqh" lang="en-US" class="invert" data-marpit-pagination="27" style="--paginate:true;--class:invert;--heading-divider:5;--theme:uaut9f7pkna0co5lb2ryww7srs2g53vpacs6y1s30riqh;" data-marpit-pagination-total="59">
|
|
<h3 id="fixing-our-code">Fixing our code</h3>
|
|
<div class='columns' markdown='1'>
|
|
<div markdown='1'>
|
|
<ol start="7">
|
|
<li data-marpit-fragment="1">Let's fix our function, as seen here.</li>
|
|
<li data-marpit-fragment="2">Run the tests again. You should see all <em><strong>pass</strong></em> now.</li>
|
|
<li data-marpit-fragment="3">Add a few more test cases
|
|
<ul>
|
|
<li data-marpit-fragment="4">(If you don't know the fibonacci sequence, you can check it from the internet.)</li>
|
|
</ul>
|
|
</li>
|
|
</ol>
|
|
</div>
|
|
<div markdown='1'>
|
|
<pre is="marp-pre" data-auto-scaling="downscale-only"><code class="language-csharp"><span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-built_in">long</span> <span class="hljs-title">Recursive</span>(<span class="hljs-params">
|
|
<span class="hljs-built_in">int</span> n,
|
|
<span class="hljs-built_in">long</span> previous = <span class="hljs-number">0</span>,
|
|
<span class="hljs-built_in">long</span> current = <span class="hljs-number">0</span>,
|
|
<span class="hljs-built_in">int</span> counter = <span class="hljs-number">0</span>
|
|
</span>)</span>
|
|
{
|
|
<span class="hljs-keyword">return</span> counter == n ?
|
|
current : <span class="hljs-comment">// changed from previous + current</span>
|
|
Recursive(
|
|
n,
|
|
current,
|
|
Math.Max(previous + current, <span class="hljs-number">1</span>),
|
|
counter + <span class="hljs-number">1</span>
|
|
);
|
|
}
|
|
</code></pre>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</foreignObject></svg><svg data-marpit-svg="" viewBox="0 0 1280 720"><foreignObject width="1280" height="720"><section id="28" data-marpit-fragments="7" data-paginate="true" data-class="exercise invert" data-heading-divider="5" data-theme="uaut9f7pkna0co5lb2ryww7srs2g53vpacs6y1s30riqh" lang="en-US" class="exercise invert" data-marpit-pagination="28" style="--paginate:true;--class:exercise invert;--heading-divider:5;--theme:uaut9f7pkna0co5lb2ryww7srs2g53vpacs6y1s30riqh;" data-marpit-pagination-total="59">
|
|
<h3 id="exercise-1-unit-testing-in-practice">Exercise 1: Unit testing in practice</h3>
|
|
|
|
<p>Answer the following questions:</p>
|
|
<ol>
|
|
<li data-marpit-fragment="1">How many test cases should we ideally have to test our <code>Recursive</code> function?</li>
|
|
<li data-marpit-fragment="2">What happens if we pass in <code>n = int.MaxValue (2147483647)</code>?
|
|
<ul>
|
|
<li data-marpit-fragment="3">What about other huge numbers?</li>
|
|
<li data-marpit-fragment="4">How would you fix the function so it works as expected?</li>
|
|
</ul>
|
|
</li>
|
|
<li data-marpit-fragment="5">What happens if we pass in <code>n = -1</code>?
|
|
<ul>
|
|
<li data-marpit-fragment="6">What about other negative numbers?</li>
|
|
<li data-marpit-fragment="7">How would you fix the function so it works as expected?</li>
|
|
</ul>
|
|
</li>
|
|
</ol>
|
|
</section>
|
|
</foreignObject></svg><svg data-marpit-svg="" viewBox="0 0 1280 720"><foreignObject width="1280" height="720"><section id="29" data-marpit-fragments="5" data-paginate="true" data-class="exercise invert" data-heading-divider="5" data-theme="uaut9f7pkna0co5lb2ryww7srs2g53vpacs6y1s30riqh" lang="en-US" class="exercise invert" data-marpit-pagination="29" style="--paginate:true;--class:exercise invert;--heading-divider:5;--theme:uaut9f7pkna0co5lb2ryww7srs2g53vpacs6y1s30riqh;" data-marpit-pagination-total="59">
|
|
<h3 id="answers-to-exercise-1-unit-testing-in-practice">Answers to Exercise 1: Unit testing in practice</h3>
|
|
|
|
<ol>
|
|
<li data-marpit-fragment="1">We should cover all the edge cases and code paths with our test cases. We do not need to have many test cases for the same code path.</li>
|
|
<li data-marpit-fragment="2"><code>Stack Overflow</code>, <code>Int64 Overflow</code>, or the test case just won't run.
|
|
<ul>
|
|
<li data-marpit-fragment="3">We should have a maximum allowed value for <code>n</code>, throw the <em><strong>argument out of range</strong></em> exception if above</li>
|
|
</ul>
|
|
</li>
|
|
<li data-marpit-fragment="4"><code>Stack Overflow</code>, since <code>current</code> can never equal <code>n</code> that is less than <code>0</code>
|
|
<ul>
|
|
<li data-marpit-fragment="5">We should have a minimum allowed value for <code>n</code>, throw the <em><strong>argument out of range</strong></em> exception if below</li>
|
|
</ul>
|
|
</li>
|
|
</ol>
|
|
</section>
|
|
</foreignObject></svg><svg data-marpit-svg="" viewBox="0 0 1280 720"><foreignObject width="1280" height="720"><section id="30" data-marpit-fragments="3" data-paginate="true" data-class="invert" data-heading-divider="5" data-theme="uaut9f7pkna0co5lb2ryww7srs2g53vpacs6y1s30riqh" lang="en-US" class="invert" data-marpit-pagination="30" style="--paginate:true;--class:invert;--heading-divider:5;--theme:uaut9f7pkna0co5lb2ryww7srs2g53vpacs6y1s30riqh;" data-marpit-pagination-total="59">
|
|
<h3 id="testing-exceptions">Testing exceptions</h3>
|
|
<ul>
|
|
<li data-marpit-fragment="1">To expect exceptions to be thrown in test cases, we can use the method <code>Assert.Throws</code></li>
|
|
<li data-marpit-fragment="2">Syntax is as follows:<pre is="marp-pre" data-auto-scaling="downscale-only"><code class="language-csharp">Assert.Throws<ExceptionType>(<span class="hljs-built_in">delegate</span>);
|
|
</code></pre>
|
|
</li>
|
|
<li data-marpit-fragment="3">In practice:<pre is="marp-pre" data-auto-scaling="downscale-only"><code class="language-csharp">Assert.Throws<NullReferenceException>(() => TestedFunction(n));
|
|
</code></pre>
|
|
</li>
|
|
</ul>
|
|
</section>
|
|
</foreignObject></svg><svg data-marpit-svg="" viewBox="0 0 1280 720"><foreignObject width="1280" height="720"><section id="31" data-marpit-fragments="2" data-paginate="true" data-class="invert" data-heading-divider="5" data-theme="uaut9f7pkna0co5lb2ryww7srs2g53vpacs6y1s30riqh" lang="en-US" class="invert" data-marpit-pagination="31" style="--paginate:true;--class:invert;--heading-divider:5;--theme:uaut9f7pkna0co5lb2ryww7srs2g53vpacs6y1s30riqh;" data-marpit-pagination-total="59">
|
|
<h3 id="handling-border-cases">Handling border cases</h3>
|
|
<div class='columns' markdown='1'>
|
|
<div markdown='1'>
|
|
<ul>
|
|
<li data-marpit-fragment="1">Continuing our Fibonacci test case, we need to add handling for <code>n</code> that is less than <code>0</code> or more than a set amount. Then we need to add another test method and two test cases to test our new functionality.</li>
|
|
</ul>
|
|
<ol>
|
|
<li data-marpit-fragment="2">Let's start with modifying our existing <code>Recursive</code> function, as shown here:</li>
|
|
</ol>
|
|
</div>
|
|
<div markdown='1'>
|
|
<pre is="marp-pre" data-auto-scaling="downscale-only"><code class="language-csharp"><span class="hljs-keyword">public</span> <span class="hljs-keyword">const</span> <span class="hljs-built_in">int</span> MinDepth = <span class="hljs-number">0</span>;
|
|
<span class="hljs-keyword">public</span> <span class="hljs-keyword">const</span> <span class="hljs-built_in">int</span> MaxDepth = <span class="hljs-number">10000</span>;
|
|
|
|
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-built_in">long</span> <span class="hljs-title">Recursive</span>(<span class="hljs-params">
|
|
<span class="hljs-built_in">int</span> n,
|
|
<span class="hljs-built_in">long</span> previous = <span class="hljs-number">0</span>,
|
|
<span class="hljs-built_in">long</span> current = <span class="hljs-number">0</span>,
|
|
<span class="hljs-built_in">int</span> counter = <span class="hljs-number">0</span>
|
|
</span>)</span>
|
|
{
|
|
<span class="hljs-keyword">if</span> (n < MinDepth || n > MaxDepth)
|
|
<span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> ArgumentOutOfRangeException(
|
|
<span class="hljs-string">"n is out of bounds!"</span>
|
|
);
|
|
|
|
<span class="hljs-keyword">return</span> counter == n ?
|
|
current :
|
|
Recursive(
|
|
n,
|
|
current,
|
|
Math.Max(previous + current, <span class="hljs-number">1</span>),
|
|
counter + <span class="hljs-number">1</span>
|
|
);
|
|
}
|
|
</code></pre>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</foreignObject></svg><svg data-marpit-svg="" viewBox="0 0 1280 720"><foreignObject width="1280" height="720"><section id="32" data-marpit-fragments="3" data-paginate="true" data-class="invert" data-heading-divider="5" data-theme="uaut9f7pkna0co5lb2ryww7srs2g53vpacs6y1s30riqh" lang="en-US" class="invert" data-marpit-pagination="32" style="--paginate:true;--class:invert;--heading-divider:5;--theme:uaut9f7pkna0co5lb2ryww7srs2g53vpacs6y1s30riqh;" data-marpit-pagination-total="59">
|
|
<h3 id="testing-that-everything-works">Testing that everything works</h3>
|
|
<ul>
|
|
<li data-marpit-fragment="1">Now our <code>Recursive</code> function throws an <code>ArgumentOutOfRange</code> exception when <code>n</code> is less than <code>0</code> or more than <code>10000</code>.</li>
|
|
</ul>
|
|
<ol start="2">
|
|
<li data-marpit-fragment="2">
|
|
<p>Let's test that everything works as expected by adding a new test method and a few test cases:</p>
|
|
<pre is="marp-pre" data-auto-scaling="downscale-only"><code class="language-csharp">[<span class="hljs-meta">TestCase(int.MaxValue)</span>]
|
|
[<span class="hljs-meta">TestCase(-1)</span>]
|
|
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">Recursive_WhenNIsOutOfBounds_ShouldThrowArgumentOutOfRange</span>(<span class="hljs-params">
|
|
<span class="hljs-built_in">int</span> n
|
|
</span>)</span>
|
|
=> Assert.Throws<ArgumentOutOfRangeException>(() => FibonacciLib.Recursive(n));
|
|
</code></pre>
|
|
</li>
|
|
<li data-marpit-fragment="3">
|
|
<p>Rename the other method to <code>Recursive_WhenNIsInBounds_ShouldReturnNthFibonacci</code></p>
|
|
</li>
|
|
</ol>
|
|
</section>
|
|
</foreignObject></svg><svg data-marpit-svg="" viewBox="0 0 1280 720"><foreignObject width="1280" height="720"><section id="33" data-marpit-fragments="4" data-paginate="true" data-class="invert" data-heading-divider="5" data-theme="uaut9f7pkna0co5lb2ryww7srs2g53vpacs6y1s30riqh" lang="en-US" class="invert" data-marpit-pagination="33" style="--paginate:true;--class:invert;--heading-divider:5;--theme:uaut9f7pkna0co5lb2ryww7srs2g53vpacs6y1s30riqh;" data-marpit-pagination-total="59">
|
|
<h3 id="unnecessary-arguments">Unnecessary arguments</h3>
|
|
<ul>
|
|
<li data-marpit-fragment="1">We have one more problem:</li>
|
|
<li data-marpit-fragment="2">Our <code>Recursive</code> function is a <code>public</code> method that has in total 4 arguments it takes, yet we only test 1 argument (<code>n</code>).</li>
|
|
<li data-marpit-fragment="3">In order for our tests to be complete, we need to test all the arguments a <code>public</code> method takes in.</li>
|
|
<li data-marpit-fragment="4">So, how do we fix this?</li>
|
|
</ul>
|
|
</section>
|
|
</foreignObject></svg><svg data-marpit-svg="" viewBox="0 0 1280 720"><foreignObject width="1280" height="720"><section id="34" data-marpit-fragments="3" data-paginate="true" data-class="invert" data-heading-divider="5" data-theme="uaut9f7pkna0co5lb2ryww7srs2g53vpacs6y1s30riqh" lang="en-US" class="invert" data-marpit-pagination="34" style="--paginate:true;--class:invert;--heading-divider:5;--theme:uaut9f7pkna0co5lb2ryww7srs2g53vpacs6y1s30riqh;" data-marpit-pagination-total="59">
|
|
<h3 id="removing-unnecessary-arguments">Removing unnecessary arguments</h3>
|
|
<div class='columns' markdown='1'>
|
|
<div markdown='1'>
|
|
<ul>
|
|
<li data-marpit-fragment="1">The naive way would be to test all the arguments with more test cases.</li>
|
|
<li data-marpit-fragment="2">It's important to realize that <em><strong>we don't have to</strong></em>.</li>
|
|
<li data-marpit-fragment="3">Since we never pass in any other arguments than <code>n</code> from outside <code>Recursive</code>, we could just create another <code>private</code> function that has those arguments, and remove them from our public function.</li>
|
|
</ul>
|
|
</div>
|
|
<div markdown='1'>
|
|
<pre is="marp-pre" data-auto-scaling="downscale-only"><code class="language-csharp"><span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-built_in">long</span> <span class="hljs-title">Recursive</span>(<span class="hljs-params"><span class="hljs-built_in">int</span> nStartingValue</span>)</span>
|
|
{
|
|
<span class="hljs-keyword">if</span> (nStartingValue < MinDepth
|
|
|| nStartingValue > MaxDepth)
|
|
<span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> ArgumentOutOfRangeException(
|
|
<span class="hljs-string">"n is out of bounds!"</span>
|
|
);
|
|
|
|
<span class="hljs-function"><span class="hljs-built_in">long</span> <span class="hljs-title">getRecursive</span>(<span class="hljs-params">
|
|
<span class="hljs-built_in">int</span> n,
|
|
<span class="hljs-built_in">long</span> previous = <span class="hljs-number">0</span>,
|
|
<span class="hljs-built_in">long</span> current = <span class="hljs-number">0</span>,
|
|
<span class="hljs-built_in">int</span> counter = <span class="hljs-number">0</span></span>)</span>
|
|
=> counter == n ?
|
|
current :
|
|
getRecursive(
|
|
n,
|
|
current,
|
|
Math.Max(previous + current, <span class="hljs-number">1</span>),
|
|
counter + <span class="hljs-number">1</span>
|
|
);
|
|
<span class="hljs-keyword">return</span> getRecursive(nStartingValue);
|
|
}
|
|
</code></pre>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</foreignObject></svg><svg data-marpit-svg="" viewBox="0 0 1280 720"><foreignObject width="1280" height="720"><section id="35" data-marpit-fragments="3" data-paginate="true" data-class="invert" data-heading-divider="5" data-theme="uaut9f7pkna0co5lb2ryww7srs2g53vpacs6y1s30riqh" lang="en-US" class="invert" data-marpit-pagination="35" style="--paginate:true;--class:invert;--heading-divider:5;--theme:uaut9f7pkna0co5lb2ryww7srs2g53vpacs6y1s30riqh;" data-marpit-pagination-total="59">
|
|
<h3 id="end-result">End Result</h3>
|
|
<ul>
|
|
<li data-marpit-fragment="1">Now we have tested all the arguments in our public <code>Recursive</code> function.</li>
|
|
<li data-marpit-fragment="2">We do not need to test private functions such as <code>getRecursive</code>, only the public API has to be tested.</li>
|
|
<li data-marpit-fragment="3">All tests should <em><strong>pass</strong></em> at this point.</li>
|
|
</ul>
|
|
</section>
|
|
</foreignObject></svg><svg data-marpit-svg="" viewBox="0 0 1280 720"><foreignObject width="1280" height="720"><section id="36" data-marpit-fragments="4" data-paginate="true" data-class="exercise invert" data-heading-divider="5" data-theme="uaut9f7pkna0co5lb2ryww7srs2g53vpacs6y1s30riqh" lang="en-US" class="exercise invert" data-marpit-pagination="36" style="--paginate:true;--class:exercise invert;--heading-divider:5;--theme:uaut9f7pkna0co5lb2ryww7srs2g53vpacs6y1s30riqh;" data-marpit-pagination-total="59">
|
|
<h3 id="exercise-2-unit-testing">Exercise 2: Unit Testing</h3>
|
|
<pre is="marp-pre" data-auto-scaling="downscale-only"><code class="language-csharp"><span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-built_in">long</span> <span class="hljs-title">DoX</span>(<span class="hljs-params"><span class="hljs-built_in">int</span> nStartingValue</span>)</span> {
|
|
<span class="hljs-keyword">if</span> (nStartingValue < <span class="hljs-number">0</span> || nStartingValue > <span class="hljs-number">150</span>)
|
|
<span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> ArgumentOutOfRangeException(<span class="hljs-string">"n is out of bounds!"</span>);
|
|
|
|
<span class="hljs-function"><span class="hljs-built_in">long</span> <span class="hljs-title">doY</span>(<span class="hljs-params"><span class="hljs-built_in">int</span> n</span>)</span> => n > <span class="hljs-number">0</span> ? n * doY(n - <span class="hljs-number">1</span>) : <span class="hljs-number">1</span>;
|
|
|
|
<span class="hljs-keyword">return</span> doY(nStartingValue);
|
|
}
|
|
</code></pre>
|
|
<ul>
|
|
<li data-marpit-fragment="1">Following the previously used architecture as closely as possible, unit test the attached function using <code>NUnit</code>. Answer the following questions:</li>
|
|
</ul>
|
|
<ol>
|
|
<li data-marpit-fragment="2">What does the function that we are testing actually <em>do</em>?</li>
|
|
<li data-marpit-fragment="3">Do we have to test <code>doY</code> as well?</li>
|
|
<li data-marpit-fragment="4">How would you name <code>DoX</code> so it is descriptive? How about <code>doY</code>?</li>
|
|
</ol>
|
|
</section>
|
|
</foreignObject></svg><svg data-marpit-svg="" viewBox="0 0 1280 720"><foreignObject width="1280" height="720"><section id="37" data-marpit-fragments="3" data-paginate="true" data-class="exercise invert" data-heading-divider="5" data-theme="uaut9f7pkna0co5lb2ryww7srs2g53vpacs6y1s30riqh" lang="en-US" class="exercise invert" data-marpit-pagination="37" style="--paginate:true;--class:exercise invert;--heading-divider:5;--theme:uaut9f7pkna0co5lb2ryww7srs2g53vpacs6y1s30riqh;" data-marpit-pagination-total="59">
|
|
<h3 id="answers-to-exercise-2-unit-testing">Answers to exercise 2: Unit Testing</h3>
|
|
|
|
<ol>
|
|
<li data-marpit-fragment="1">It gets the nth factorial (<code>n!</code>)</li>
|
|
<li data-marpit-fragment="2"><code>doY</code> is a private method that gets tested when we test <code>DoX</code>, so no.</li>
|
|
<li data-marpit-fragment="3">Example names: <code>DoX</code> <mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: -0.054ex;" xmlns="http://www.w3.org/2000/svg" width="2.262ex" height="1.242ex" role="img" focusable="false" viewBox="0 -525 1000 549"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mo"><path data-c="21D2" d="M580 514Q580 525 596 525Q601 525 604 525T609 525T613 524T615 523T617 520T619 517T622 512Q659 438 720 381T831 300T927 263Q944 258 944 250T935 239T898 228T840 204Q696 134 622 -12Q618 -21 615 -22T600 -24Q580 -24 580 -17Q580 -13 585 0Q620 69 671 123L681 133H70Q56 140 56 153Q56 168 72 173H725L735 181Q774 211 852 250Q851 251 834 259T789 283T735 319L725 327H72Q56 332 56 347Q56 360 70 367H681L671 377Q638 412 609 458T580 514Z"></path></g></g></g></svg></mjx-container> <code>TryGetFactorial</code>, <code>doY</code> <mjx-container class="MathJax" jax="SVG"><svg style="vertical-align: -0.054ex;" xmlns="http://www.w3.org/2000/svg" width="2.262ex" height="1.242ex" role="img" focusable="false" viewBox="0 -525 1000 549"><g stroke="currentColor" fill="currentColor" stroke-width="0" transform="scale(1,-1)"><g data-mml-node="math"><g data-mml-node="mo"><path data-c="21D2" d="M580 514Q580 525 596 525Q601 525 604 525T609 525T613 524T615 523T617 520T619 517T622 512Q659 438 720 381T831 300T927 263Q944 258 944 250T935 239T898 228T840 204Q696 134 622 -12Q618 -21 615 -22T600 -24Q580 -24 580 -17Q580 -13 585 0Q620 69 671 123L681 133H70Q56 140 56 153Q56 168 72 173H725L735 181Q774 211 852 250Q851 251 834 259T789 283T735 319L725 327H72Q56 332 56 347Q56 360 70 367H681L671 377Q638 412 609 458T580 514Z"></path></g></g></g></svg></mjx-container> <code>getFactorial</code></li>
|
|
</ol>
|
|
</section>
|
|
</foreignObject></svg><svg data-marpit-svg="" viewBox="0 0 1280 720"><foreignObject width="1280" height="720"><section id="38" data-marpit-fragments="1" data-paginate="true" data-class="invert" data-heading-divider="5" data-theme="uaut9f7pkna0co5lb2ryww7srs2g53vpacs6y1s30riqh" lang="en-US" class="invert" data-marpit-pagination="38" style="--paginate:true;--class:invert;--heading-divider:5;--theme:uaut9f7pkna0co5lb2ryww7srs2g53vpacs6y1s30riqh;" data-marpit-pagination-total="59">
|
|
<h3 id="unit-testing-final-testing-code">Unit Testing: Final testing code</h3>
|
|
<ul>
|
|
<li data-marpit-fragment="1">
|
|
<pre is="marp-pre" data-auto-scaling="downscale-only"><code class="language-csharp">[<span class="hljs-meta">TestCase(0, 1)</span>]
|
|
[<span class="hljs-meta">TestCase(1, 1)</span>]
|
|
[<span class="hljs-meta">TestCase(2, 2)</span>]
|
|
[<span class="hljs-meta">TestCase(3, 6)</span>]
|
|
[<span class="hljs-meta">TestCase(15, 1307674368000)</span>]
|
|
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">TryGetFactorial_WhenNIsInBounds_ShouldReturnNthFactorial</span>(<span class="hljs-params">
|
|
<span class="hljs-built_in">int</span> n, <span class="hljs-built_in">long</span> expected
|
|
</span>)</span>
|
|
=> Assert.That(expected, Is.EqualTo(TestExerciseLib.TryGetFactorial(n)));
|
|
</code></pre>
|
|
<pre is="marp-pre" data-auto-scaling="downscale-only"><code class="language-csharp">[<span class="hljs-meta">TestCase(-1)</span>]
|
|
[<span class="hljs-meta">TestCase(151)</span>]
|
|
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">TryGetFactorial_NIsOutOfBounds_ShouldThrowArgumentOutOfRange</span>(<span class="hljs-params">
|
|
<span class="hljs-built_in">int</span> n
|
|
</span>)</span>
|
|
=> Assert.Throws<ArgumentOutOfRangeException>(
|
|
() => TestExerciseLib.TryGetFactorial(n)
|
|
);
|
|
</code></pre>
|
|
</li>
|
|
</ul>
|
|
</section>
|
|
</foreignObject></svg><svg data-marpit-svg="" viewBox="0 0 1280 720"><foreignObject width="1280" height="720"><section id="39" data-marpit-fragments="5" data-paginate="true" data-class="invert" data-heading-divider="5" data-theme="uaut9f7pkna0co5lb2ryww7srs2g53vpacs6y1s30riqh" lang="en-US" class="invert" data-marpit-pagination="39" style="--paginate:true;--class:invert;--heading-divider:5;--theme:uaut9f7pkna0co5lb2ryww7srs2g53vpacs6y1s30riqh;" data-marpit-pagination-total="59">
|
|
<h2 id="writing-testable-code">Writing testable code</h2>
|
|
<ul>
|
|
<li data-marpit-fragment="1">Writing testable code often means that you're writing good code</li>
|
|
<li data-marpit-fragment="2">Testable units should
|
|
<ul>
|
|
<li data-marpit-fragment="3">be deterministic (functional): same input always produces the same output</li>
|
|
<li data-marpit-fragment="4">be <a href="https://en.wikipedia.org/wiki/Loose_coupling">loosely coupled</a>: components have a loose and limited connection to other components in the system, meaning changes in one component should not require changes to many others</li>
|
|
<li data-marpit-fragment="5">abide to the <em><strong>Single Responsibility Principle</strong></em>: a function, method or component is only responsible for one purpose</li>
|
|
</ul>
|
|
</li>
|
|
</ul>
|
|
</section>
|
|
</foreignObject></svg><svg data-marpit-svg="" viewBox="0 0 1280 720"><foreignObject width="1280" height="720"><section id="40" data-marpit-fragments="2" data-paginate="true" data-class="invert" data-heading-divider="5" data-theme="uaut9f7pkna0co5lb2ryww7srs2g53vpacs6y1s30riqh" lang="en-US" class="invert" data-marpit-pagination="40" style="--paginate:true;--class:invert;--heading-divider:5;--theme:uaut9f7pkna0co5lb2ryww7srs2g53vpacs6y1s30riqh;" data-marpit-pagination-total="59">
|
|
<h3 id="writing-testable-code-example-1">Writing testable code: Example 1</h3>
|
|
<div class='columns23' markdown='1'>
|
|
<div markdown='1'>
|
|
<ul>
|
|
<li data-marpit-fragment="1">Can <code>GetCurrentSeason()</code> be unit tested? Why / why not?</li>
|
|
<li data-marpit-fragment="2">How to fix the problems, if any?</li>
|
|
</ul>
|
|
</div>
|
|
<div markdown='1'>
|
|
<pre is="marp-pre" data-auto-scaling="downscale-only"><code class="language-csharp"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">class</span> <span class="hljs-title">Season</span>
|
|
{
|
|
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-built_in">string</span> <span class="hljs-title">GetCurrentSeason</span>()</span> {
|
|
DateTime currentTime = DateTime.Now;
|
|
<span class="hljs-keyword">if</span> (currentTime.Month > <span class="hljs-number">11</span> && currentTime.Month < <span class="hljs-number">3</span>)
|
|
<span class="hljs-keyword">return</span> <span class="hljs-string">"Winter"</span>;
|
|
<span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (currentTime.Month < <span class="hljs-number">6</span>)
|
|
<span class="hljs-keyword">return</span> <span class="hljs-string">"Spring"</span>;
|
|
<span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (currentTime.Month < <span class="hljs-number">9</span>)
|
|
<span class="hljs-keyword">return</span> <span class="hljs-string">"Summer"</span>;
|
|
<span class="hljs-keyword">else</span>
|
|
<span class="hljs-keyword">return</span> <span class="hljs-string">"Fall"</span>;
|
|
}
|
|
}
|
|
</code></pre>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</foreignObject></svg><svg data-marpit-svg="" viewBox="0 0 1280 720"><foreignObject width="1280" height="720"><section id="41" data-marpit-fragments="3" data-paginate="true" data-class="invert" data-heading-divider="5" data-theme="uaut9f7pkna0co5lb2ryww7srs2g53vpacs6y1s30riqh" lang="en-US" class="invert" data-marpit-pagination="41" style="--paginate:true;--class:invert;--heading-divider:5;--theme:uaut9f7pkna0co5lb2ryww7srs2g53vpacs6y1s30riqh;" data-marpit-pagination-total="59">
|
|
<h3 id="writing-testable-code-example-1-fixed">Writing testable code: Example 1 fixed</h3>
|
|
<div class='columns23' markdown='1'>
|
|
<div markdown='1'>
|
|
<ul>
|
|
<li data-marpit-fragment="1">Now the method is purely functional and deterministic
|
|
<ul>
|
|
<li data-marpit-fragment="2">Will return the same value for every call with equal arguments</li>
|
|
</ul>
|
|
</li>
|
|
<li data-marpit-fragment="3"><code>GetCurrentSeason()</code> can now easily be tested as shown next</li>
|
|
</ul>
|
|
</div>
|
|
<div markdown='1'>
|
|
<pre is="marp-pre" data-auto-scaling="downscale-only"><code class="language-csharp"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">class</span> <span class="hljs-title">Season</span> {
|
|
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-built_in">string</span> <span class="hljs-title">GetCurrentSeason</span>(<span class="hljs-params">
|
|
DateTime currentTime
|
|
</span>)</span> {
|
|
<span class="hljs-keyword">if</span> (currentTime.Month > <span class="hljs-number">11</span> && currentTime.Month < <span class="hljs-number">3</span>)
|
|
<span class="hljs-keyword">return</span> <span class="hljs-string">"Winter"</span>;
|
|
<span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (currentTime.Month < <span class="hljs-number">6</span>)
|
|
<span class="hljs-keyword">return</span> <span class="hljs-string">"Spring"</span>;
|
|
<span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (currentTime.Month < <span class="hljs-number">9</span>)
|
|
<span class="hljs-keyword">return</span> <span class="hljs-string">"Summer"</span>;
|
|
<span class="hljs-keyword">else</span>
|
|
<span class="hljs-keyword">return</span> <span class="hljs-string">"Fall"</span>;
|
|
}
|
|
}
|
|
</code></pre>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</foreignObject></svg><svg data-marpit-svg="" viewBox="0 0 1280 720"><foreignObject width="1280" height="720"><section id="42" data-paginate="true" data-class="invert" data-heading-divider="5" data-theme="uaut9f7pkna0co5lb2ryww7srs2g53vpacs6y1s30riqh" lang="en-US" class="invert" data-marpit-pagination="42" style="--paginate:true;--class:invert;--heading-divider:5;--theme:uaut9f7pkna0co5lb2ryww7srs2g53vpacs6y1s30riqh;" data-marpit-pagination-total="59">
|
|
<pre is="marp-pre" data-auto-scaling="downscale-only"><code class="language-csharp"><span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title">GetCurrentSeasonTest</span>
|
|
{
|
|
[<span class="hljs-meta">TestCase(12, <span class="hljs-string">"Winter"</span>)</span>]
|
|
[<span class="hljs-meta">TestCase(3, <span class="hljs-string">"Spring"</span>)</span>]
|
|
[<span class="hljs-meta">TestCase(6, <span class="hljs-string">"Summer"</span>)</span>]
|
|
[<span class="hljs-meta">TestCase(9, <span class="hljs-string">"Fall"</span>)</span>]
|
|
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">GetCurrentSeason_ForFirstMonthsOfSeasons_ReturnsCorrectSeason</span>(<span class="hljs-params">
|
|
<span class="hljs-built_in">int</span> month,
|
|
<span class="hljs-built_in">string</span> season
|
|
</span>)</span>
|
|
{
|
|
Assert.That(season,
|
|
Is.EqualTo(
|
|
Season.GetCurrentSeason(<span class="hljs-keyword">new</span> DateTime(<span class="hljs-number">2020</span>, month, <span class="hljs-number">1</span>))
|
|
)
|
|
);
|
|
}
|
|
}
|
|
</code></pre>
|
|
</section>
|
|
</foreignObject></svg><svg data-marpit-svg="" viewBox="0 0 1280 720"><foreignObject width="1280" height="720"><section id="43" data-marpit-fragments="1" data-paginate="true" data-class="invert" data-heading-divider="5" data-theme="uaut9f7pkna0co5lb2ryww7srs2g53vpacs6y1s30riqh" lang="en-US" class="invert" data-marpit-pagination="43" style="--paginate:true;--class:invert;--heading-divider:5;--theme:uaut9f7pkna0co5lb2ryww7srs2g53vpacs6y1s30riqh;" data-marpit-pagination-total="59">
|
|
<div class='centered'>
|
|
<p><img src="imgs/9-testing_2.png" alt="" /></p>
|
|
</div>
|
|
<ul>
|
|
<li data-marpit-fragment="1">The tests fail, as shown above.</li>
|
|
</ul>
|
|
</section>
|
|
</foreignObject></svg><svg data-marpit-svg="" viewBox="0 0 1280 720"><foreignObject width="1280" height="720"><section id="44" data-marpit-fragments="2" data-paginate="true" data-class="invert" data-heading-divider="5" data-theme="uaut9f7pkna0co5lb2ryww7srs2g53vpacs6y1s30riqh" lang="en-US" class="invert" data-marpit-pagination="44" style="--paginate:true;--class:invert;--heading-divider:5;--theme:uaut9f7pkna0co5lb2ryww7srs2g53vpacs6y1s30riqh;" data-marpit-pagination-total="59">
|
|
<div class='columns32' markdown='1'>
|
|
<div markdown='1'>
|
|
<ul>
|
|
<li data-marpit-fragment="1">Test results indicate there is something wrong with the code path that should return <code>Winter</code><pre is="marp-pre" data-auto-scaling="downscale-only"><code class="language-csharp"><span class="hljs-keyword">if</span> (currentTime.Month > <span class="hljs-number">11</span> && currentTime.Month < <span class="hljs-number">3</span>)
|
|
</code></pre>
|
|
</li>
|
|
<li data-marpit-fragment="2">The line should be changed to<pre is="marp-pre" data-auto-scaling="downscale-only"><code class="language-csharp"><span class="hljs-keyword">if</span> (currentTime.Month > <span class="hljs-number">11</span> || currentTime.Month < <span class="hljs-number">3</span>)
|
|
</code></pre>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
<div markdown='1'>
|
|
<p><img src="imgs/9-testing_3.png" alt="" /></p>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</foreignObject></svg><svg data-marpit-svg="" viewBox="0 0 1280 720"><foreignObject width="1280" height="720"><section id="45" data-marpit-fragments="1" data-paginate="true" data-class="invert" data-heading-divider="5" data-theme="uaut9f7pkna0co5lb2ryww7srs2g53vpacs6y1s30riqh" lang="en-US" class="invert" data-marpit-pagination="45" style="--paginate:true;--class:invert;--heading-divider:5;--theme:uaut9f7pkna0co5lb2ryww7srs2g53vpacs6y1s30riqh;" data-marpit-pagination-total="59">
|
|
<h3 id="writing-testable-code-example-2">Writing testable code: Example 2</h3>
|
|
<ul>
|
|
<li data-marpit-fragment="1">Let's look at a <code>HeatingUnit</code> class that returns a heating setting based on the current season. The current date problem is back again:<pre is="marp-pre" data-auto-scaling="downscale-only"><code class="language-csharp"><span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title">HeatingUnit</span>
|
|
{
|
|
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-built_in">string</span> <span class="hljs-title">GetHeatingSetting</span>()</span>
|
|
{
|
|
DateTime currentDate = DateTime.Now;
|
|
<span class="hljs-keyword">if</span> (Season.GetCurrentSeason(currentDate) == <span class="hljs-string">"Winter"</span>)
|
|
<span class="hljs-keyword">return</span> <span class="hljs-string">"HEAT_SETTING_HIGH"</span>;
|
|
<span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (Season.GetCurrentSeason(currentDate) == <span class="hljs-string">"Summer"</span>)
|
|
<span class="hljs-keyword">return</span> <span class="hljs-string">"HEAT_SETTING_OFF"</span>;
|
|
<span class="hljs-keyword">else</span>
|
|
<span class="hljs-keyword">return</span> <span class="hljs-string">"HEAT_SETTING_MEDIUM"</span>;
|
|
}
|
|
}
|
|
</code></pre>
|
|
</li>
|
|
</ul>
|
|
</section>
|
|
</foreignObject></svg><svg data-marpit-svg="" viewBox="0 0 1280 720"><foreignObject width="1280" height="720"><section id="46" data-marpit-fragments="3" data-paginate="true" data-class="invert" data-heading-divider="5" data-theme="uaut9f7pkna0co5lb2ryww7srs2g53vpacs6y1s30riqh" lang="en-US" class="invert" data-marpit-pagination="46" style="--paginate:true;--class:invert;--heading-divider:5;--theme:uaut9f7pkna0co5lb2ryww7srs2g53vpacs6y1s30riqh;" data-marpit-pagination-total="59">
|
|
<h3 id="writing-testable-code-example-2-continued">Writing testable code: Example 2 continued</h3>
|
|
<ul>
|
|
<li data-marpit-fragment="1">Instead of taking the current date initialization even higher in the class hierarchy, let's make a <em><strong>service</strong></em> that returns the current date<pre is="marp-pre" data-auto-scaling="downscale-only"><code class="language-csharp"><span class="hljs-keyword">public</span> <span class="hljs-keyword">interface</span> <span class="hljs-title">IDateTimeProvider</span>
|
|
{
|
|
<span class="hljs-function">DateTime <span class="hljs-title">GetDateTime</span>()</span>;
|
|
}
|
|
</code></pre>
|
|
</li>
|
|
<li data-marpit-fragment="2">After this the service can be injected into <code>HeatingUnit</code> with constructor injection</li>
|
|
<li data-marpit-fragment="3">Code example shown next</li>
|
|
</ul>
|
|
</section>
|
|
</foreignObject></svg><svg data-marpit-svg="" viewBox="0 0 1280 720"><foreignObject width="1280" height="720"><section id="47" data-paginate="true" data-class="invert" data-heading-divider="5" data-theme="uaut9f7pkna0co5lb2ryww7srs2g53vpacs6y1s30riqh" lang="en-US" class="invert" data-marpit-pagination="47" style="--paginate:true;--class:invert;--heading-divider:5;--theme:uaut9f7pkna0co5lb2ryww7srs2g53vpacs6y1s30riqh;" data-marpit-pagination-total="59">
|
|
<pre is="marp-pre" data-auto-scaling="downscale-only"><code class="language-csharp"><span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title">HeatingUnit</span>
|
|
{
|
|
<span class="hljs-keyword">private</span> <span class="hljs-keyword">readonly</span> IDateTimeProvider _dateTimeProvider;
|
|
|
|
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-title">HeatingUnit</span>(<span class="hljs-params">IDateTimeProvider dateTimeProvider</span>)</span>
|
|
{
|
|
_dateTimeProvider = dateTimeProvider;
|
|
}
|
|
|
|
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-built_in">string</span> <span class="hljs-title">GetHeatingSetting</span>()</span> {
|
|
<span class="hljs-keyword">if</span> (Season.GetCurrentSeason(_dateTimeProvider.GetDateTime()) == <span class="hljs-string">"Winter"</span>)
|
|
<span class="hljs-keyword">return</span> <span class="hljs-string">"HEAT_SETTING_HIGH"</span>;
|
|
<span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (Season.GetCurrentSeason(_dateTimeProvider.GetDateTime()) == <span class="hljs-string">"Summer"</span>)
|
|
<span class="hljs-keyword">return</span> <span class="hljs-string">"HEAT_SETTING_OFF"</span>;
|
|
<span class="hljs-keyword">else</span>
|
|
<span class="hljs-keyword">return</span> <span class="hljs-string">"HEAT_SETTING_MEDIUM"</span>;
|
|
}
|
|
}
|
|
</code></pre>
|
|
</section>
|
|
</foreignObject></svg><svg data-marpit-svg="" viewBox="0 0 1280 720"><foreignObject width="1280" height="720"><section id="48" data-marpit-fragments="3" data-paginate="true" data-class="invert" data-heading-divider="5" data-theme="uaut9f7pkna0co5lb2ryww7srs2g53vpacs6y1s30riqh" lang="en-US" class="invert" data-marpit-pagination="48" style="--paginate:true;--class:invert;--heading-divider:5;--theme:uaut9f7pkna0co5lb2ryww7srs2g53vpacs6y1s30riqh;" data-marpit-pagination-total="59">
|
|
<ul>
|
|
<li data-marpit-fragment="1">
|
|
<p>For testing, a fake <code>DateTimeProvider</code> service can be used for injecting the <code>HeatingUnit</code> with any date:</p>
|
|
<pre is="marp-pre" data-auto-scaling="downscale-only"><code class="language-csharp"><span class="hljs-keyword">class</span> <span class="hljs-title">FakeDateTimeProvider</span> : <span class="hljs-title">IDateTimeProvider</span>
|
|
{
|
|
<span class="hljs-keyword">public</span> DateTime Date { <span class="hljs-keyword">get</span>; <span class="hljs-keyword">set</span>; }
|
|
<span class="hljs-function"><span class="hljs-keyword">public</span> DateTime <span class="hljs-title">GetDateTime</span>()</span> => Date;
|
|
}
|
|
</code></pre>
|
|
</li>
|
|
<li data-marpit-fragment="2">
|
|
<p>Using this kind of a fake service instead of the real one is called <a href="https://en.wikipedia.org/wiki/Mock_object">mocking</a></p>
|
|
</li>
|
|
<li data-marpit-fragment="3">
|
|
<p>In the real application the provider would return the real current date</p>
|
|
</li>
|
|
</ul>
|
|
</section>
|
|
</foreignObject></svg><svg data-marpit-svg="" viewBox="0 0 1280 720"><foreignObject width="1280" height="720"><section id="49" data-paginate="true" data-class="invert" data-heading-divider="5" data-theme="uaut9f7pkna0co5lb2ryww7srs2g53vpacs6y1s30riqh" lang="en-US" class="invert" data-marpit-pagination="49" style="--paginate:true;--class:invert;--heading-divider:5;--theme:uaut9f7pkna0co5lb2ryww7srs2g53vpacs6y1s30riqh;" data-marpit-pagination-total="59">
|
|
<pre is="marp-pre" data-auto-scaling="downscale-only"><code class="language-csharp"><span class="hljs-keyword">class</span> <span class="hljs-title">SetHeatingSettingTest</span>
|
|
{
|
|
[<span class="hljs-meta">TestCase(12, <span class="hljs-string">"HEAT_SETTING_HIGH"</span>)</span>]
|
|
[<span class="hljs-meta">TestCase(3, <span class="hljs-string">"HEAT_SETTING_MEDIUM"</span>)</span>]
|
|
[<span class="hljs-meta">TestCase(6, <span class="hljs-string">"HEAT_SETTING_OFF"</span>)</span>]
|
|
[<span class="hljs-meta">TestCase(9, <span class="hljs-string">"HEAT_SETTING_MEDIUM"</span>)</span>]
|
|
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">SetHeatingSetting_ForFirstMonthsOfSeasons_ShouldReturnCorrectSetting</span>(<span class="hljs-params">
|
|
<span class="hljs-built_in">int</span> month,
|
|
<span class="hljs-built_in">string</span> setting
|
|
</span>)</span>
|
|
{
|
|
FakeDateTimeProvider timeProvider
|
|
= <span class="hljs-keyword">new</span> FakeDateTimeProvider { Date = <span class="hljs-keyword">new</span> DateTime(<span class="hljs-number">2020</span>, month, <span class="hljs-number">1</span>) };
|
|
HeatingUnit heatingUnit = <span class="hljs-keyword">new</span> HeatingUnit(timeProvider);
|
|
|
|
Assert.That(setting, Is.EqualTo(heatingUnit.GetHeatingSetting()));
|
|
}
|
|
}
|
|
</code></pre>
|
|
</section>
|
|
</foreignObject></svg><svg data-marpit-svg="" viewBox="0 0 1280 720"><foreignObject width="1280" height="720"><section id="50" data-paginate="true" data-class="invert" data-heading-divider="5" data-theme="uaut9f7pkna0co5lb2ryww7srs2g53vpacs6y1s30riqh" lang="en-US" class="invert" data-marpit-pagination="50" style="--paginate:true;--class:invert;--heading-divider:5;--theme:uaut9f7pkna0co5lb2ryww7srs2g53vpacs6y1s30riqh;" data-marpit-pagination-total="59">
|
|
<div class='centered'>
|
|
<p><img src="imgs/9-testing_4.png" alt="" /><br />
|
|
Tests should pass!</p>
|
|
</div>
|
|
</section>
|
|
</foreignObject></svg><svg data-marpit-svg="" viewBox="0 0 1280 720"><foreignObject width="1280" height="720"><section id="51" data-marpit-fragments="4" data-paginate="true" data-class="invert" data-heading-divider="5" data-theme="uaut9f7pkna0co5lb2ryww7srs2g53vpacs6y1s30riqh" lang="en-US" class="invert" data-marpit-pagination="51" style="--paginate:true;--class:invert;--heading-divider:5;--theme:uaut9f7pkna0co5lb2ryww7srs2g53vpacs6y1s30riqh;" data-marpit-pagination-total="59">
|
|
<h2 id="testing-with-postman">Testing with Postman</h2>
|
|
<ul>
|
|
<li data-marpit-fragment="1">So far, we have created individual request with Postman to see what the response of an API is for each request</li>
|
|
<li data-marpit-fragment="2">This is not testing an API, this is <em><strong>exploring</strong></em> an API</li>
|
|
<li data-marpit-fragment="3">Tests in Postman are inserted in the <em>Tests</em> tab
|
|
<ul>
|
|
<li data-marpit-fragment="4">Postman uses the <em><strong>Chai.js</strong></em> testing library for creating tests<br />
|
|
<img src="imgs/9-testing_5.png" alt="" /></li>
|
|
</ul>
|
|
</li>
|
|
</ul>
|
|
</section>
|
|
</foreignObject></svg><svg data-marpit-svg="" viewBox="0 0 1280 720"><foreignObject width="1280" height="720"><section id="52" data-marpit-fragments="2" data-paginate="true" data-class="invert" data-heading-divider="5" data-theme="uaut9f7pkna0co5lb2ryww7srs2g53vpacs6y1s30riqh" lang="en-US" class="invert" data-marpit-pagination="52" style="--paginate:true;--class:invert;--heading-divider:5;--theme:uaut9f7pkna0co5lb2ryww7srs2g53vpacs6y1s30riqh;" data-marpit-pagination-total="59">
|
|
<ul>
|
|
<li data-marpit-fragment="1">To get started immediately, you can select <em>snippets</em> from the right</li>
|
|
<li data-marpit-fragment="2">Let's select the <em>Status code: Code is 200</em> snippet<br />
|
|
<img src="imgs/9-testing_6.png" alt="" /></li>
|
|
</ul>
|
|
</section>
|
|
</foreignObject></svg><svg data-marpit-svg="" viewBox="0 0 1280 720"><foreignObject width="1280" height="720"><section id="53" data-marpit-fragments="1" data-paginate="true" data-class="invert" data-heading-divider="5" data-theme="uaut9f7pkna0co5lb2ryww7srs2g53vpacs6y1s30riqh" lang="en-US" class="invert" data-marpit-pagination="53" style="--paginate:true;--class:invert;--heading-divider:5;--theme:uaut9f7pkna0co5lb2ryww7srs2g53vpacs6y1s30riqh;" data-marpit-pagination-total="59">
|
|
<ul>
|
|
<li data-marpit-fragment="1">Successfully run tests show up in green in the <em>Test Result</em> tab of the response:<br />
|
|
<img src="imgs/9-testing_7.png" alt="" /></li>
|
|
</ul>
|
|
</section>
|
|
</foreignObject></svg><svg data-marpit-svg="" viewBox="0 0 1280 720"><foreignObject width="1280" height="720"><section id="54" data-marpit-fragments="2" data-paginate="true" data-class="invert" data-heading-divider="5" data-theme="uaut9f7pkna0co5lb2ryww7srs2g53vpacs6y1s30riqh" lang="en-US" class="invert" data-marpit-pagination="54" style="--paginate:true;--class:invert;--heading-divider:5;--theme:uaut9f7pkna0co5lb2ryww7srs2g53vpacs6y1s30riqh;" data-marpit-pagination-total="59">
|
|
<ul>
|
|
<li data-marpit-fragment="1"><a href="https://learning.postman.com/docs/writing-scripts/test-scripts/">The official Postman web page</a> is a good starting point for learning testing with Postman</li>
|
|
<li data-marpit-fragment="2">Using <code>pm.expect</code> will give a bit more info about the test:<br />
|
|
<img src="imgs/9-testing_8.png" alt="" /></li>
|
|
</ul>
|
|
</section>
|
|
</foreignObject></svg><svg data-marpit-svg="" viewBox="0 0 1280 720"><foreignObject width="1280" height="720"><section id="55" data-marpit-fragments="2" data-paginate="true" data-class="invert" data-heading-divider="5" data-theme="uaut9f7pkna0co5lb2ryww7srs2g53vpacs6y1s30riqh" lang="en-US" class="invert" data-marpit-pagination="55" style="--paginate:true;--class:invert;--heading-divider:5;--theme:uaut9f7pkna0co5lb2ryww7srs2g53vpacs6y1s30riqh;" data-marpit-pagination-total="59">
|
|
<ul>
|
|
<li data-marpit-fragment="1">The response to /serverinfo has the following body:<pre is="marp-pre" data-auto-scaling="downscale-only"><code class="language-json"><span class="hljs-punctuation">{</span>
|
|
<span class="hljs-attr">"appSettings"</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
|
|
<span class="hljs-attr">"applicationUrl"</span><span class="hljs-punctuation">:</span> <span class="hljs-string">"http://localhost:63741"</span><span class="hljs-punctuation">,</span>
|
|
<span class="hljs-attr">"aspNetCoreEnvironment"</span><span class="hljs-punctuation">:</span> <span class="hljs-string">"Development"</span>
|
|
<span class="hljs-punctuation">}</span><span class="hljs-punctuation">,</span>
|
|
<span class="hljs-attr">"serverStatus"</span><span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
|
|
<span class="hljs-attr">"dbConnectionStatus"</span><span class="hljs-punctuation">:</span> <span class="hljs-string">"Connected"</span>
|
|
<span class="hljs-punctuation">}</span>
|
|
<span class="hljs-punctuation">}</span>
|
|
</code></pre>
|
|
</li>
|
|
<li data-marpit-fragment="2">Let's make a test to see whether the server is connected to the database</li>
|
|
</ul>
|
|
</section>
|
|
</foreignObject></svg><svg data-marpit-svg="" viewBox="0 0 1280 720"><foreignObject width="1280" height="720"><section id="56" data-marpit-fragments="1" data-paginate="true" data-class="invert" data-heading-divider="5" data-theme="uaut9f7pkna0co5lb2ryww7srs2g53vpacs6y1s30riqh" lang="en-US" class="invert" data-marpit-pagination="56" style="--paginate:true;--class:invert;--heading-divider:5;--theme:uaut9f7pkna0co5lb2ryww7srs2g53vpacs6y1s30riqh;" data-marpit-pagination-total="59">
|
|
<ul>
|
|
<li data-marpit-fragment="1">Variables can be declared within the test:<br />
|
|
<img src="imgs/9-testing_9.png" alt="" /></li>
|
|
</ul>
|
|
</section>
|
|
</foreignObject></svg><svg data-marpit-svg="" viewBox="0 0 1280 720"><foreignObject width="1280" height="720"><section id="57" data-marpit-fragments="3" data-paginate="true" data-class="exercise invert" data-heading-divider="5" data-theme="uaut9f7pkna0co5lb2ryww7srs2g53vpacs6y1s30riqh" lang="en-US" class="exercise invert" data-marpit-pagination="57" style="--paginate:true;--class:exercise invert;--heading-divider:5;--theme:uaut9f7pkna0co5lb2ryww7srs2g53vpacs6y1s30riqh;" data-marpit-pagination-total="59">
|
|
<h3 id="exercise-3-testing-with-postman">Exercise 3: Testing with Postman</h3>
|
|
|
|
<ol>
|
|
<li data-marpit-fragment="1">Create a new collection in Postman by selecting the <em>Collections</em> tab and clicking <em>New Collection</em>. Name it <code>CourseAPI Tests</code>. Launch the CourseAPI you have developed during the lectures, and make sure the <code>course_db</code> database is connected</li>
|
|
<li data-marpit-fragment="2">Create a new <code>GET</code> request for the URI <code>/api/courses</code>. Add a test: status code of the request should be 200. Save the request to the <code>CourseAPI Tests</code> collection.</li>
|
|
<li data-marpit-fragment="3">Create a new <code>GET</code> request for the URI <code>/api/courses/999999999</code>. Add a test: status code of the request should be 404. Save the request to the <code>CourseAPI Tests</code> collection.</li>
|
|
</ol>
|
|
</section>
|
|
</foreignObject></svg><svg data-marpit-svg="" viewBox="0 0 1280 720"><foreignObject width="1280" height="720"><section id="58" data-marpit-fragments="2" data-paginate="true" data-class="exercise invert" data-heading-divider="5" data-theme="uaut9f7pkna0co5lb2ryww7srs2g53vpacs6y1s30riqh" lang="en-US" class="exercise invert" data-marpit-pagination="58" style="--paginate:true;--class:exercise invert;--heading-divider:5;--theme:uaut9f7pkna0co5lb2ryww7srs2g53vpacs6y1s30riqh;" data-marpit-pagination-total="59">
|
|
<ol start="4">
|
|
<li data-marpit-fragment="1">Create a new <code>POST</code> request for the URI <code>/api/courses</code>. Set the headers and the content correctly, but set the value of credits to 100. Add a test; status code should be 400. Save the request to the CourseAPI Tests collection.</li>
|
|
<li data-marpit-fragment="2">Hover on CourseAPI Tests collection, click the arrow and click <em>Run</em>. From the opened window, scroll down and click <em>Run CourseAPI Tests</em>. This will create all the requests in your collection.</li>
|
|
</ol>
|
|
</section>
|
|
</foreignObject></svg><svg data-marpit-svg="" viewBox="0 0 1280 720"><foreignObject width="1280" height="720"><section id="59" data-marpit-fragments="6" data-paginate="true" data-class="invert" data-heading-divider="5" data-theme="uaut9f7pkna0co5lb2ryww7srs2g53vpacs6y1s30riqh" lang="en-US" class="invert" data-marpit-pagination="59" style="--paginate:true;--class:invert;--heading-divider:5;--theme:uaut9f7pkna0co5lb2ryww7srs2g53vpacs6y1s30riqh;" data-marpit-pagination-total="59">
|
|
<h2 id="cicdct">CI/CD/CT</h2>
|
|
<ul>
|
|
<li data-marpit-fragment="1">CI stands for <em><strong>Continuous Integration</strong></em>
|
|
<ul>
|
|
<li data-marpit-fragment="2">Each change in code triggers a build-and-test sequence for the given project</li>
|
|
<li data-marpit-fragment="3">Goal is to have a consistent and automated way to build, package, and test applications</li>
|
|
</ul>
|
|
</li>
|
|
<li data-marpit-fragment="4">CD stands for <em><strong>Continuous Delivery</strong></em>
|
|
<ul>
|
|
<li data-marpit-fragment="5">Automates the delivery of applications to selected environments, such as Azure, AWS, GCP</li>
|
|
</ul>
|
|
</li>
|
|
<li data-marpit-fragment="6">Both require <em><strong>Continuous Testing</strong></em>, which includes automated regression, performance and other tests</li>
|
|
</ul>
|
|
</section>
|
|
<script>!function(){"use strict";const t={h1:{proto:()=>HTMLHeadingElement,attrs:{role:"heading","aria-level":"1"},style:"display: block; font-size: 2em; margin-block-start: 0.67em; margin-block-end: 0.67em; margin-inline-start: 0px; margin-inline-end: 0px; font-weight: bold;"},h2:{proto:()=>HTMLHeadingElement,attrs:{role:"heading","aria-level":"2"},style:"display: block; font-size: 1.5em; margin-block-start: 0.83em; margin-block-end: 0.83em; margin-inline-start: 0px; margin-inline-end: 0px; font-weight: bold;"},h3:{proto:()=>HTMLHeadingElement,attrs:{role:"heading","aria-level":"3"},style:"display: block; font-size: 1.17em; margin-block-start: 1em; margin-block-end: 1em; margin-inline-start: 0px; margin-inline-end: 0px; font-weight: bold;"},h4:{proto:()=>HTMLHeadingElement,attrs:{role:"heading","aria-level":"4"},style:"display: block; margin-block-start: 1.33em; margin-block-end: 1.33em; margin-inline-start: 0px; margin-inline-end: 0px; font-weight: bold;"},h5:{proto:()=>HTMLHeadingElement,attrs:{role:"heading","aria-level":"5"},style:"display: block; font-size: 0.83em; margin-block-start: 1.67em; margin-block-end: 1.67em; margin-inline-start: 0px; margin-inline-end: 0px; font-weight: bold;"},h6:{proto:()=>HTMLHeadingElement,attrs:{role:"heading","aria-level":"6"},style:"display: block; font-size: 0.67em; margin-block-start: 2.33em; margin-block-end: 2.33em; margin-inline-start: 0px; margin-inline-end: 0px; font-weight: bold;"},span:{proto:()=>HTMLSpanElement},pre:{proto:()=>HTMLElement,style:"display: block; font-family: monospace; white-space: pre; margin: 1em 0; --marp-auto-scaling-white-space: pre;"}},e="data-marp-auto-scaling-wrapper",i="data-marp-auto-scaling-svg",n="data-marp-auto-scaling-container";class s extends HTMLElement{constructor(){super(),this.svgPreserveAspectRatio="xMinYMid meet";const t=t=>([e])=>{const{width:i,height:n}=e.contentRect;this[t]={width:i,height:n},this.updateSVGRect()};this.attachShadow({mode:"open"}),this.containerObserver=new ResizeObserver(t("containerSize")),this.wrapperObserver=new ResizeObserver(((...e)=>{t("wrapperSize")(...e),this.flushSvgDisplay()}))}static get observedAttributes(){return["data-downscale-only"]}connectedCallback(){var t,s,o,r,a;this.shadowRoot.innerHTML=`\n<style>\n svg[${i}] { display: block; width: 100%; height: auto; vertical-align: top; }\n span[${n}] { display: table; white-space: var(--marp-auto-scaling-white-space, nowrap); width: max-content; }\n</style>\n<div ${e}>\n <svg part="svg" ${i}>\n <foreignObject><span ${n}><slot></slot></span></foreignObject>\n </svg>\n</div>\n `.split(/\n\s*/).join(""),this.wrapper=null!==(t=this.shadowRoot.querySelector(`div[${e}]`))&&void 0!==t?t:void 0;const l=this.svg;this.svg=null!==(o=null===(s=this.wrapper)||void 0===s?void 0:s.querySelector(`svg[${i}]`))&&void 0!==o?o:void 0,this.svg!==l&&(this.svgComputedStyle=this.svg?window.getComputedStyle(this.svg):void 0),this.container=null!==(a=null===(r=this.svg)||void 0===r?void 0:r.querySelector(`span[${n}]`))&&void 0!==a?a:void 0,this.observe()}disconnectedCallback(){this.svg=void 0,this.svgComputedStyle=void 0,this.wrapper=void 0,this.container=void 0,this.observe()}attributeChangedCallback(){this.observe()}flushSvgDisplay(){const{svg:t}=this;t&&(t.style.display="inline",requestAnimationFrame((()=>{t.style.display=""})))}observe(){this.containerObserver.disconnect(),this.wrapperObserver.disconnect(),this.wrapper&&this.wrapperObserver.observe(this.wrapper),this.container&&this.containerObserver.observe(this.container),this.svgComputedStyle&&this.observeSVGStyle(this.svgComputedStyle)}observeSVGStyle(t){const e=()=>{const i=(()=>{const e=t.getPropertyValue("--preserve-aspect-ratio");if(e)return e.trim();return`x${(({textAlign:t,direction:e})=>{if(t.endsWith("left"))return"Min";if(t.endsWith("right"))return"Max";if("start"===t||"end"===t){let i="rtl"===e;return"end"===t&&(i=!i),i?"Max":"Min"}return"Mid"})(t)}YMid meet`})();i!==this.svgPreserveAspectRatio&&(this.svgPreserveAspectRatio=i,this.updateSVGRect()),t===this.svgComputedStyle&&requestAnimationFrame(e)};e()}updateSVGRect(){var t,e,i,n,s,o,r;let a=Math.ceil(null!==(e=null===(t=this.containerSize)||void 0===t?void 0:t.width)&&void 0!==e?e:0);const l=Math.ceil(null!==(n=null===(i=this.containerSize)||void 0===i?void 0:i.height)&&void 0!==n?n:0);void 0!==this.dataset.downscaleOnly&&(a=Math.max(a,null!==(o=null===(s=this.wrapperSize)||void 0===s?void 0:s.width)&&void 0!==o?o:0));const c=null===(r=this.svg)||void 0===r?void 0:r.querySelector(":scope > foreignObject");if(null==c||c.setAttribute("width",`${a}`),null==c||c.setAttribute("height",`${l}`),this.svg&&(this.svg.setAttribute("viewBox",`0 0 ${a} ${l}`),this.svg.setAttribute("preserveAspectRatio",this.svgPreserveAspectRatio),this.svg.style.height=a<=0||l<=0?"0":""),this.container){const t=this.svgPreserveAspectRatio.toLowerCase();this.container.style.marginLeft=t.startsWith("xmid")||t.startsWith("xmax")?"auto":"0",this.container.style.marginRight=t.startsWith("xmi")?"auto":"0"}}}const o=(t,{attrs:e={},style:i})=>class extends t{constructor(...t){super(...t);for(const[t,i]of Object.entries(e))this.hasAttribute(t)||this.setAttribute(t,i);this.attachShadow({mode:"open"})}static get observedAttributes(){return["data-auto-scaling"]}connectedCallback(){this._update()}attributeChangedCallback(){this._update()}_update(){const t=i?`<style>:host { ${i} }</style>`:"";let e="<slot></slot>";const{autoScaling:n}=this.dataset;if(void 0!==n){e=`<marp-auto-scaling exportparts="svg:auto-scaling" ${"downscale-only"===n?"data-downscale-only":""}>${e}</marp-auto-scaling>`}this.shadowRoot.innerHTML=t+e}};let r;const a=Symbol();let l;const c="marpitSVGPolyfill:setZoomFactor,",d=Symbol(),g=Symbol();const h=()=>{const t="Apple Computer, Inc."===navigator.vendor,e=t?[u]:[],i={then:e=>(t?(async()=>{if(void 0===l){const t=document.createElement("canvas");t.width=10,t.height=10;const e=t.getContext("2d"),i=new Image(10,10),n=new Promise((t=>{i.addEventListener("load",(()=>t()))}));i.crossOrigin="anonymous",i.src="data:image/svg+xml;charset=utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2210%22%20height%3D%2210%22%20viewBox%3D%220%200%201%201%22%3E%3CforeignObject%20width%3D%221%22%20height%3D%221%22%20requiredExtensions%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2Fxhtml%22%3E%3Cdiv%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F1999%2Fxhtml%22%20style%3D%22width%3A%201px%3B%20height%3A%201px%3B%20background%3A%20red%3B%20position%3A%20relative%22%3E%3C%2Fdiv%3E%3C%2FforeignObject%3E%3C%2Fsvg%3E",await n,e.drawImage(i,0,0),l=e.getImageData(5,5,1,1).data[3]<128}return l})().then((t=>{null==e||e(t?[u]:[])})):null==e||e([]),i)};return Object.assign(e,i)};let p,m;function u(t){const e="object"==typeof t&&t.target||document,i="object"==typeof t?t.zoom:t;window[g]||(Object.defineProperty(window,g,{configurable:!0,value:!0}),document.body.style.zoom=1.0001,document.body.offsetHeight,document.body.style.zoom=1,window.addEventListener("message",(({data:t,origin:e})=>{if(e===window.origin)try{if(t&&"string"==typeof t&&t.startsWith(c)){const[,e]=t.split(","),i=Number.parseFloat(e);Number.isNaN(i)||(m=i)}}catch(t){console.error(t)}})));let n=!1;Array.from(e.querySelectorAll("svg[data-marpit-svg]"),(t=>{var e,s,o,r;t.style.transform||(t.style.transform="translateZ(0)");const a=i||m||t.currentScale||1;p!==a&&(p=a,n=a);const l=t.getBoundingClientRect(),{length:c}=t.children;for(let i=0;i<c;i+=1){const n=t.children[i];if(n.getScreenCTM){const t=n.getScreenCTM();if(t){const i=null!==(s=null===(e=n.x)||void 0===e?void 0:e.baseVal.value)&&void 0!==s?s:0,c=null!==(r=null===(o=n.y)||void 0===o?void 0:o.baseVal.value)&&void 0!==r?r:0,d=n.children.length;for(let e=0;e<d;e+=1){const s=n.children[e];if("SECTION"===s.tagName){const{style:e}=s;e.transformOrigin||(e.transformOrigin=`${-i}px ${-c}px`),e.transform=`scale(${a}) matrix(${t.a}, ${t.b}, ${t.c}, ${t.d}, ${t.e-l.left}, ${t.f-l.top}) translateZ(0.0001px)`;break}}}}}})),!1!==n&&Array.from(e.querySelectorAll("iframe"),(({contentWindow:t})=>{null==t||t.postMessage(`${c}${n}`,"null"===window.origin?"*":window.origin)}))}function v({once:t=!1,target:e=document}={}){const i=function(t=document){if(t[d])return t[d];let e=!0;const i=()=>{e=!1,delete t[d]};Object.defineProperty(t,d,{configurable:!0,value:i});let n=[],s=!1;(async()=>{try{n=await h()}finally{s=!0}})();const o=()=>{for(const e of n)e({target:t});s&&0===n.length||e&&window.requestAnimationFrame(o)};return o(),i}(e);return t?(i(),()=>{}):i}p=1,m=void 0;const b=Symbol(),w=(e=document)=>{if("undefined"==typeof window)throw new Error("Marp Core's browser script is valid only in browser context.");if(((e=document)=>{const i=window[a];i||customElements.define("marp-auto-scaling",s);for(const n of Object.keys(t)){const s=`marp-${n}`,a=t[n].proto();null!=r||(r=!!document.createElement("div",{is:"marp-auto-scaling"}).outerHTML.startsWith("<div is")),r&&a!==HTMLElement?i||customElements.define(s,o(a,{style:t[n].style}),{extends:n}):(i||customElements.define(s,o(HTMLElement,t[n])),e.querySelectorAll(`${n}[is="${s}"]`).forEach((t=>{t.outerHTML=t.outerHTML.replace(new RegExp(`^<${n}`,"i"),`<${s}`).replace(new RegExp(`</${n}>$`,"i"),`</${s}>`)})))}window[a]=!0})(e),e[b])return e[b];const i=v({target:e}),n=()=>{i(),delete e[b]},l=Object.assign(n,{cleanup:n,update:()=>w(e)});return Object.defineProperty(e,b,{configurable:!0,value:l}),l},y=document.currentScript;w(y?y.getRootNode():document)}();
|
|
</script></foreignObject></svg></div><script>/*!! License: https://unpkg.com/@marp-team/marp-cli@3.4.0/lib/bespoke.js.LICENSE.txt */
|
|
!function(){"use strict";function e(e){return e&&e.__esModule&&Object.prototype.hasOwnProperty.call(e,"default")?e.default:e}var t={from:function(e,t){var n,r=1===(e.parent||e).nodeType?e.parent||e:document.querySelector(e.parent||e),o=[].filter.call("string"==typeof e.slides?r.querySelectorAll(e.slides):e.slides||r.children,(function(e){return"SCRIPT"!==e.nodeName})),i={},a=function(e,t){return(t=t||{}).index=o.indexOf(e),t.slide=e,t},s=function(e,t){i[e]=(i[e]||[]).filter((function(e){return e!==t}))},l=function(e,t){return(i[e]||[]).reduce((function(e,n){return e&&!1!==n(t)}),!0)},c=function(e,t){o[e]&&(n&&l("deactivate",a(n,t)),n=o[e],l("activate",a(n,t)))},d=function(e,t){var r=o.indexOf(n)+e;l(e>0?"next":"prev",a(n,t))&&c(r,t)},u={off:s,on:function(e,t){return(i[e]||(i[e]=[])).push(t),s.bind(null,e,t)},fire:l,slide:function(e,t){if(!arguments.length)return o.indexOf(n);l("slide",a(o[e],t))&&c(e,t)},next:d.bind(null,1),prev:d.bind(null,-1),parent:r,slides:o,destroy:function(e){l("destroy",a(n,e)),i={}}};return(t||[]).forEach((function(e){e(u)})),n||c(0),u}},n=e(t);const r=document.body,o=(...e)=>history.replaceState(...e),i="presenter",a="next",s=["",i,a],l="bespoke-marp-",c=`data-${l}`,d=(e,{protocol:t,host:n,pathname:r,hash:o}=location)=>{const i=e.toString();return`${t}//${n}${r}${i?"?":""}${i}${o}`},u=()=>r.dataset.bespokeView,f=e=>new URLSearchParams(location.search).get(e),m=(e,t={})=>{var n;const r={location,setter:o,...t},i=new URLSearchParams(r.location.search);for(const t of Object.keys(e)){const n=e[t];"string"==typeof n?i.set(t,n):i.delete(t)}try{r.setter({...null!==(n=window.history.state)&&void 0!==n?n:{}},"",d(i,r.location))}catch(e){console.error(e)}},g=(()=>{const e="bespoke-marp";try{return localStorage.setItem(e,e),localStorage.removeItem(e),!0}catch(e){return!1}})(),p=e=>{try{return localStorage.getItem(e)}catch(e){return null}},v=(e,t)=>{try{return localStorage.setItem(e,t),!0}catch(e){return!1}},h=e=>{try{return localStorage.removeItem(e),!0}catch(e){return!1}},y=(e,t)=>{const n="aria-hidden";t?e.setAttribute(n,"true"):e.removeAttribute(n)},b=e=>{e.parent.classList.add(`${l}parent`),e.slides.forEach((e=>e.classList.add(`${l}slide`))),e.on("activate",(t=>{const n=`${l}active`,r=t.slide,o=r.classList,i=!o.contains(n);if(e.slides.forEach((e=>{e.classList.remove(n),y(e,!0)})),o.add(n),y(r,!1),i){const e=`${n}-ready`;o.add(e),document.body.clientHeight,o.remove(e)}}))},w=e=>{let t=0,n=0;Object.defineProperty(e,"fragments",{enumerable:!0,value:e.slides.map((e=>[null,...e.querySelectorAll("[data-marpit-fragment]")]))});const r=r=>void 0!==e.fragments[t][n+r],o=(r,o)=>{t=r,n=o,e.fragments.forEach(((e,t)=>{e.forEach(((e,n)=>{if(null==e)return;const i=t<r||t===r&&n<=o;e.setAttribute(`${c}fragment`,(i?"":"in")+"active");const a=`${c}current-fragment`;t===r&&n===o?e.setAttribute(a,"current"):e.removeAttribute(a)}))})),e.fragmentIndex=o;const i={slide:e.slides[r],index:r,fragments:e.fragments[r],fragmentIndex:o};e.fire("fragment",i)};e.on("next",(({fragment:i=!0})=>{if(i){if(r(1))return o(t,n+1),!1;const i=t+1;e.fragments[i]&&o(i,0)}else{const r=e.fragments[t].length;if(n+1<r)return o(t,r-1),!1;const i=e.fragments[t+1];i&&o(t+1,i.length-1)}})),e.on("prev",(({fragment:i=!0})=>{if(r(-1)&&i)return o(t,n-1),!1;const a=t-1;e.fragments[a]&&o(a,e.fragments[a].length-1)})),e.on("slide",(({index:t,fragment:n})=>{let r=0;if(void 0!==n){const o=e.fragments[t];if(o){const{length:e}=o;r=-1===n?e-1:Math.min(Math.max(n,0),e-1)}}o(t,r)})),o(0,0)},x=document,k=()=>!(!x.fullscreenEnabled&&!x.webkitFullscreenEnabled),$=()=>!(!x.fullscreenElement&&!x.webkitFullscreenElement),E=e=>{e.fullscreen=()=>{k()&&(async()=>{return $()?null===(e=x.exitFullscreen||x.webkitExitFullscreen)||void 0===e?void 0:e.call(x):((e=x.body)=>{var t;return null===(t=e.requestFullscreen||e.webkitRequestFullscreen)||void 0===t?void 0:t.call(e)})();var e})()},document.addEventListener("keydown",(t=>{"f"!==t.key&&"F11"!==t.key||t.altKey||t.ctrlKey||t.metaKey||!k()||(e.fullscreen(),t.preventDefault())}))},L=`${l}inactive`,S=(e=2e3)=>({parent:t,fire:n})=>{const r=t.classList,o=e=>n(`marp-${e?"":"in"}active`);let i;const a=()=>{i&&clearTimeout(i),i=setTimeout((()=>{r.add(L),o()}),e),r.contains(L)&&(r.remove(L),o(!0))};for(const e of["mousedown","mousemove","touchend"])document.addEventListener(e,a);setTimeout(a,0)},P=["AUDIO","BUTTON","INPUT","SELECT","TEXTAREA","VIDEO"],_=e=>{e.parent.addEventListener("keydown",(e=>{if(!e.target)return;const t=e.target;(P.includes(t.nodeName)||"true"===t.contentEditable)&&e.stopPropagation()}))},T=e=>{window.addEventListener("load",(()=>{for(const t of e.slides){const e=t.querySelector("marp-auto-scaling, [data-auto-scaling], [data-marp-fitting]");t.setAttribute(`${c}load`,e?"":"hideable")}}))},I=({interval:e=250}={})=>t=>{document.addEventListener("keydown",(e=>{if(" "===e.key&&e.shiftKey)t.prev();else if("ArrowLeft"===e.key||"ArrowUp"===e.key||"PageUp"===e.key)t.prev({fragment:!e.shiftKey});else if(" "!==e.key||e.shiftKey)if("ArrowRight"===e.key||"ArrowDown"===e.key||"PageDown"===e.key)t.next({fragment:!e.shiftKey});else if("End"===e.key)t.slide(t.slides.length-1,{fragment:-1});else{if("Home"!==e.key)return;t.slide(0)}else t.next();e.preventDefault()}));let n,r,o=0;t.parent.addEventListener("wheel",(i=>{let a=!1;const s=(e,t)=>{e&&(a=a||((e,t)=>((e,t)=>{const n="X"===t?"Width":"Height";return e[`client${n}`]<e[`scroll${n}`]})(e,t)&&((e,t)=>{const{overflow:n}=e,r=e[`overflow${t}`];return"auto"===n||"scroll"===n||"auto"===r||"scroll"===r})(getComputedStyle(e),t))(e,t)),(null==e?void 0:e.parentElement)&&s(e.parentElement,t)};if(0!==i.deltaX&&s(i.target,"X"),0!==i.deltaY&&s(i.target,"Y"),a)return;i.preventDefault();const l=Math.sqrt(i.deltaX**2+i.deltaY**2);if(void 0!==i.wheelDelta){if(void 0===i.webkitForce&&Math.abs(i.wheelDelta)<40)return;if(i.deltaMode===i.DOM_DELTA_PIXEL&&l<4)return}else if(i.deltaMode===i.DOM_DELTA_PIXEL&&l<12)return;r&&clearTimeout(r),r=setTimeout((()=>{n=0}),e);const c=Date.now()-o<e,d=l<=n;if(n=l,c||d)return;let u;(i.deltaX>0||i.deltaY>0)&&(u="next"),(i.deltaX<0||i.deltaY<0)&&(u="prev"),u&&(t[u](),o=Date.now())}))},M=(e=`.${l}osc`)=>{const t=document.querySelector(e);if(!t)return()=>{};const n=(e,n)=>{t.querySelectorAll(`[${c}osc=${JSON.stringify(e)}]`).forEach(n)};return k()||n("fullscreen",(e=>e.style.display="none")),g||n("presenter",(e=>{e.disabled=!0,e.title="Presenter view is disabled due to restricted localStorage."})),e=>{t.addEventListener("click",(t=>{if(t.target instanceof HTMLElement){const{bespokeMarpOsc:n}=t.target.dataset;n&&t.target.blur();const r={fragment:!t.shiftKey};"next"===n?e.next(r):"prev"===n?e.prev(r):"fullscreen"===n?null==e||e.fullscreen():"presenter"===n&&e.openPresenterView()}})),e.parent.appendChild(t),e.on("activate",(({index:t})=>{n("page",(n=>n.textContent=`Page ${t+1} of ${e.slides.length}`))})),e.on("fragment",(({index:t,fragments:r,fragmentIndex:o})=>{n("prev",(e=>e.disabled=0===t&&0===o)),n("next",(n=>n.disabled=t===e.slides.length-1&&o===r.length-1))})),e.on("marp-active",(()=>y(t,!1))),e.on("marp-inactive",(()=>y(t,!0))),k()&&(e=>{for(const t of["","webkit"])x.addEventListener(t+"fullscreenchange",e)})((()=>n("fullscreen",(e=>e.classList.toggle("exit",k()&&$())))))}},O=e=>{window.addEventListener("message",(t=>{if(t.origin!==window.origin)return;const[n,r]=t.data.split(":");if("navigate"===n){const[t,n]=r.split(",");let o=Number.parseInt(t,10),i=Number.parseInt(n,10)+1;i>=e.fragments[o].length&&(o+=1,i=0),e.slide(o,{fragment:i})}}))};var A=["area","base","br","col","command","embed","hr","img","input","keygen","link","meta","param","source","track","wbr"];let C=e=>String(e).replace(/[&<>"']/g,(e=>`&${D[e]};`)),D={"&":"amp","<":"lt",">":"gt",'"':"quot","'":"apos"},N="dangerouslySetInnerHTML",B={className:"class",htmlFor:"for"},q={};function K(e,t){let n=[],r="";t=t||{};for(let e=arguments.length;e-- >2;)n.push(arguments[e]);if("function"==typeof e)return t.children=n.reverse(),e(t);if(e){if(r+="<"+e,t)for(let e in t)!1!==t[e]&&null!=t[e]&&e!==N&&(r+=` ${B[e]?B[e]:C(e)}="${C(t[e])}"`);r+=">"}if(-1===A.indexOf(e)){if(t[N])r+=t[N].__html;else for(;n.length;){let e=n.pop();if(e)if(e.pop)for(let t=e.length;t--;)n.push(e[t]);else r+=!0===q[e]?e:C(e)}r+=e?`</${e}>`:""}return q[r]=!0,r}const j=({children:e})=>K(null,null,...e),F=`${l}presenter-`,V={container:`${F}container`,dragbar:`${F}dragbar-container`,next:`${F}next`,nextContainer:`${F}next-container`,noteContainer:`${F}note-container`,noteWrapper:`${F}note-wrapper`,noteButtons:`${F}note-buttons`,infoContainer:`${F}info-container`,infoPage:`${F}info-page`,infoPageText:`${F}info-page-text`,infoPagePrev:`${F}info-page-prev`,infoPageNext:`${F}info-page-next`,noteButtonsBigger:`${F}note-bigger`,noteButtonsSmaller:`${F}note-smaller`,infoTime:`${F}info-time`,infoTimer:`${F}info-timer`},U=e=>{const{title:t}=document;document.title="[Presenter view]"+(t?` - ${t}`:"");const n={},r=e=>(n[e]=n[e]||document.querySelector(`.${e}`),n[e]);document.body.appendChild((e=>{const t=document.createElement("div");return t.className=V.container,t.appendChild(e),t.insertAdjacentHTML("beforeend",K(j,null,K("div",{class:V.nextContainer},K("iframe",{class:V.next,src:"?view=next"})),K("div",{class:V.dragbar}),K("div",{class:V.noteContainer},K("div",{class:V.noteWrapper}),K("div",{class:V.noteButtons},K("button",{class:V.noteButtonsSmaller,tabindex:"-1",title:"Smaller notes font size"},"Smaller notes font size"),K("button",{class:V.noteButtonsBigger,tabindex:"-1",title:"Bigger notes font size"},"Bigger notes font size"))),K("div",{class:V.infoContainer},K("div",{class:V.infoPage},K("button",{class:V.infoPagePrev,tabindex:"-1",title:"Previous"},"Previous"),K("span",{class:V.infoPageText}),K("button",{class:V.infoPageNext,tabindex:"-1",title:"Next"},"Next")),K("time",{class:V.infoTime,title:"Current time"}),K("time",{class:V.infoTimer,title:"Timer"})))),t})(e.parent)),(e=>{let t=!1;r(V.dragbar).addEventListener("mousedown",(()=>{t=!0,r(V.dragbar).classList.add("active")})),window.addEventListener("mouseup",(()=>{t=!1,r(V.dragbar).classList.remove("active")})),window.addEventListener("mousemove",(e=>{if(!t)return;const n=e.clientX/document.documentElement.clientWidth*100;r(V.container).style.setProperty("--bespoke-marp-presenter-split-ratio",`${Math.max(0,Math.min(100,n))}%`)})),r(V.nextContainer).addEventListener("click",(()=>e.next()));const n=r(V.next),o=(i=n,(e,t)=>{var n;return null===(n=i.contentWindow)||void 0===n?void 0:n.postMessage(`navigate:${e},${t}`,"null"===window.origin?"*":window.origin)});var i;n.addEventListener("load",(()=>{r(V.nextContainer).classList.add("active"),o(e.slide(),e.fragmentIndex),e.on("fragment",(({index:e,fragmentIndex:t})=>o(e,t)))}));const a=document.querySelectorAll(".bespoke-marp-note");a.forEach((e=>{e.addEventListener("keydown",(e=>e.stopPropagation())),r(V.noteWrapper).appendChild(e)})),e.on("activate",(()=>a.forEach((t=>t.classList.toggle("active",t.dataset.index==e.slide())))));let s=0;const l=e=>{s=Math.max(-5,s+e),r(V.noteContainer).style.setProperty("--bespoke-marp-note-font-scale",(1.2**s).toFixed(4))},c=()=>l(1),d=()=>l(-1),u=r(V.noteButtonsBigger),f=r(V.noteButtonsSmaller);u.addEventListener("click",(()=>{u.blur(),c()})),f.addEventListener("click",(()=>{f.blur(),d()})),document.addEventListener("keydown",(e=>{"+"===e.key&&c(),"-"===e.key&&d()}),!0),e.on("activate",(({index:t})=>{r(V.infoPageText).textContent=`${t+1} / ${e.slides.length}`}));const m=r(V.infoPagePrev),g=r(V.infoPageNext);m.addEventListener("click",(t=>{m.blur(),e.prev({fragment:!t.shiftKey})})),g.addEventListener("click",(t=>{g.blur(),e.next({fragment:!t.shiftKey})})),e.on("fragment",(({index:t,fragments:n,fragmentIndex:r})=>{m.disabled=0===t&&0===r,g.disabled=t===e.slides.length-1&&r===n.length-1}));let p=new Date;const v=()=>{const e=new Date,t=e=>`${Math.floor(e)}`.padStart(2,"0"),n=e.getTime()-p.getTime(),o=t(n/1e3%60),i=t(n/1e3/60%60),a=t(n/36e5%24);r(V.infoTime).textContent=e.toLocaleTimeString(),r(V.infoTimer).textContent=`${a}:${i}:${o}`};v(),setInterval(v,250),r(V.infoTimer).addEventListener("click",(()=>{p=new Date}))})(e)},X=e=>{if(!(e=>e.syncKey&&"string"==typeof e.syncKey)(e))throw new Error("The current instance of Bespoke.js is invalid for Marp bespoke presenter plugin.");Object.defineProperties(e,{openPresenterView:{enumerable:!0,value:H},presenterUrl:{enumerable:!0,get:R}}),g&&document.addEventListener("keydown",(t=>{"p"!==t.key||t.altKey||t.ctrlKey||t.metaKey||(t.preventDefault(),e.openPresenterView())}))};function H(){const{max:e,floor:t}=Math,n=e(t(.85*window.innerWidth),640),r=e(t(.85*window.innerHeight),360);return window.open(this.presenterUrl,F+this.syncKey,`width=${n},height=${r},menubar=no,toolbar=no`)}function R(){const e=new URLSearchParams(location.search);return e.set("view","presenter"),e.set("sync",this.syncKey),d(e)}const W=e=>{const t=u();return t===a&&e.appendChild(document.createElement("span")),{"":X,[i]:U,[a]:O}[t]},J=e=>{e.on("activate",(t=>{document.querySelectorAll(".bespoke-progress-parent > .bespoke-progress-bar").forEach((n=>{n.style.flexBasis=100*t.index/(e.slides.length-1)+"%"}))}))},Y=e=>{const t=Number.parseInt(e,10);return Number.isNaN(t)?null:t},z=(e={})=>{const t={history:!0,...e};return e=>{let n=!0;const r=e=>{const t=n;try{return n=!0,e()}finally{n=t}},o=(t={fragment:!0})=>{let n=t.fragment?Y(f("f")||""):null;((t,n)=>{const{min:r,max:o}=Math,{fragments:i,slides:a}=e,s=o(0,r(t,a.length-1)),l=o(0,r(n||0,i[s].length-1));s===e.slide()&&l===e.fragmentIndex||e.slide(s,{fragment:l})})((()=>{var t,r;if(location.hash){const[o]=location.hash.slice(1).split(":~:");if(/^\d+$/.test(o))return(null!==(t=Y(o))&&void 0!==t?t:1)-1;const i=document.getElementById(o)||document.querySelector(`a[name="${CSS.escape(o)}"]`);if(i){const{length:t}=e.slides;for(let o=0;o<t;o+=1)if(e.slides[o].contains(i)){const t=null===(r=e.fragments)||void 0===r?void 0:r[o],a=i.closest("[data-marpit-fragment]");if(t&&a){const e=t.indexOf(a);e>=0&&(n=e)}return o}}}return 0})(),n)};e.on("fragment",(({index:e,fragmentIndex:r})=>{n||m({f:0===r||r.toString()},{location:{...location,hash:`#${e+1}`},setter:(...e)=>t.history?history.pushState(...e):history.replaceState(...e)})})),setTimeout((()=>{o(),window.addEventListener("hashchange",(()=>r((()=>{o({fragment:!1}),m({f:void 0})})))),window.addEventListener("popstate",(()=>{n||r((()=>o()))})),n=!1}),0)}},G=(e={})=>{var t;const n=e.key||(null===(t=window.history.state)||void 0===t?void 0:t.marpBespokeSyncKey)||Math.random().toString(36).slice(2),r=`bespoke-marp-sync-${n}`;var i;i={marpBespokeSyncKey:n},m({},{setter:(e,...t)=>o({...e,...i},...t)});const a=()=>{const e=p(r);return e?JSON.parse(e):Object.create(null)},s=e=>{const t=a(),n={...t,...e(t)};return v(r,JSON.stringify(n)),n},l=()=>{window.removeEventListener("pageshow",l),s((e=>({reference:(e.reference||0)+1})))};return e=>{l(),Object.defineProperty(e,"syncKey",{value:n,enumerable:!0});let t=!0;setTimeout((()=>{e.on("fragment",(e=>{t&&s((()=>({index:e.index,fragmentIndex:e.fragmentIndex})))}))}),0),window.addEventListener("storage",(n=>{if(n.key===r&&n.oldValue&&n.newValue){const r=JSON.parse(n.oldValue),o=JSON.parse(n.newValue);if(r.index!==o.index||r.fragmentIndex!==o.fragmentIndex)try{t=!1,e.slide(o.index,{fragment:o.fragmentIndex,forSync:!0})}finally{t=!0}}}));const o=()=>{const{reference:e}=a();void 0===e||e<=1?h(r):s((()=>({reference:e-1})))};window.addEventListener("pagehide",(e=>{e.persisted&&window.addEventListener("pageshow",l),o()})),e.on("destroy",o)}},{PI:Q,abs:Z,sqrt:ee,atan2:te}=Math,ne={passive:!0},re=({slope:e=-.7,swipeThreshold:t=30}={})=>n=>{let r;const o=n.parent,i=e=>{const t=o.getBoundingClientRect();return{x:e.pageX-(t.left+t.right)/2,y:e.pageY-(t.top+t.bottom)/2}};o.addEventListener("touchstart",(({touches:e})=>{r=1===e.length?i(e[0]):void 0}),ne),o.addEventListener("touchmove",(e=>{if(r)if(1===e.touches.length){e.preventDefault();const t=i(e.touches[0]),n=t.x-r.x,o=t.y-r.y;r.delta=ee(Z(n)**2+Z(o)**2),r.radian=te(n,o)}else r=void 0})),o.addEventListener("touchend",(o=>{if(r){if(r.delta&&r.delta>=t&&r.radian){const t=(r.radian-e+Q)%(2*Q)-Q;n[t<0?"next":"prev"](),o.stopPropagation()}r=void 0}}),ne)},oe=new Map;oe.clear(),oe.set("none",{backward:{both:void 0,incoming:void 0,outgoing:void 0},forward:{both:void 0,incoming:void 0,outgoing:void 0}});const ie={both:"",outgoing:"outgoing-",incoming:"incoming-"},ae={forward:"",backward:"-backward"},se=e=>`--marp-bespoke-transition-animation-${e}`,le=e=>`--marp-transition-${e}`,ce=se("name"),de=se("duration"),ue=e=>new Promise((t=>{const n={},r=document.createElement("div"),o=e=>{r.remove(),t(e)};r.addEventListener("animationstart",(()=>o(n))),Object.assign(r.style,{animationName:e,animationDuration:"1s",animationFillMode:"both",animationPlayState:"paused",position:"absolute",pointerEvents:"none"}),document.body.appendChild(r);const i=getComputedStyle(r).getPropertyValue(le("duration"));i&&(n.defaultDuration=i),((e,t)=>{requestAnimationFrame((()=>{e.style.animationPlayState="running",requestAnimationFrame((()=>t(void 0)))}))})(r,o)})),fe=async e=>oe.has(e)?oe.get(e):(e=>{const t={},n=[];for(const[r,o]of Object.entries(ie))for(const[i,a]of Object.entries(ae)){const s=`marp-${o}transition${a}-${e}`;n.push(ue(s).then((e=>{t[i]=t[i]||{},t[i][r]=e?{...e,name:s}:void 0})))}return Promise.all(n).then((()=>t))})(e).then((t=>(oe.set(e,t),t))),me=e=>Object.values(e).flatMap(Object.values).every((e=>!e)),ge=(e,{type:t,backward:n})=>{const r=e[n?"backward":"forward"],o=(()=>{const e=r[t],n=e=>({[ce]:e.name});if(e)return n(e);if(r.both){const e=n(r.both);return"incoming"===t&&(e[se("direction")]="reverse"),e}})();return!o&&n?ge(e,{type:t,backward:!1}):o||{[ce]:"__bespoke_marp_transition_no_animation__"}},pe=e=>{if(e)try{const t=JSON.parse(e);if((e=>{if("object"!=typeof e)return!1;const t=e;return"string"==typeof t.name&&(void 0===t.duration||"string"==typeof t.duration)})(t))return t}catch(e){}},ve="_tSId",he="_tA",ye="bespoke-marp-transition-warming-up",be=window.matchMedia("(prefers-reduced-motion: reduce)"),we="__bespoke_marp_transition_reduced_outgoing__",xe="__bespoke_marp_transition_reduced_incoming__",ke={forward:{both:void 0,incoming:{name:xe},outgoing:{name:we}},backward:{both:void 0,incoming:{name:xe},outgoing:{name:we}}},$e=e=>{if(!document.startViewTransition)return;const t=t=>(void 0!==t&&(e._tD=t),e._tD);let n;t(!1),((...e)=>{const t=[...new Set(e).values()];return Promise.all(t.map((e=>fe(e)))).then()})(...Array.from(document.querySelectorAll("section[data-transition], section[data-transition-back]")).flatMap((e=>[e.dataset.transition,e.dataset.transitionBack].flatMap((e=>{const t=pe(e);return[null==t?void 0:t.name,(null==t?void 0:t.builtinFallback)?`__builtin__${t.name}`:void 0]})).filter((e=>!!e))))).then((()=>{document.querySelectorAll("style").forEach((e=>{e.innerHTML=e.innerHTML.replace(/--marp-transition-duration:[^;}]*[;}]/g,(e=>e.slice(0,-1)+"!important"+e.slice(-1)))}))}));const r=(n,{back:r,cond:o})=>i=>{var a;const s=t();if(s)return!!i[he]||!("object"!=typeof s||(s.skipTransition(),!i.forSync));if(!o(i))return!0;const l=e.slides[e.slide()],c=()=>{var e;return null!==(e=i.back)&&void 0!==e?e:r},d="data-transition"+(c()?"-back":""),u=l.querySelector(`section[${d}]`);if(!u)return!0;const f=pe(null!==(a=u.getAttribute(d))&&void 0!==a?a:void 0);return!f||((async(e,{builtinFallback:t=!0}={})=>{let n=await fe(e);if(me(n)){if(!t)return;return n=await fe(`__builtin__${e}`),me(n)?void 0:n}return n})(f.name,{builtinFallback:f.builtinFallback}).then((e=>{if(!e){t(!0);try{n(i)}finally{t(!1)}return}let r=e;be.matches&&(console.warn("Use a constant animation to transition because preferring reduced motion by viewer has detected."),r=ke);const o=document.getElementById(ve);o&&o.remove();const a=document.createElement("style");a.id=ve,document.head.appendChild(a),((e,t)=>{const n=[`:root{${le("direction")}:${t.backward?-1:1};}`,":root:has(.bespoke-marp-inactive){cursor:none;}"],r=t=>{var n,o,i;const a=(null===(n=e[t].both)||void 0===n?void 0:n.defaultDuration)||(null===(o=e[t].outgoing)||void 0===o?void 0:o.defaultDuration)||(null===(i=e[t].incoming)||void 0===i?void 0:i.defaultDuration);return"forward"===t?a:a||r("forward")},o=t.duration||r(t.backward?"backward":"forward");void 0!==o&&n.push(`::view-transition-group(*){${de}:${o};}`);const i=e=>Object.entries(e).map((([e,t])=>`${e}:${t};`)).join("");return n.push(`::view-transition-old(root){${i(ge(e,{...t,type:"outgoing"}))}}`,`::view-transition-new(root){${i(ge(e,{...t,type:"incoming"}))}}`),n})(r,{backward:c(),duration:f.duration}).forEach((e=>{var t;return null===(t=a.sheet)||void 0===t?void 0:t.insertRule(e)}));const s=document.documentElement.classList;s.add(ye);let l=!1;const d=()=>{l||(n(i),l=!0,s.remove(ye))},u=()=>{t(!1),a.remove(),s.remove(ye)};try{t(!0);const e=document.startViewTransition(d);t(e),e.finished.finally(u)}catch(e){console.error(e),d(),u()}})),!1)};e.on("prev",r((t=>e.prev({...t,[he]:!0})),{back:!0,cond:e=>{var t;return e.index>0&&!((null===(t=e.fragment)||void 0===t||t)&&n.fragmentIndex>0)}})),e.on("next",r((t=>e.next({...t,[he]:!0})),{cond:t=>t.index+1<e.slides.length&&!(n.fragmentIndex+1<n.fragments.length)})),setTimeout((()=>{e.on("slide",r((t=>e.slide(t.index,{...t,[he]:!0})),{cond:t=>{const n=e.slide();return t.index!==n&&(t.back=t.index<n,!0)}}))}),0),e.on("fragment",(e=>{n=e}))};let Ee;const Le=()=>(void 0===Ee&&(Ee="wakeLock"in navigator&&navigator.wakeLock),Ee),Se=async()=>{const e=Le();if(e)try{return await e.request("screen")}catch(e){console.warn(e)}return null},Pe=async()=>{if(!Le())return;let e;const t=()=>{e&&"visible"===document.visibilityState&&Se()};for(const e of["visibilitychange","fullscreenchange"])document.addEventListener(e,t);return e=await Se(),e};((e=document.getElementById(":$p"))=>{(()=>{const e=f("view");r.dataset.bespokeView=e===a||e===i?e:""})();const t=(e=>{const t=f(e);return m({[e]:void 0}),t})("sync")||void 0;n.from(e,((...e)=>{const t=s.findIndex((e=>u()===e));return e.map((([e,n])=>e[t]&&n)).filter((e=>e))})([[1,1,0],G({key:t})],[[1,1,1],W(e)],[[1,1,0],_],[[1,1,1],b],[[1,0,0],S()],[[1,1,1],T],[[1,1,1],z({history:!1})],[[1,1,0],I()],[[1,1,0],E],[[1,0,0],J],[[1,1,0],re()],[[1,0,0],M()],[[1,0,0],$e],[[1,1,1],w],[[1,1,0],Pe]))})()}();</script></body></html> |