CINXE.COM
Testing Your Code with Python's pytest, Part II | Linux Journal
<!DOCTYPE html> <html lang="en" dir="ltr" prefix="content: http://purl.org/rss/1.0/modules/content/ dc: http://purl.org/dc/terms/ foaf: http://xmlns.com/foaf/0.1/ og: http://ogp.me/ns# rdfs: http://www.w3.org/2000/01/rdf-schema# schema: http://schema.org/ sioc: http://rdfs.org/sioc/ns# sioct: http://rdfs.org/sioc/types# skos: http://www.w3.org/2004/02/skos/core# xsd: http://www.w3.org/2001/XMLSchema# " class="no-js wf-loading"> <head> <meta charset="utf-8" /> <meta name="Generator" content="Drupal 9 (https://www.drupal.org)" /> <meta name="MobileOptimized" content="width" /> <meta name="HandheldFriendly" content="true" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <link rel="icon" href="/themes/linuxjournal/favicon.ico" type="image/vnd.microsoft.icon" /> <link rel="canonical" href="https://www.linuxjournal.com/content/testing-your-code-pythons-pytest-part-ii" /> <link rel="shortlink" href="https://www.linuxjournal.com/node/1340256" /> <title>Testing Your Code with Python's pytest, Part II | Linux Journal</title> <link rel="stylesheet" media="all" href="/core/modules/system/css/components/align.module.css?smwnh4" /> <link rel="stylesheet" media="all" href="/core/modules/system/css/components/fieldgroup.module.css?smwnh4" /> <link rel="stylesheet" media="all" href="/core/modules/system/css/components/container-inline.module.css?smwnh4" /> <link rel="stylesheet" media="all" href="/core/modules/system/css/components/clearfix.module.css?smwnh4" /> <link rel="stylesheet" media="all" href="/core/modules/system/css/components/details.module.css?smwnh4" /> <link rel="stylesheet" media="all" href="/core/modules/system/css/components/hidden.module.css?smwnh4" /> <link rel="stylesheet" media="all" href="/core/modules/system/css/components/item-list.module.css?smwnh4" /> <link rel="stylesheet" media="all" href="/core/modules/system/css/components/js.module.css?smwnh4" /> <link rel="stylesheet" media="all" href="/core/modules/system/css/components/nowrap.module.css?smwnh4" /> <link rel="stylesheet" media="all" href="/core/modules/system/css/components/position-container.module.css?smwnh4" /> <link rel="stylesheet" media="all" href="/core/modules/system/css/components/progress.module.css?smwnh4" /> <link rel="stylesheet" media="all" href="/core/modules/system/css/components/reset-appearance.module.css?smwnh4" /> <link rel="stylesheet" media="all" href="/core/modules/system/css/components/resize.module.css?smwnh4" /> <link rel="stylesheet" media="all" href="/core/modules/system/css/components/sticky-header.module.css?smwnh4" /> <link rel="stylesheet" media="all" href="/core/modules/system/css/components/system-status-counter.css?smwnh4" /> <link rel="stylesheet" media="all" href="/core/modules/system/css/components/system-status-report-counters.css?smwnh4" /> <link rel="stylesheet" media="all" href="/core/modules/system/css/components/system-status-report-general-info.css?smwnh4" /> <link rel="stylesheet" media="all" href="/core/modules/system/css/components/tablesort.module.css?smwnh4" /> <link rel="stylesheet" media="all" href="/core/modules/system/css/components/tree-child.module.css?smwnh4" /> <link rel="stylesheet" media="all" href="/modules/contrib/poll/css/poll.base.css?smwnh4" /> <link rel="stylesheet" media="all" href="/modules/contrib/poll/css/poll.theme.css?smwnh4" /> <link rel="stylesheet" media="all" href="/core/modules/views/css/views.module.css?smwnh4" /> <link rel="stylesheet" media="all" href="/libraries/shariff/shariff.complete.css?smwnh4" /> <link rel="stylesheet" media="all" href="/modules/contrib/webform/modules/webform_bootstrap/css/webform_bootstrap.css?smwnh4" /> <link rel="stylesheet" media="all" href="/themes/linuxjournal/css/style.css?smwnh4" /> <link rel="stylesheet" media="all" href="/themes/linuxjournal/css/fonts.css?smwnh4" /> <!-- <script defer src="https://use.fontawesome.com/releases/v5.0.9/js/all.js" integrity="sha384-8iPTk2s/jMVj81dnzb/iFR2sdA7u06vHJyyLlAd4snFpCl/SnyUjRrbdJsw1pGIl" crossorigin="anonymous"></script>--> <script> // Non-blocking webfonts. (function iife() { 'use strict'; // JS is able to start executing. document.documentElement.classList.remove('no-js'); // Optimization for Repeat Views // Stop early for repeat views that have already cached fonts. if (sessionStorage.fontsLoadedLateef && sessionStorage.fontsLoadedOpensans /* && sessionStorage.fontsLoadedNEWFONT */) { document.documentElement.classList.remove('wf-loading'); document.documentElement.classList.add('wf-opensans'); document.documentElement.classList.add('wf-lateef'); // document.documentElement.classList.add('wf-NEWFONT'); return; } // Inlined Font Face Observer script + Promise polyfill. /*! Font Face Observer v2.0.13 - © Bram Stein. License: BSD-3-Clause */ (function(){'use strict';var f,g=[];function l(a){g.push(a);1==g.length&&f()}function m(){for(;g.length;)g[0](),g.shift()}f=function(){setTimeout(m)};function n(a){this.a=p;this.b=void 0;this.f=[];var b=this;try{a(function(a){q(b,a)},function(a){r(b,a)})}catch(c){r(b,c)}}var p=2;function t(a){return new n(function(b,c){c(a)})}function u(a){return new n(function(b){b(a)})}function q(a,b){if(a.a==p){if(b==a)throw new TypeError;var c=!1;try{var d=b&&b.then;if(null!=b&&"object"==typeof b&&"function"==typeof d){d.call(b,function(b){c||q(a,b);c=!0},function(b){c||r(a,b);c=!0});return}}catch(e){c||r(a,e);return}a.a=0;a.b=b;v(a)}} function r(a,b){if(a.a==p){if(b==a)throw new TypeError;a.a=1;a.b=b;v(a)}}function v(a){l(function(){if(a.a!=p)for(;a.f.length;){var b=a.f.shift(),c=b[0],d=b[1],e=b[2],b=b[3];try{0==a.a?"function"==typeof c?e(c.call(void 0,a.b)):e(a.b):1==a.a&&("function"==typeof d?e(d.call(void 0,a.b)):b(a.b))}catch(h){b(h)}}})}n.prototype.g=function(a){return this.c(void 0,a)};n.prototype.c=function(a,b){var c=this;return new n(function(d,e){c.f.push([a,b,d,e]);v(c)})}; function w(a){return new n(function(b,c){function d(c){return function(d){h[c]=d;e+=1;e==a.length&&b(h)}}var e=0,h=[];0==a.length&&b(h);for(var k=0;k<a.length;k+=1)u(a[k]).c(d(k),c)})}function x(a){return new n(function(b,c){for(var d=0;d<a.length;d+=1)u(a[d]).c(b,c)})};window.Promise||(window.Promise=n,window.Promise.resolve=u,window.Promise.reject=t,window.Promise.race=x,window.Promise.all=w,window.Promise.prototype.then=n.prototype.c,window.Promise.prototype["catch"]=n.prototype.g);}()); (function(){function l(a,b){document.addEventListener?a.addEventListener("scroll",b,!1):a.attachEvent("scroll",b)}function m(a){document.body?a():document.addEventListener?document.addEventListener("DOMContentLoaded",function c(){document.removeEventListener("DOMContentLoaded",c);a()}):document.attachEvent("onreadystatechange",function k(){if("interactive"==document.readyState||"complete"==document.readyState)document.detachEvent("onreadystatechange",k),a()})};function r(a){this.a=document.createElement("div");this.a.setAttribute("aria-hidden","true");this.a.appendChild(document.createTextNode(a));this.b=document.createElement("span");this.c=document.createElement("span");this.h=document.createElement("span");this.f=document.createElement("span");this.g=-1;this.b.style.cssText="max-width:none;display:inline-block;position:absolute;height:100%;width:100%;overflow:scroll;font-size:16px;";this.c.style.cssText="max-width:none;display:inline-block;position:absolute;height:100%;width:100%;overflow:scroll;font-size:16px;"; this.f.style.cssText="max-width:none;display:inline-block;position:absolute;height:100%;width:100%;overflow:scroll;font-size:16px;";this.h.style.cssText="display:inline-block;width:200%;height:200%;font-size:16px;max-width:none;";this.b.appendChild(this.h);this.c.appendChild(this.f);this.a.appendChild(this.b);this.a.appendChild(this.c)} function t(a,b){a.a.style.cssText="max-width:none;min-width:20px;min-height:20px;display:inline-block;overflow:hidden;position:absolute;width:auto;margin:0;padding:0;top:-999px;white-space:nowrap;font-synthesis:none;font:"+b+";"}function y(a){var b=a.a.offsetWidth,c=b+100;a.f.style.width=c+"px";a.c.scrollLeft=c;a.b.scrollLeft=a.b.scrollWidth+100;return a.g!==b?(a.g=b,!0):!1}function z(a,b){function c(){var a=k;y(a)&&a.a.parentNode&&b(a.g)}var k=a;l(a.b,c);l(a.c,c);y(a)};function A(a,b){var c=b||{};this.family=a;this.style=c.style||"normal";this.weight=c.weight||"normal";this.stretch=c.stretch||"normal"}var B=null,C=null,E=null,F=null;function G(){if(null===C)if(J()&&/Apple/.test(window.navigator.vendor)){var a=/AppleWebKit\/([0-9]+)(?:\.([0-9]+))(?:\.([0-9]+))/.exec(window.navigator.userAgent);C=!!a&&603>parseInt(a[1],10)}else C=!1;return C}function J(){null===F&&(F=!!document.fonts);return F} function K(){if(null===E){var a=document.createElement("div");try{a.style.font="condensed 100px sans-serif"}catch(b){}E=""!==a.style.font}return E}function L(a,b){return[a.style,a.weight,K()?a.stretch:"","100px",b].join(" ")} A.prototype.load=function(a,b){var c=this,k=a||"BESbswy",q=0,D=b||3E3,H=(new Date).getTime();return new Promise(function(a,b){if(J()&&!G()){var M=new Promise(function(a,b){function e(){(new Date).getTime()-H>=D?b():document.fonts.load(L(c,'"'+c.family+'"'),k).then(function(c){1<=c.length?a():setTimeout(e,25)},function(){b()})}e()}),N=new Promise(function(a,c){q=setTimeout(c,D)});Promise.race([N,M]).then(function(){clearTimeout(q);a(c)},function(){b(c)})}else m(function(){function u(){var b;if(b=-1!= f&&-1!=g||-1!=f&&-1!=h||-1!=g&&-1!=h)(b=f!=g&&f!=h&&g!=h)||(null===B&&(b=/AppleWebKit\/([0-9]+)(?:\.([0-9]+))/.exec(window.navigator.userAgent),B=!!b&&(536>parseInt(b[1],10)||536===parseInt(b[1],10)&&11>=parseInt(b[2],10))),b=B&&(f==v&&g==v&&h==v||f==w&&g==w&&h==w||f==x&&g==x&&h==x)),b=!b;b&&(d.parentNode&&d.parentNode.removeChild(d),clearTimeout(q),a(c))}function I(){if((new Date).getTime()-H>=D)d.parentNode&&d.parentNode.removeChild(d),b(c);else{var a=document.hidden;if(!0===a||void 0===a)f=e.a.offsetWidth, g=n.a.offsetWidth,h=p.a.offsetWidth,u();q=setTimeout(I,50)}}var e=new r(k),n=new r(k),p=new r(k),f=-1,g=-1,h=-1,v=-1,w=-1,x=-1,d=document.createElement("div");d.dir="ltr";t(e,L(c,"sans-serif"));t(n,L(c,"serif"));t(p,L(c,"monospace"));d.appendChild(e.a);d.appendChild(n.a);d.appendChild(p.a);document.body.appendChild(d);v=e.a.offsetWidth;w=n.a.offsetWidth;x=p.a.offsetWidth;I();z(e,function(a){f=a;u()});t(e,L(c,'"'+c.family+'",sans-serif'));z(n,function(a){g=a;u()});t(n,L(c,'"'+c.family+'",serif')); z(p,function(a){h=a;u()});t(p,L(c,'"'+c.family+'",monospace'))})})};"object"===typeof module?module.exports=A:(window.FontFaceObserver=A,window.FontFaceObserver.prototype.load=A.prototype.load);}()); // Load and observe Lateef var Lateef = new FontFaceObserver('Lateef', {weight: 400}); Promise.all([Lateef.load()]).then(function() { document.documentElement.classList.remove('wf-loading'); document.documentElement.classList.add('wf-lateef'); // Optimization for Repeat Views // Set a flag in localstorage so repeat views can skip processing FFO. sessionStorage.fontsLoadedLateef = true; }); // Load and observe Open Sans var os300 = new FontFaceObserver('Open Sans', {weight: 300}); var os400 = new FontFaceObserver('Open Sans', {weight: 400}); Promise.all([os300.load(), os400.load()]).then(function() { document.documentElement.classList.remove('wf-loading'); document.documentElement.classList.add('wf-opensans'); // Optimization for Repeat Views // Set a flag in localstorage so repeat views can skip processing FFO. sessionStorage.fontsLoadedOpensans = true; }); // Load and observe NEWFONT /* var NEWFONT = new FontFaceObserver('NEWFONT', {weight: 400}); Promise.all([NEWFONT.load()]).then(function() { document.documentElement.classList.remove('wf-loading'); document.documentElement.classList.add('wf-NEWFONT'); // Optimization for Repeat Views // Set a flag in localstorage so repeat views can skip processing FFO. sessionStorage.fontsLoadedNEWFONT = true; }); */ })(); </script> <script src="https://www.google.com/recaptcha/api.js" async defer></script> <script src=https://slashdot.org/country.js></script> <script src="/themes/linuxjournal/js/ada.js"></script> <script src="/themes/linuxjournal/js/accessibility.js"></script> <script>window.addEventListener('load', function() { new Accessibility(); });</script> <script src="//a.fsdn.com/con/js/sftheme/cmp2.js"></script> <link rel="stylesheet" href="//a.fsdn.com/con/css/sftheme/sandiego/cmp.css" type="text/css"> <link rel="stylesheet" href="/themes/linuxjournal/css/ccpa.css" type="text/css"> <script>window.bizx.cmp.init({ geo: window });</script> <script> function bm_trace() { (function (w,d,t) { _ml = w._ml || {}; _ml.eid = '771'; var s, cd, tag; s = d.getElementsByTagName(t)[0]; cd = new Date(); tag = d.createElement(t); tag.async = 1; tag.src = 'https://ml314.com/tag.aspx?' + cd.getDate() + cd.getMonth(); s.parentNode.insertBefore(tag, s); })(window,document,'script'); } bizx.cmp.ifConsent('', ['all', 'bombora'],bm_trace); </script> </head> <body class="path-node page-node-type-story has-glyphicons"> <a href="#main-content" class="visually-hidden focusable skip-link"> Skip to main content </a> <div class="dialog-off-canvas-main-canvas" data-off-canvas-main-canvas> <div id="wrap"> <div class="leader-wrapper" id="leader-wrapper"> </div> <div class="brand-wrapper" id="brand-wrapper"> <div class="brand container"> <div class="region region-brand"> <a class="logo navbar-btn" href="/" title="Home" rel="home"> <h1><img src="/themes/linuxjournal/images/ljlogo.png" alt="Linux Journal" /></h1> </a> <section id="block-topbannernearlogo" class="block block-block-content block-block-contenta8e75a3d-967c-416d-8944-4015d1f90a84 clearfix"> <div class="field field--name-body field--type-text-with-summary field--label-hidden field--item"><a href="https://www.windriver.com/products/elxr-pro?utm_source=ba&utm_medium=pa&utm_campaign=ba-dg-amer-awa-vsmf-eLxrProLau_lin_dis_09172024"><img alt="Windriver eLxrPro" data-entity-type="file" data-entity-uuid="8ba04368-7803-4329-a196-dc1ea780ec2a" src="/sites/default/files/inline-images/WindRiver-eLxrPro_0.png" width="728" height="90" loading="lazy" /></a></div> </section> </div> </div> <div class="header-wrapper"> <header class="navbar navbar-default container" id="navbar" role="banner"> <button type="button" class="navbar-toggle" data-toggle="collapse" data-target="#navbar-collapse"> <span class="sr-only">Toggle navigation</span> <span class="icon-bar"></span> <span class="icon-bar"></span> <span class="icon-bar"></span> </button> <div id="navbar-collapse" class="navbar-collapse collapse"> <div class="region region-navigation-collapsible"> <section id="block-linuxjournal-main-menu" class="block block-system block-system-menu-blockmain clearfix navbar__menu col-xs-12 col-sm-8"> <ul class="menu menu--main nav navbar-nav"> <li class="expanded dropdown first"> <a href="/" class="dropdown-toggle" data-toggle="dropdown" data-drupal-link-system-path="<front>">Topics+ <span class="caret"></span></a> <ul class="dropdown-menu"> <li class="first"> <a href="/tag/cloud">Cloud</a> </li> <li> <a href="/tag/containers" data-drupal-link-system-path="taxonomy/term/978">Containers</a> </li> <li> <a href="/tag/desktop">Desktop</a> </li> <li> <a href="/tag/kernel" data-drupal-link-system-path="taxonomy/term/662">Kernel</a> </li> <li> <a href="/tag/mobile">Mobile</a> </li> <li> <a href="/tag/networking">Networking</a> </li> <li> <a href="/tag/privacy">Privacy</a> </li> <li> <a href="/tag/programming">Programming</a> </li> <li> <a href="/tag/security" data-drupal-link-system-path="taxonomy/term/31">Security</a> </li> <li> <a href="/tag/servers">Servers</a> </li> <li class="last"> <a href="/tag/sysadmin" data-drupal-link-system-path="taxonomy/term/21">SysAdmin</a> </li> </ul> </li> <li> <a href="/news" data-drupal-link-system-path="news">News</a> </li> <li class="last"> <a href="/books" data-drupal-link-system-path="books">eBooks</a> </li> </ul> </section> <section class="search-block-form block block-search block-search-form-block clearfix navbar__search col-xs-12 col-sm-4 col-md-4 col-md-offset-0" data-drupal-selector="search-block-form" id="block-linuxjournal-search" role="search"> <h2 class="block-title sr-only">Search</h2> <form action="/search/node" method="get" id="search-block-form" accept-charset="UTF-8"> <div class="form-item js-form-item form-type-search js-form-type-search form-item-keys js-form-item-keys form-no-label form-group"> <label for="edit-keys" class="control-label sr-only">Search</label> <div class="input-group"><input title="Enter the terms you wish to search for." data-drupal-selector="edit-keys" class="form-search form-control" placeholder="Search" type="search" id="edit-keys" name="keys" value="" size="15" maxlength="128" /><span class="input-group-btn"><button type="submit" value="Search" class="button js-form-submit form-submit btn-primary btn icon-only" name=""><span class="sr-only">Search</span><span class="icon glyphicon glyphicon-search" aria-hidden="true"></span></button></span></div> <div id="edit-keys--description" class="description help-block"> Enter the terms you wish to search for. </div> </div> <div class="form-actions form-group js-form-wrapper form-wrapper" data-drupal-selector="edit-actions" id="edit-actions"></div> </form> </section> </div> </div> <div class="navbar-header"> <div class="region region-navigation"> <section id="block-mobilenavigation" class="block block-system block-system-menu-blockmenu-mobile-navbar clearfix col-xs-12 col-md-8 col-md-offset-2 col-lg-8 col-lg-offset-0 navbar__menu navbar__mobilenavigation"> <ul class="menu menu--menu-mobile-navbar nav"> <li class="first"> <a href="/news" data-drupal-link-system-path="news">News</a> </li> <li> <a href="/popular" data-drupal-link-system-path="popular">Popular</a> </li> <li class="last"> <a href="/recent" data-drupal-link-system-path="recent">Recent</a> </li> </ul> </section> </div> </div> </header> </div> </div> <div role="main" class="main-container container js-quickedit-main-content" id="main"> <div class="row"> <section class="col-sm-12"> <div class="highlighted"> <div class="region region-highlighted"> <div data-drupal-messages-fallback class="hidden"></div> </div> </div> <a id="main-content"></a> <div class="region region-content"> <article data-history-node-id="1340256" class="row bs-2col-stacked node node--type-story node--view-mode-full"> <div class="col-sm-12 bs-region bs-region--top"> <div class="field field--name-field-tags field--type-entity-reference field--label-hidden field--items"> <div class="field--item"><div about="/tag/python" typeof="schema:Thing"> <h2><a href="/tag/python"> <div property="schema:name" class="field field--name-name field--type-string field--label-hidden field--item">python</div> </a></h2> <span property="schema:name" content="python" class="hidden"></span> </div> </div> <div class="field--item"><div about="/tag/programming" typeof="schema:Thing"> <h2><a href="/tag/programming"> <div property="schema:name" class="field field--name-name field--type-string field--label-hidden field--item">Programming</div> </a></h2> <span property="schema:name" content="Programming" class="hidden"></span> </div> </div> <div class="field--item"><div about="/tag/how-tos" typeof="schema:Thing"> <h2><a href="/tag/how-tos"> <div property="schema:name" class="field field--name-name field--type-string field--label-hidden field--item">HOW-TOs</div> </a></h2> <span property="schema:name" content="HOW-TOs" class="hidden"></span> </div> </div> </div> <div class="field field--name-node-title field--type-ds field--label-hidden field--item"><h1> Testing Your Code with Python's pytest, Part II </h1> </div> <div class="field field--name-node-author field--type-ds field--label-hidden field--item">by <a title="View user profile." href="/users/reuven-m-lerner" lang="" about="/users/reuven-m-lerner" typeof="schema:Person" property="schema:name" datatype="">Reuven M. Lerner</a></div> <div class="field field--name-node-post-date field--type-ds field--label-hidden field--item">on December 11, 2018</div> </div> <div class="col-sm-9 bs-region bs-region--left"> <div class="shariff" data-services="["twitter","facebook","linkedin","reddit","mail"]" data-theme="white" data-css="complete" data-orientation="horizontal" data-twitter-via="linuxjournal" data-mail-url="mailto:" data-lang="en"> </div> <div class="field field--name-body field--type-text-with-summary field--label-hidden field--item"><p><em>Testing functions isn't hard, but how do you test user input and output?</em></p> <p> In my <a href="https://www.linuxjournal.com/content/testing-your-code-pythons-pytest">last article</a>, I started looking at "pytest", a framework for testing Python programs that's really changed the way I look at testing. For the first time, I really feel like testing is something I can and should do on a regular basis; pytest makes things so easy and straightforward. </p> <p> One of the main topics I didn't cover in my last article is user input and output. How can you test programs that expect to get input from files or from the user? And, how can you test programs that are supposed to display something on the screen? </p> <p> So in this article, I describe how to test input and output in a variety of ways, allowing you to test programs that interact with the outside world. I try not only to explain what you can do, but also show how it fits into the larger context of testing in general and pytest in particular. </p> <span class="h3-replacement"> User Input</span> <p> Say you have a function that asks the user to enter an integer and then returns the value of that integer, doubled. You can imagine that the function would look like this: </p><pre> <code> def double(): x = input("Enter an integer: ") return int(x) * 2 </code> </pre> <p> How can you test that function with pytest? If the function were to take an argument, the answer would be easy. But in this case, the function is asking for interactive input from the user. That's a bit harder to deal with. After all, how can you, in your tests, pretend to ask the user for input? </p> <p> In most programming languages, user input comes from a source known as standard input (or <code>stdin</code>). In Python, <code>sys.stdin</code> is a read-only file object from which you can grab the user's input. </p> <p> So, if you want to test the "double" function from above, you can (should) replace <code>sys.stdin</code> with another file. There are two problems with this, however. First, you don't really want to start opening files on disk. And second, do you really want to replace the value of <code>sys.stdin</code> in your tests? That'll affect more than just one test. </p> <p> The solution comes in two parts. First, you can use the pytest "monkey patching" facility to assign a value to a system object temporarily for the duration of the test. This facility requires that you define your test function with a parameter named <code>monkeypatch</code>. The pytest system notices that you've defined it with that parameter, and then not only sets the <code>monkeypatch</code> local variable, but also sets it up to let you temporarily set attribute names. </p> <p> In theory, then, you could define your test like this: </p><pre> <code> def test_double(monkeypatch): monkeypatch.setattr('sys.stdin', open('/etc/passwd')) print(double()) </code> </pre> <p> In other words, this tells pytest that you want to open /etc/passwd and feed its contents to pytest. This has numerous problems, starting with the fact that /etc/passwd contains multiple lines, and that each of its lines is non-numeric. The function thus chokes and exits with an error before it even gets to the (useless) call to <code>print</code>. </p> <p> But there's another problem here, one that I mentioned above. You don't really want to be opening files during testing, if you can avoid it. Thus, one of the great tools in my testing toolbox is Python's <code>StringIO</code> class. The beauty of <code>StringIO</code> is its simplicity. It implements the API of a "file" object, but exists only in memory and is effectively a string. If you can create a <code>StringIO</code> instance, you can pass it to the call to <code>monkeypatch.setattr</code>, and thus make your tests precisely the way you want. </p> <p> Here's how to do that: </p><pre> <code> from io import StringIO from double import double number_inputs = StringIO('1234\n') def test_double(monkeypatch): monkeypatch.setattr('sys.stdin', number_inputs) assert double() == 2468 </code> </pre> <p> You first create a <code>StringIO</code> object containing the input you want to simulate from the user. Note that it must contain a newline (<code>\n</code>) to ensure that you'll see the end of the user's input and not hang. </p> <p> You assign that to a global variable, which means you'll be able to access it from within your test function. You then add the assertion to your test function, saying that the result should be 2468. And sure enough, it works. </p> <p> I've used this technique to simulate much longer files, and I've been quite satisfied by the speed and flexibility. Just remember that each line in the input "file" should end with a newline character. I've found that creating a <code>StringIO</code> with a triple-quoted string, which lets me include newlines and write in a more natural file-like way, works well. </p> <p> You can use <code>monkeypatch</code> to simulate calls to a variety of other objects as well. I haven't had much occasion to do that, but you can imagine all sorts of network-related objects that you don't actually want to use when testing. By monkey-patching those objects during testing, you can pretend to connect to a remote server, when in fact you're just getting pre-programmed text back. </p> <span class="h3-replacement"> Exceptions</span> <p> What happens if you call the <code>test_double</code> function with a string? You probably should test that too: </p><pre> <code> str_inputs = StringIO('abcd\n') def test_double_str(monkeypatch): monkeypatch.setattr('sys.stdin', str_inputs) assert double() == 'abcdabcd' </code> </pre> <p> It looks great, right? Actually, not so much: </p><pre> <code> E ValueError: invalid literal for int() with base 10: 'abcd' </code> </pre> <p> The test failed, because the function exited with an exception. And that's okay; after all, the function <em>should</em> raise an exception if the user gives input that isn't numeric. But, wouldn't it be nice to specify and test it? </p> <p> The thing is, how can you test for an exception? You can't exactly use a usual <code>assert</code> statement, much as you might like to. After all, you somehow need to trap the exception, and you can't simply say: </p><pre> <code> assert double() == ValueError </code> </pre> <p> That's because exceptions aren't values that are returned. They are raised through a different mechanism. </p> <p> Fortunately, pytest offers a good solution to this, albeit with slightly different syntax than you've seen before. It uses a <code>with</code> statement, which many Python developers recognize from its common use in ensuring that files are flushed and closed when you write to them. The <code>with</code> statement opens a block, and if an exception occurs during that block, then the "context manager"—that is, the object that the <code>with</code> runs on—has an opportunity to handle the exception. pytest takes advantage of this with the <code>pytest.raises</code> context manager, which you can use in the following way: </p><pre> <code> def test_double_str(monkeypatch): with pytest.raises(ValueError): monkeypatch.setattr('sys.stdin', str_inputs) result = double() </code> </pre> <p> Notice that you don't need an <code>assert</code> statement here, because the <code>pytest.raises</code> is, effectively, the <code>assert</code> statement. And, you do have to indicate the type of error (<code>ValueError</code>) that you're trying to trap, meaning what you expect to receive. </p> <p> If you want to capture (or assert) something having to do with the exception that was raised, you can use the <code>as</code> part of the <code>with</code> statement. For example: </p><pre> <code> def test_double_str(monkeypatch): with pytest.raises(ValueError) as e: monkeypatch.setattr('sys.stdin', str_inputs) results = double() assert str(e.value) == "invalid literal for int() ↪with base 10: 'abcd'" </code> </pre> <p> Now you can be sure that not only was a <code>ValueError</code> exception raised, but also what message was raised. </p> <span class="h3-replacement"> Output</span> <p> I generally advise people not to use <code>print</code> in their functions. After all, I'd like to get some value back from a function; I don't really want to display something on the screen. But at some point, you really do actually need to display things to the user. How can you test for that? </p> <p> The pytest solution is via the <code>capsys</code> plugin. Similar to <code>monkeypatch</code>, you declare <code>capsys</code> as a parameter to your test function. You then run your function, allowing it to produce its output. Then you invoke the <code>readouterr</code> function on <code>capsys</code>, which returns a tuple of two strings containing the output to <code>stdout</code> and its error-message counterpart, <code>stderr</code>. You then can run assertions on those strings. </p> <p> For example, let's assume this function: </p><pre> <code> def hello(name): print(f"Hello, {name}!") </code> </pre> <p> You can test it in the following way: </p><pre> <code> def test_hello(capsys): hello('world') captured_stdout, captured_stderr = capsys.readouterr() assert captured_stdout == 'Hello, world!' </code> </pre> <p> But wait! This test actually fails: </p><pre> <code> E AssertionError: assert 'Hello, world!\n' == 'Hello, world!' E - Hello, world! E ? - E + Hello, world! </code> </pre> <p> Do you see the problem? The output, as written by <code>print</code>, includes a trailing newline (<code>\n</code>) character. But the test didn't check for that. Thus, you can check for the trailing newline, or you can use <code>str.strip</code> on <code>stdout</code>: </p><pre> <code> def test_hello(capsys): hello('world') captured_stdout, captured_stderr = capsys.readouterr() assert captured_stdout.strip() == 'Hello, world!' </code> </pre> <span class="h3-replacement"> Summary</span> <p> pytest continues to impress me as a testing framework, in no small part because it combines a lot of small, simple ideas in ways that feel natural to me as a developer. It has gone a long way toward increasing my use of tests, both in general development and in my teaching. My "Weekly Python Exercise" subscription service now includes tests, and I feel like it has improved a great deal as a result. </p> <p> In my next article, I plan to take a third and final look at pytest, exploring some of the other ways it can interact with (and help) write robust and useful programs. </p> <span class="h3-replacement"> Resources</span> <ul><li> The <a href="http://pytest.org">pytest website</a>.</li> <li> An excellent book on the subject is Brian Okken's <em>Python testing with pytest</em>, published by Pragmatic Programmers. He also has many other resources, about pytest and code testing in general, at <a href="http://pythontesting.net">http://pythontesting.net</a>. </li> <li> <a href="https://www.linuxjournal.com/content/testing-your-code-pythons-pytest">"Testing Your Code with Python's pytest" by Reuven M. Lerner</a></li></ul></div> <div class="field field--name-dynamic-token-fieldnode-author field--type-ds field--label-hidden field--item"><div></div> <p> Reuven Lerner teaches Python, data science and Git to companies around the world. You can subscribe to his free, weekly "better developers" e-mail list, and learn from his books and courses at <a href="http://lerner.co.il">http://lerner.co.il</a>. Reuven lives with his wife and children in Modi'in, Israel. </p> </div> <section class="section--disqus"> <a id="comments-link" class="btn btn-default" href="https://www.linuxjournal.com/content/testing-your-code-pythons-pytest-part-ii#disqus_thread">Load Disqus comments</a> <div id="disqus_thread"></div> <noscript>Our discussions are <a href="https://disqus.com/?ref_noscript" rel="nofollow">powered by Disqus</a>, which require JavaScript.</noscript> </section> </div> <div class="col-sm-3 bs-region bs-region--right"> <div class="field field--name-dynamic-block-fieldnode-sidebar-image field--type-ds field--label-hidden field--item"><div class="views-element-container form-group"><div class="view view-sidebar-image view-id-sidebar_image view-display-id-block_1 js-view-dom-id-80739037abbda8d02dd66f09519fd55c25f797fac80030456a9d61d67d6f4cab"> </div> </div> </div> <div class="field field--name-dynamic-block-fieldnode-recent-content field--type-ds field--label-hidden field--item"><div class="views-element-container form-group"><div class="view view-related-content view-id-related_content view-display-id-block_2 js-view-dom-id-01bda6bc6728bd2b83a0c7168899dc9cd246066e3c639b913ed96426aaaa0416"> <div class="view-header"> <h2 class="block-title">Recent Articles</h2> </div> <div class="view-content"> <div class="views-row"><div class="views-field views-field-field-node-image"><div class="field-content"> <a href="/content/harnessing-quantum-potential-quantum-computing-and-qiskit-ubuntu" hreflang="en"><img loading="lazy" src="/sites/default/files/styles/75x75_square/public/nodeimage/story/harnessing-quantum-potential-quantum-computing-and-qiskit-on-ubuntu.jpg?itok=IDfHHIeP" width="75" height="75" alt="Harnessing Quantum Potential: Quantum Computing and Qiskit on Ubuntu" typeof="foaf:Image" class="img-responsive" /> </a> </div></div><div class="views-field views-field-title"><span class="field-content"><a href="/content/harnessing-quantum-potential-quantum-computing-and-qiskit-ubuntu" hreflang="en">Harnessing Quantum Potential: Quantum Computing and Qiskit on Ubuntu</a></span></div><div class="views-field views-field-uid"><span class="field-content"><a href="/users/george-whittaker" hreflang="en">George Whittaker</a></span></div></div> <div class="views-row"><div class="views-field views-field-field-node-image"><div class="field-content"> <a href="/content/using-maxqda-qualitative-data-analysis-linux" hreflang="en"><img loading="lazy" src="/sites/default/files/styles/75x75_square/public/nodeimage/story/using-maxqda-for-qualitative-data-analysis-on-linux.jpg?itok=KFsvAt23" width="75" height="75" alt="Using MAXQDA for Qualitative Data Analysis on Linux" typeof="foaf:Image" class="img-responsive" /> </a> </div></div><div class="views-field views-field-title"><span class="field-content"><a href="/content/using-maxqda-qualitative-data-analysis-linux" hreflang="en">Using MAXQDA for Qualitative Data Analysis on Linux</a></span></div><div class="views-field views-field-uid"><span class="field-content"><a href="/users/george-whittaker" hreflang="en">George Whittaker</a></span></div></div> <div class="views-row"><div class="views-field views-field-field-node-image"><div class="field-content"> <a href="/content/haproxy-ubuntu-load-balancing-and-failover-resilient-infrastructure" hreflang="en"><img loading="lazy" src="/sites/default/files/styles/75x75_square/public/nodeimage/story/haproxy-on-ubuntu-load-balancing-and-failover-for-resilient-infrastructure.jpg?itok=2rVXWl_c" width="75" height="75" alt="HAProxy on Ubuntu: Load Balancing and Failover for Resilient Infrastructure" typeof="foaf:Image" class="img-responsive" /> </a> </div></div><div class="views-field views-field-title"><span class="field-content"><a href="/content/haproxy-ubuntu-load-balancing-and-failover-resilient-infrastructure" hreflang="en">HAProxy on Ubuntu: Load Balancing and Failover for Resilient Infrastructure</a></span></div><div class="views-field views-field-uid"><span class="field-content"><a href="/users/germansuarez" hreflang="en">german.suarez</a></span></div></div> <div class="views-row"><div class="views-field views-field-field-node-image"><div class="field-content"> <a href="/content/linux-binary-analysis-reverse-engineering-and-vulnerability-discovery" hreflang="en"><img loading="lazy" src="/sites/default/files/styles/75x75_square/public/nodeimage/story/linux-binary-analysis-for-reverse-engineering-and-vulnerability-discovery.jpg?itok=vTlNabrQ" width="75" height="75" alt="Linux Binary Analysis for Reverse Engineering and Vulnerability Discovery" typeof="foaf:Image" class="img-responsive" /> </a> </div></div><div class="views-field views-field-title"><span class="field-content"><a href="/content/linux-binary-analysis-reverse-engineering-and-vulnerability-discovery" hreflang="en">Linux Binary Analysis for Reverse Engineering and Vulnerability Discovery</a></span></div><div class="views-field views-field-uid"><span class="field-content"><a href="/users/george-whittaker" hreflang="en">George Whittaker</a></span></div></div> <div class="views-row"><div class="views-field views-field-field-node-image"><div class="field-content"> <a href="/content/debian-backup-and-recovery-solutions-safeguard-your-data-confidence" hreflang="en"><img loading="lazy" src="/sites/default/files/styles/75x75_square/public/nodeimage/story/debian-backup-and-recovery-solutions-safeguard-your-data-with-confidence.jpg?itok=LhESic3E" width="75" height="75" alt="Debian Backup and Recovery Solutions: Safeguard Your Data with Confidence" typeof="foaf:Image" class="img-responsive" /> </a> </div></div><div class="views-field views-field-title"><span class="field-content"><a href="/content/debian-backup-and-recovery-solutions-safeguard-your-data-confidence" hreflang="en">Debian Backup and Recovery Solutions: Safeguard Your Data with Confidence</a></span></div><div class="views-field views-field-uid"><span class="field-content"><a href="/users/george-whittaker" hreflang="en">George Whittaker</a></span></div></div> <div class="views-row"><div class="views-field views-field-field-node-image"><div class="field-content"> <a href="/content/installing-development-tools-debian-setting-compilers-libraries-and-ides-robust-development" hreflang="en"><img loading="lazy" src="/sites/default/files/styles/75x75_square/public/nodeimage/story/installing-development-tools-on-debian-setting-up-compilers-libraries-and-ides-for-a-robust-development-environment.jpg?itok=Aq209Ota" width="75" height="75" alt="Installing Development Tools on Debian: Setting Up Compilers, Libraries, and IDEs for a Robust Development Environment" typeof="foaf:Image" class="img-responsive" /> </a> </div></div><div class="views-field views-field-title"><span class="field-content"><a href="/content/installing-development-tools-debian-setting-compilers-libraries-and-ides-robust-development" hreflang="en">Installing Development Tools on Debian: Setting Up Compilers, Libraries, and IDEs for a Robust Development Environment</a></span></div><div class="views-field views-field-uid"><span class="field-content"><a href="/users/george-whittaker" hreflang="en">George Whittaker</a></span></div></div> </div> </div> </div> </div> <div class="field field--name-dynamic-block-fieldnode-sponsors field--type-ds field--label-hidden field--item"><div class="views-element-container form-group"><div class="view view-latest-sponsor-block view-id-latest_sponsor_block view-display-id-block_1 js-view-dom-id-9e3a6a4831af2b603d11facbee76c024ddfe7fd9c47e23bbfd6b586fb36ed7cf"> <div class="view-header"> </div> <div class="view-content"> <div class="views-row"> <div class="field field--name-body field--type-text-with-summary field--label-hidden field--item"> </div> </div> </div> </div> </div> </div> </div> </article> </div> </section> </div> </div> <footer class="footer container" role="contentinfo"> <div class="region region-footer"> <section class="views-element-container block block-views block-views-blocknewsletter-promo-block-block-1 clearfix" id="block-views-block-newsletter-promo-block-block-1"> <div class="form-group"><div class="view view-newsletter-promo-block view-id-newsletter_promo_block view-display-id-block_1 js-view-dom-id-d6fe0593f574c5583214bb9138feaf5e1c597b50723122ba8a74ed8d05f5411a"> <div class="view-content"> <div class="views-row"> <div class="field field--name-body field--type-text-with-summary field--label-hidden field--item"><div class="newsletter-bottom"> <div class="col-md-9 col-sm-12 signup-form"> <!--<h3>Linux Journal Week in Review</h3> <p>Sign up to get all the good stuff delivered to your inbox every week.</p> <form id="subForm" class="js-cm-form" action="https://www.createsend.com/t/subscribeerror?description=" method="post" data-id="A61C50BEC994754B1D79C5819EC1255CDBC34DDF3E170B13E2BAC2D68C42BF424F853383C84F5BAE38EAB4CFA5C73907CAC074FF8192503AF80F3699F7FE5CE1"> <div class="flexform">--> <!--<label for="fieldEmail">Email</label> <br />--> <!--<input id="fieldEmail" name="cm-gjjtdh-gjjtdh" type="email" class="js-cm-email-input" placeholder="Enter your email. Get the newsletter." required /> <button class="js-cm-submit-button" type="submit">Sign Up</button> </div> <div> <input id="cm-privacy-consent" name="cm-privacy-consent" required type="checkbox" /> <label for="cm-privacy-consent">I give my consent to be emailed</label> <input id="cm-privacy-consent-hidden" name="cm-privacy-consent-hidden" type="hidden" value="true" /> </div> </form> <script type="text/javascript" src="https://js.createsend1.com/javascript/copypastesubscribeformlogic.js"></script> </div>--> <!--<div class="col-md-3 col-sm-12 subs-callout"> <img src="/sites/default/files/styles/large/public/2019-01/LJ294-Jan2019-Cover_0.jpg" width="100px" /> <h3>The Value of Open Source Journalism</h3> <p> Subscribe and support our coverage for technology's biggest thinkers – with up to 52% savings. </p> <strong><a href="https://www.linuxjournal.com/subscribe">Subscribe <i class="fa fa-angle-double-right" aria-hidden="true"></i></a> </strong> </div>--> </div> </div> </div> </div> </div> </div> </section> </div> <div class="footer-blocks col-sm-12"> <div class="col-md-6 col-sm-12 footer-left"> <div class="region region-footer-left"> <section id="block-connectwithusfooter" class="block block-block-content block-block-content5e722bd4-5e08-454b-8507-956089bfa661 clearfix"> <div class="field field--name-body field--type-text-with-summary field--label-hidden field--item"><div class="footer-heading">Connect With Us <span class="social-media sm-a-no-underline"><a href="https://youtube.com/linuxjournalonline" alt="Linux Journal on YouTube" aria-label="YouTube"><i class="fa fa-youtube fa-2x"></i></a><a href="https://www.facebook.com/linuxjournal/" alt="Linux Journal on Facebook" aria-label="Facebook"><i class="fa fa-facebook-f fa-2x"></i><a href="https://twitter.com/linuxjournal" alt="Linux Journal on Twitter" aria-label="Twitter"><i class="fa fa-twitter fa-2x"></i></a></span></div> <p>Linux Journal, representing 25+ years of publication, is the original magazine of the global Open Source community.</p></div> </section> <section id="block-linuxjournal-block-9" class="block block-block-content block-block-content8669793b-e217-4426-a79e-eb3c21ede127 clearfix"> <div class="field field--name-body field--type-text-with-summary field--label-hidden field--item"><div id="copyright">© 2024 Slashdot Media, LLC. All rights reserved.</div> </div> </section> <section id="block-privacyterms" class="block block-block-content block-block-contenta203b8bd-80ef-4982-bf6e-784dd7b44120 clearfix"> <div class="field field--name-body field--type-text-with-summary field--label-hidden field--item"><ul class="menu menu--footer-submenu nav" id="terms-nav"> <li><a href="https://slashdotmedia.com/privacy-statement/" rel="nofollow" target="_blank">PRIVACY POLICY</a></li> <li><a href="https://slashdotmedia.com/terms-of-use/" rel="nofollow" target="_blank">TERMS OF SERVICE</a></li> <li><a href="/sponsors">ADVERTISE</a></li> </ul></div> </section> </div> </div> <div class="col-md-4 col-sm-8 footer-middle"> <div class="region region-footer-middle"> <nav role="navigation" aria-labelledby="block-footermenucolumn2-menu" id="block-footermenucolumn2"> <h2 class="visually-hidden" id="block-footermenucolumn2-menu">Footer Menu Column 2</h2> <ul class="menu menu--footer-menu-column-2 nav"> <li class="first"> <a href="/content/masthead" data-drupal-link-system-path="node/1007727">Masthead</a> </li> <li> <a href="/author" data-drupal-link-system-path="node/1009249">Authors</a> </li> <li class="last"> <a href="/form/contact" data-drupal-link-system-path="webform/contact">Contact Us</a> </li> </ul> </nav> <nav role="navigation" aria-labelledby="block-footermenucolumn3-menu" id="block-footermenucolumn3"> <h2 class="visually-hidden" id="block-footermenucolumn3-menu">Footer Menu Column 3</h2> <ul class="menu menu--footer-menu-column-3 nav"> <li class="first"> <a href="/rss_feeds" data-drupal-link-system-path="node/1000457">RSS Feeds</a> </li> <li class="last"> <a href="/aboutus" data-drupal-link-system-path="node/1000267">About Us</a> </li> </ul> </nav> </div> </div> </div> </footer> </div> </div> <script type="application/json" data-drupal-selector="drupal-settings-json">{"path":{"baseUrl":"\/","scriptPath":null,"pathPrefix":"","currentPath":"node\/1340256","currentPathIsAdmin":false,"isFront":false,"currentLanguage":"en"},"pluralDelimiter":"\u0003","suppressDeprecationErrors":true,"bootstrap":{"forms_has_error_value_toggle":1,"popover_enabled":1,"popover_animation":1,"popover_auto_close":1,"popover_container":"body","popover_content":"","popover_delay":"0","popover_html":0,"popover_placement":"right","popover_selector":"","popover_title":"","popover_trigger":"click"},"linuxjournal":{"disqus":{"origin":"https:\/\/www.linuxjournal.com","prettyUrl":"\/content\/testing-your-code-pythons-pytest-part-ii","fullUrl":"https:\/\/www.linuxjournal.com\/content\/testing-your-code-pythons-pytest-part-ii","embedUrl":"https:\/\/linuxjournal.disqus.com\/embed.js","shortname":"linuxjournal"}},"statistics":{"data":{"nid":"1340256"},"url":"\/core\/modules\/statistics\/statistics.php"},"ajaxTrustedUrl":{"\/search\/node":true},"user":{"uid":0,"permissionsHash":"a7b3a803411eb9cbd5d7d374ffb326721ee8274ab5a665df8f38311e3aad858c"}}</script> <script src="/core/assets/vendor/jquery/jquery.min.js?v=3.6.3"></script> <script src="/core/assets/vendor/underscore/underscore-min.js?v=1.13.6"></script> <script src="/core/misc/polyfills/element.matches.js?v=9.5.9"></script> <script src="/core/misc/polyfills/object.assign.js?v=9.5.9"></script> <script src="/core/assets/vendor/once/once.min.js?v=1.0.1"></script> <script src="/core/assets/vendor/jquery-once/jquery.once.min.js?v=9.5.9"></script> <script src="/core/misc/drupalSettingsLoader.js?v=9.5.9"></script> <script src="/core/misc/drupal.js?v=9.5.9"></script> <script src="/core/misc/drupal.init.js?v=9.5.9"></script> <script src="/themes/contrib/bootstrap/js/drupal.bootstrap.js?smwnh4"></script> <script src="/themes/contrib/bootstrap/js/attributes.js?smwnh4"></script> <script src="/themes/contrib/bootstrap/js/theme.js?smwnh4"></script> <script src="/themes/linuxjournal/bootstrap/assets/javascripts/bootstrap/affix.js?smwnh4"></script> <script src="/themes/linuxjournal/bootstrap/assets/javascripts/bootstrap/alert.js?smwnh4"></script> <script src="/themes/linuxjournal/bootstrap/assets/javascripts/bootstrap/button.js?smwnh4"></script> <script src="/themes/linuxjournal/bootstrap/assets/javascripts/bootstrap/carousel.js?smwnh4"></script> <script src="/themes/linuxjournal/bootstrap/assets/javascripts/bootstrap/collapse.js?smwnh4"></script> <script src="/themes/linuxjournal/bootstrap/assets/javascripts/bootstrap/dropdown.js?smwnh4"></script> <script src="/themes/linuxjournal/bootstrap/assets/javascripts/bootstrap/modal.js?smwnh4"></script> <script src="/themes/linuxjournal/bootstrap/assets/javascripts/bootstrap/tooltip.js?smwnh4"></script> <script src="/themes/linuxjournal/bootstrap/assets/javascripts/bootstrap/popover.js?smwnh4"></script> <script src="/themes/linuxjournal/bootstrap/assets/javascripts/bootstrap/scrollspy.js?smwnh4"></script> <script src="/themes/linuxjournal/bootstrap/assets/javascripts/bootstrap/tab.js?smwnh4"></script> <script src="/themes/linuxjournal/bootstrap/assets/javascripts/bootstrap/transition.js?smwnh4"></script> <script src="/themes/linuxjournal/js/lj-consentmanager.js?smwnh4"></script> <script src="/modules/contrib/webform/js/webform.behaviors.js?v=9.5.9"></script> <script src="/core/misc/jquery.once.bc.js?v=9.5.9"></script> <script src="/core/misc/states.js?v=9.5.9"></script> <script src="/themes/contrib/bootstrap/js/misc/states.js?smwnh4"></script> <script src="/modules/contrib/webform/js/webform.states.js?v=9.5.9"></script> <script src="/modules/contrib/webform/modules/webform_bootstrap/js/webform_bootstrap.states.js?v=9.5.9"></script> <script src="/themes/contrib/bootstrap/js/popover.js?smwnh4"></script> <script src="/core/modules/statistics/statistics.js?v=9.5.9"></script> <script src="/themes/linuxjournal/js/lj-disqus.js?smwnh4"></script> <script src="/libraries/shariff/shariff.complete.js?v=9.5.9"></script> <!-- START EMBED --> <noscript><img src="https://api.b2c.com/api/noscript-448i7exgpyqpr9c144q.gif"></noscript> <!-- END EMBED --> <!-- Matomo --> <script type="text/javascript"> var _paq = _paq || []; function initPiwik() { _paq.push(['trackPageView']); _paq.push(['enableLinkTracking']); (function() { var u="https://analytics.linuxjournal.com/"; _paq.push(['setTrackerUrl', u+'piwik.php']); _paq.push(['setSiteId', '50']); var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0]; g.type='text/javascript'; g.async=true; g.defer=true; g.src=u+'piwik.js'; s.parentNode.insertBefore(g,s); })(); } </script> <noscript><p><img src="https://analytics.linuxjournal.com/piwik.php?idsite=50&rec=1" style="border:0;" alt="" /></p></noscript> <!-- End Matomo Code --> <div class="modal-custom overlay-custom" id="ccpa-modal" style="margin-left: 0; display: none; max-width: 100%; width: 100%"> <div id="modal-content" class="modal-content"> <div class="modal-header" id="ccpa-modal-content-destination"></div> <span class="close" id="modal-close">×</span> </div> </div> </body> </html>