181 lines
No EOL
26 KiB
HTML
181 lines
No EOL
26 KiB
HTML
<!DOCTYPE html><html><head><meta content="text/html; charset=utf-8" http-equiv="Content-Type" /><meta content="width=device-width, initial-scale=1" name="viewport" /><!--replace-start-0--><!--replace-start-5--><!--replace-start-8--><title>Testing Python code - My Zettelkasten</title><!--replace-end-8--><!--replace-end-5--><!--replace-end-0--><link href="https://cdn.jsdelivr.net/npm/fomantic-ui@2.8.7/dist/semantic.min.css" rel="stylesheet" /><link href="https://fonts.googleapis.com/css?family=Merriweather|Libre+Franklin|Roboto+Mono&display=swap" rel="stylesheet" /><!--replace-start-1--><!--replace-start-4--><!--replace-start-7--><link href="https://raw.githubusercontent.com/srid/neuron/master/assets/neuron.svg" rel="icon" /><meta content="Pytest is the most popular testing library for Python. It is not included with the Python standard library so it must be installed with pip. While it does not include a declaration library, it is robust enough to handle most scenarios having a rich and expressive set of constructs and decorators tha" name="description" /><meta content="Testing Python code" property="og:title" /><meta content="My Zettelkasten" property="og:site_name" /><meta content="article" property="og:type" /><meta content="Testing_Python_code" property="neuron:zettel-id" /><meta content="Testing_Python_code" property="neuron:zettel-slug" /><meta content="python" property="neuron:zettel-tag" /><meta content="testing" property="neuron:zettel-tag" /><script type="application/ld+json">[]</script><style type="text/css">body{background-color:#eeeeee !important;font-family:"Libre Franklin", serif !important}body .ui.container{font-family:"Libre Franklin", serif !important}body h1, h2, h3, h4, h5, h6, .ui.header, .headerFont{font-family:"Merriweather", sans-serif !important}body code, pre, tt, .monoFont{font-family:"Roboto Mono","SFMono-Regular","Menlo","Monaco","Consolas","Liberation Mono","Courier New", monospace !important}body div.z-index p.info{color:#808080}body div.z-index ul{list-style-type:square;padding-left:1.5em}body div.z-index .uplinks{margin-left:0.29999em}body .zettel-content h1#title-h1{background-color:rgba(33,133,208,0.1)}body nav.bottomPane{background-color:rgba(33,133,208,2.0e-2)}body div#footnotes{border-top-color:#2185d0}body p{line-height:150%}body img{max-width:100%}body .deemphasized{font-size:0.94999em}body .deemphasized:hover{opacity:1}body .deemphasized:not(:hover){opacity:0.69999}body .deemphasized:not(:hover) a{color:#808080 !important}body div.container.universe{padding-top:1em}body div.zettel-view ul{padding-left:1.5em;list-style-type:square}body div.zettel-view .pandoc .highlight{background-color:#ffff00}body div.zettel-view .pandoc .ui.disabled.fitted.checkbox{margin-right:0.29999em;vertical-align:middle}body div.zettel-view .zettel-content .metadata{margin-top:1em}body div.zettel-view .zettel-content .metadata div.date{text-align:center;color:#808080}body div.zettel-view .zettel-content h1{padding-top:0.2em;padding-bottom:0.2em;text-align:center}body div.zettel-view .zettel-content h2{border-bottom:solid 1px #4682b4;margin-bottom:0.5em}body div.zettel-view .zettel-content h3{margin:0px 0px 0.4em 0px}body div.zettel-view .zettel-content h4{opacity:0.8}body div.zettel-view .zettel-content div#footnotes{margin-top:4em;border-top-style:groove;border-top-width:2px;font-size:0.9em}body div.zettel-view .zettel-content div#footnotes ol > li > p:only-of-type{display:inline;margin-right:0.5em}body div.zettel-view .zettel-content aside.footnote-inline{width:30%;padding-left:15px;margin-left:15px;float:right;background-color:#d3d3d3}body div.zettel-view .zettel-content .overflows{overflow:auto}body div.zettel-view .zettel-content code{margin:auto auto auto auto;font-size:100%}body div.zettel-view .zettel-content p code, li code, ol code{padding:0.2em 0.2em 0.2em 0.2em;background-color:#f5f2f0}body div.zettel-view .zettel-content pre{overflow:auto}body div.zettel-view .zettel-content dl dt{font-weight:bold}body div.zettel-view .zettel-content blockquote{background-color:#f9f9f9;border-left:solid 10px #cccccc;margin:1.5em 0px 1.5em 0px;padding:0.5em 10px 0.5em 10px}body div.zettel-view .zettel-content.raw{background-color:#dddddd}body .ui.label.zettel-tag{color:#000000}body .ui.label.zettel-tag a{color:#000000}body nav.bottomPane ul.backlinks > li{padding-bottom:0.4em;list-style-type:disc}body nav.bottomPane ul.context-list > li{list-style-type:lower-roman}body .footer-version img{-webkit-filter:grayscale(100%);-moz-filter:grayscale(100%);-ms-filter:grayscale(100%);-o-filter:grayscale(100%);filter:grayscale(100%)}body .footer-version img:hover{-webkit-filter:grayscale(0%);-moz-filter:grayscale(0%);-ms-filter:grayscale(0%);-o-filter:grayscale(0%);filter:grayscale(0%)}body .footer-version, .footer-version a, .footer-version a:visited{color:#808080}body .footer-version a{font-weight:bold}body .footer-version{margin-top:1em !important;font-size:0.69999em}@media only screen and (max-width: 768px){body div#zettel-container{margin-left:0.4em !important;margin-right:0.4em !important}}body span.zettel-link-container span.zettel-link a{color:#2185d0;font-weight:bold;text-decoration:none}body span.zettel-link-container span.zettel-link a:hover{background-color:rgba(33,133,208,0.1)}body span.zettel-link-container span.extra{color:auto}body span.zettel-link-container.errors{border:solid 1px #ff0000}body span.zettel-link-container.errors span.zettel-link a:hover{text-decoration:none !important;cursor:not-allowed}body [data-tooltip]:after{font-size:0.69999em}body div.tag-tree div.node{font-weight:bold}body div.tag-tree div.node a.inactive{color:#555555}body .tree.flipped{-webkit-transform:rotate(180deg);-moz-transform:rotate(180deg);-ms-transform:rotate(180deg);-o-transform:rotate(180deg);transform:rotate(180deg)}body .tree{overflow:auto}body .tree ul.root{padding-top:0px;margin-top:0px}body .tree ul{position:relative;padding:1em 0px 0px 0px;white-space:nowrap;margin:0px auto 0px auto;text-align:center}body .tree ul::after{content:"";display:table;clear:both}body .tree ul:last-child{padding-bottom:0.1em}body .tree li{display:inline-block;vertical-align:top;text-align:center;list-style-type:none;position:relative;padding:1em 0.5em 0em 0.5em}body .tree li::before{content:"";position:absolute;top:0px;right:50%;border-top:solid 2px #cccccc;width:50%;height:1.19999em}body .tree li::after{content:"";position:absolute;top:0px;right:50%;border-top:solid 2px #cccccc;width:50%;height:1.19999em}body .tree li::after{right:auto;left:50%;border-left:solid 2px #cccccc}body .tree li:only-child{padding-top:0em}body .tree li:only-child::after{display:none}body .tree li:only-child::before{display:none}body .tree li:first-child::before{border-style:none;border-width:0px}body .tree li:first-child::after{border-radius:5px 0px 0px 0px}body .tree li:last-child::after{border-style:none;border-width:0px}body .tree li:last-child::before{border-right:solid 2px #cccccc;border-radius:0px 5px 0px 0px}body .tree ul ul::before{content:"";position:absolute;top:0px;left:50%;border-left:solid 2px #cccccc;width:0px;height:1.19999em}body .tree li div.forest-link{border:solid 2px #cccccc;padding:0.2em 0.29999em 0.2em 0.29999em;text-decoration:none;display:inline-block;border-radius:5px 5px 5px 5px;color:#333333;position:relative;top:2px}body .tree.flipped li div.forest-link{-webkit-transform:rotate(180deg);-moz-transform:rotate(180deg);-ms-transform:rotate(180deg);-o-transform:rotate(180deg);transform:rotate(180deg)}</style><script
|
||
async=""
|
||
id="MathJax-script"
|
||
src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"
|
||
></script>
|
||
<link
|
||
href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.23.0/themes/prism.min.css"
|
||
rel="stylesheet"
|
||
/><link rel="preconnect" href="https://fonts.googleapis.com" /><link
|
||
rel="preconnect"
|
||
href="https://fonts.gstatic.com"
|
||
crossorigin
|
||
/><link
|
||
href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;1,100;1,200;1,300;1,400;1,500;1,600;1,700&family=IBM+Plex+Sans+Condensed:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;1,100;1,200;1,300;1,400;1,500;1,600;1,700&family=IBM+Plex+Sans:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;1,100;1,200;1,300;1,400;1,500;1,600;1,700&family=IBM+Plex+Serif:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;1,100;1,200;1,300;1,400;1,500;1,600;1,700&display=swap"
|
||
rel="stylesheet"
|
||
/>
|
||
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.23.0/components/prism-core.min.js"></script>
|
||
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.23.0/plugins/autoloader/prism-autoloader.min.js"></script>
|
||
<style>
|
||
body .ui.container,
|
||
body ul {
|
||
font-family: "IBM Plex Sans" !important;
|
||
}
|
||
body blockquote {
|
||
border-left-width: 3px !important;
|
||
font-style: italic;
|
||
}
|
||
.headerFont,
|
||
.ui.header,
|
||
body h1,
|
||
h2,
|
||
h3,
|
||
h4,
|
||
h5,
|
||
h6 {
|
||
font-family: "IBM Plex Sans Condensed" !important;
|
||
}
|
||
body p {
|
||
line-height: 1.4;
|
||
}
|
||
.monoFont,
|
||
body code,
|
||
pre,
|
||
tt {
|
||
font-family: "IBM Plex Mono" !important;
|
||
font-size: 12px !important;
|
||
line-height: 1.4 !important;
|
||
}
|
||
</style>
|
||
<!--replace-end-7--><!--replace-end-4--><!--replace-end-1--></head><body><div class="ui fluid container universe"><!--replace-start-2--><!--replace-start-3--><!--replace-start-6--><div class="ui text container" id="zettel-container" style="position: relative"><div class="zettel-view"><article class="ui raised attached segment zettel-content"><div class="pandoc"><h1 id="title-h1">Testing Python code</h1><h2 id="pytest"><code>pytest</code></h2><p>Pytest is the most popular testing library for Python. It is not included with the Python standard library so it must be installed with <span class="zettel-link-container cf"><span class="zettel-link" title="Zettel: Package management"><a href="Python_package_management.html">pip</a></span></span>. While it does not include a declaration library, it is robust enough to handle most scenarios having a rich and expressive set of constructs and decorators that let you declare what your tests should do, under what conditions they should run, and how they should interact with the rest of your system.</p><h3 id="using-pytest">Using <code>pytest</code></h3><ul><li>Pytest will automatically detect test files so long as they are named appropriately. E.g. for a module called <code>lorem</code>, it will detzect the unit test files <code>lorem_test.py</code> and <code>test_lorem.py</code>.</li><li>In order to detect tests it should be run from a directory level above them</li></ul><h3 id="examples">Examples</h3><p>Here is a basic example of using pytest for a local module callled <code>palindrome</code>:</p><pre><code class="py language-py">import palindrome
|
||
|
||
def test_is_palindrome():
|
||
assert palindrome.is_palindrome('soros')
|
||
assert palindrome.is_palindrome('torot')
|
||
assert not palindrome.is_palindrome('chair')</code></pre><h2 id="mocking">Mocking</h2><p><code>patch()</code> and <code>Mock</code> enable us to mock objects whilst testing (classes, functions, methods and properties belonging). They are used in combination.</p><p>The <code>@patch</code> decorator temporarily replaces a specified object in your code with a mock object and restores the original object after the test is complete</p><p>A Mock object simulates the object it replaces so that the object behaves as expected during testing. For example, if your code calls <code>some_function.some_method()</code>, and <code>some_method</code> is mocked, calling <code>some_method</code> will not execute real logic but will interact with the Mock object instead. Mock objects record details about how they have been used, like what methods have been called, with what arguments, etc., allowing you to make assertions about how they have been used.</p><blockquote><p><code>@patch</code> and <code>Mock</code> work together because a patch is used to replace an object or attribute with a Mock object. <code>Mock</code> handles the simulated functionality, and <code>@patch</code> designates the real value we are replacing with the mock.</p></blockquote><h3 id="example-case">Example case</h3><p>I will use the following example from one my projects:</p><pre><code class="py language-py"># get_articles.py
|
||
def get_articles(article_type: str) -> Optional[Dict[str, Any]]:
|
||
"""Retrieve articles from Pocket API"""
|
||
|
||
if POCKET_LAMBDA_ENDPOINT is None:
|
||
logging.error("Error: POCKET_LAMBDA_ENDPOINT envinronment variable is not set")
|
||
return None
|
||
else:
|
||
# Interpolate the article_type into the Pocket request URL
|
||
endpoint = POCKET_LAMBDA_ENDPOINT.format(article_type=article_type)
|
||
|
||
try:
|
||
response = requests.get(endpoint)
|
||
response.raise_for_status()
|
||
return response.json()
|
||
|
||
except RequestException as e:
|
||
print(f"An error occurred: {e}")
|
||
return None</code></pre><p>This function: sources a URL from an environment variable, interpolates a query string into the URL (which comes in as a parameter), makes a request to the URL, and returns the response as JSON.</p><p>It has some safeguards in place:</p><ul><li>It checks that the environment variable is set</li><li>It checks that the request was successful</li></ul><p>In the example we could use a Mock object to simulate the response from the Pocket API. This would allow us to test the function without having to make a real request to the API:</p><pre><code class="py language-py">def test_successful_request():
|
||
# Replace the requests.get function with a Mock object (mock_get)
|
||
with patch("requests.get") as mock_get:
|
||
# Specify the return value of the mock_get object)
|
||
mock_get.return_value = Mock(ok=true)
|
||
mock_get.return_value.json.return_value = {"value": "test"}
|
||
|
||
# Call the function under test
|
||
result = get_articles("gaby")
|
||
|
||
# Assert expected outcomes
|
||
mock_get.assert_called_once_with(endpoint)
|
||
assert result == mock_json_response</code></pre><p>The example above follows the <strong>Arrange, Act, Assert</strong> pattern:</p><table class="ui table"><thead><tr><th>Stage</th><th>Action</th></tr></thead><tbody><tr><td>Arrange</td><td>Replace the <code>requests.get</code> function with <code>patch</code> and set properties with <code>Mock</code></td></tr><tr><td>Act</td><td>Call the function under test</td></tr><tr><td>Assert</td><td>Assert that the function under test behaved as expected</td></tr></tbody></table><h3 id="alternative-mock-syntax">Alternative mock syntax</h3><p>The <code>with patch(...) as mock_name</code> syntax is fine for small-scale mocking but can become cumbersome when you are mocking several dependencies.</p><p>There is another syntax (which I actually find clearer). Say we have a function with three dependencies: <code>update_worksheet</code>, <code>process_articles</code>, <code>get_articles</code>. We could mock like so:</p><pre><code class="py language-py">@patch("app.update_worksheet")
|
||
@patch("app.process_articles")
|
||
@patch("app.get_articles")
|
||
def test_success(
|
||
mock_get_articles, mock_process_articles, mock_update_worksheet
|
||
):
|
||
mock_get_articles.return_value = [1, 2, 3]</code></pre><p>Here the patching is done by the decorator and the mocks are defined as parameters to the test function (always in reverse order)</p><h3 id="mock-assertion-lexicon">Mock assertion lexicon</h3><h4 id="return_value"><code>return_value</code></h4><p>State what the mock should return</p><pre><code class="py language-py">my_mocked_function.return_value = ['one', 'two', 'three']</code></pre><h4 id="call_count"><code>call_count</code></h4><p>Test how many times a dependent function is called</p><pre><code class="py language-py">assert my_mocked_function.call_count = 3</code></pre><h4 id="assert_any_call"><code>assert_any_call()</code></h4><p>Test that a given mock is called at least once during the execution of the function under test</p><pre><code class="py language-py">my_mocked_function.assert_any_call(some_mocked_return_value)</code></pre><p>When the output of one function is used as a parameter to another, and we don’t particularly care about the details of what is concerned we can just pass the executed function, e.g:</p><pre><code class="py language-py">my_mocked_function.assert_any_call(preceding_function())</code></pre><h4 id="call_args_list"><code>call_args_list</code></h4><p>Get a list of all the arguments that a mock object was called with during the test.</p><p><code>call_args_list</code> is useful when you want to check the arguments that a mock object was called with during the test, especially if the mock object was called multiple times with different arguments. You can use it to inspect the arguments of each call and make assertions based on them.</p><pre><code class="py language-py">second_my_mocked_function_call = my_mocked_function.call_args_list[1]
|
||
|
||
# check the first argument of the second call:
|
||
|
||
assert second_my_mocked_function_call[0][0] == "expected arg"</code></pre><h4 id="side_effect"><code>side_effect</code></h4><p>Use to trigger a side effect when returning a value from a mock. Most useful for mocking exceptions.</p><pre><code class="py language-py">my_mocked_function.side_effect = Exception("Some exception raised")</code></pre><h2 id="testing-exceptions-with-raises">Testing exceptions with <code>raises</code></h2><p>Testing exceptions is quite straightforward. You can use the <code>raises</code> helper provided by pytest, and combine this with <code>excinfo</code> (“exception info”) to inspect the exception message.</p><pre><code class="py language-py">if POCKET_LAMBDA_ENDPOINT is None:
|
||
raise ValueError(
|
||
"Error: POCKET_LAMBDA_ENDPOINT environment variable is not set"
|
||
)</code></pre><p>Then to test this, we would use pytest’s <code>excinfo</code> fixture along with <code>raises</code>:</p><pre><code class="py language-py"> with pytest.raises(ValueError) as excinfo: # Watch for the ValueError
|
||
get_articles("some_type")
|
||
|
||
assert "Error: POCKET_LAMBDA_ENDPOINT environment variable is not set" in str(
|
||
excinfo.value
|
||
)</code></pre><p>We could actually simplify the above test by using the <code>match</code> parameter with <code>raise</code>. This way we do not need the separate assertion:</p><pre><code class="py language-py">with pytest.raises(ValueError, match="Error: POCKET_LAMBDA_ENDPOINT environment variable is not set"):
|
||
get_articles("some_type")</code></pre><p>Note that <code>excinfo</code> is best used for testing the exception text that you the developer explicitly <code>raise</code>. For exceptions tha may occur naturaly in the code you are testing, you should use <code>caplog</code> or <code>capsys</code> (see below).</p><h2 id="before-each-and-after-each">Before-each and after-each</h2><p>When testing functions, we achieve this in Python using <code>setup_function</code> and <code>teardown_function</code> methods. These methods are called before and after each test method respectively.</p><p>To apply a “before each” to <em>every test</em> just put your setup function and/or teardown function at the top level of your test module.</p><p>For example, below we set and remove an env var before and after each test:</p><pre><code class="py language-py">@pytest.fixture(scope="function") # specify that this fixture should be run before each function test
|
||
def setup_function():
|
||
print("Setting up test environment...")
|
||
os.environ["POCKET_LAMBDA_ENDPOINT"] = "https://some_endpoint.com/{article_type}"
|
||
|
||
|
||
def teardown_function():
|
||
print("Tearing down test environment...")
|
||
del os.environ["POCKET_LAMBDA_ENDPOINT"]</code></pre><p>If the setup/teardown should only be applied to a subset of tests, just pass the name of the fixture as a parameter to the test function:</p><pre><code class="py language-py">def some_function(setup_function):
|
||
# setup_function will be run before this test</code></pre><p>You don’t need to use the names <code>setup_function</code> and <code>teardown_function</code> so long as you are passing the fixture as a parameter.</p><p>You can also use <code>yield</code> to combine the setup and teardown into a single function:</p><pre><code class="py language-py">@pytest.fixture(scope="function")
|
||
def setup_function():
|
||
os.environ["POCKET_LAMBDA_ENDPOINT"] = "https://some_endpoint.com/{article_type}"
|
||
yield
|
||
del os.environ["POCKET_LAMBDA_ENDPOINT"]</code></pre><h3 id="another-example">Another example:</h3><p>The following test suite uses the same three mocked functions in every test. The following set-up assigns the mocks before each test and resets after each individual test has run:</p><pre><code class="py language-py">@pytest.fixture(scope="function")
|
||
def setup_function():
|
||
with patch("app.get_articles") as mock_get_articles, patch(
|
||
"app.process_articles"
|
||
) as mock_process_articles, patch("app.update_worksheet") as mock_update_worksheet:
|
||
yield mock_get_articles, mock_process_articles, mock_update_worksheet</code></pre><p>Then to use:</p><pre><code class="py language-py">def individual_test(setup_function):
|
||
mock_get_articles, mock_process_articles, mock_update_worksheet = setup_function
|
||
# Now each mock can be referenced using the vars above
|
||
</code></pre><h2 id="parameterized-tests">Parameterized tests</h2><p>For a sequence of tests that are repetitive, to avoid repeating the same code over and over again, we can use parameterized tests. This is where we pass in a list of parameters to the test function and the test function is run once for each parameter.</p><p>For example, in the function below I am handling numerous possible Exceptions that could be raised by the <code>requests.get</code> method:</p><pre><code class="py language-py"> try:
|
||
response = requests.get(endpoint)
|
||
response.raise_for_status()
|
||
return response.json()
|
||
|
||
except HTTPError as http_err:
|
||
logging.error(f"HTTP Error occurred: {http_err}")
|
||
|
||
except ConnectionError as conn_err:
|
||
logging.error(f"Connection Error occurred: {conn_err}")
|
||
|
||
except Timeout as timeout_err:
|
||
logging.error(f"Timeout Error occurred: {timeout_err}")
|
||
|
||
except RequestException as e:
|
||
logging.error(f"Request Exception occurred: {e}")
|
||
|
||
return None</code></pre><p>Instead of writing something like the following for each of the four exceptions:</p><pre><code class="py language-py">
|
||
def test_exception_generic(caplog):
|
||
with patch("requests.get", side_effect=RequestException("Some error")):
|
||
result = get_articles("some_type")
|
||
|
||
assert "Request Exception occurred" in caplog.text
|
||
assert result is None</code></pre><p>I could parameterize like so:</p><pre><code class="py language-py">@pytest.mark.parametrize(
|
||
"exception_type, log_message",
|
||
[
|
||
(RequestException, "Request Exception occurred: "),
|
||
(HTTPError, "HTTP Error occurred: "),
|
||
(Timeout, "Timeout Error occurred: "),
|
||
(ConnectionError, "Connection Error occurred: "),
|
||
],
|
||
)
|
||
def test_exceptions(caplog, exception_type, log_message):
|
||
with patch("requests.get", side_effect=exception_type("Some error")):
|
||
result = get_articles("some_type")
|
||
|
||
assert log_message in caplog.text
|
||
assert result is None</code></pre><h2 id="caplog-syslog-excinfo">Caplog, syslog, excinfo</h2><p><code>caplog</code> and <code>capsys</code> are built-in pytest fixtures. <code>caplog</code> lets you test log messages. <code>capsys</code> lets you test stdout and stderr. As such they are very useful when testing that error messages are logged correctly.</p><h3 id="caplog"><code>caplog</code></h3><p>In our example, if the endpoing environment is not set, we log an error message. We can test that this message is logged correctly using <code>caplog</code>:</p><pre><code class="py language-py">def test_no_endpoint_env_var(caplog):
|
||
os.environ.pop("POCKET_LAMBDA_ENDPOINT", None) # Remove env variable if it exists
|
||
|
||
with caplog.at_level(logging.ERROR):
|
||
result = get_articles("some_type")
|
||
|
||
assert (
|
||
"Error: POCKET_LAMBDA_ENDPOINT environment variable is not set" in caplog.text
|
||
)
|
||
assert result is None</code></pre><blockquote><p>Note tha we pass in <code>caplog</code> as a parameter to the test function. This is how pytest knows to use it as a fixture.</p></blockquote><h3 id="capsys"><code>capsys</code></h3><p>In our example, if the request is unsuccessful, we log an error message with <code>print</code> rather than <code>logging</code>. We can test that this message is printed correctly using <code>capsys</code> to check the stdout:</p><pre><code class="py language-py">def test_http_error(capsys):
|
||
with patch("requests.get") as mock_get:
|
||
mock_get.return_value = Mock(ok=False, status_code=404)
|
||
|
||
# Raise an HTTP error when raise_for_status is called
|
||
mock_get.return_value.raise_for_status.side_effect = RequestException()
|
||
|
||
result = get_articles("some_type")
|
||
|
||
captured = capsys.readouterr()
|
||
assert "An error occurred" in captured.out</code></pre><h3 id="excinfo"><code>excinfo</code></h3></div></article><nav class="ui attached segment deemphasized bottomPane" id="neuron-tags-pane"><div><span class="ui basic label zettel-tag" title="Tag">python</span><span class="ui basic label zettel-tag" title="Tag">testing</span></div></nav><nav class="ui bottom attached icon compact inverted menu blue" id="neuron-nav-bar"><!--replace-start-9--><!--replace-end-9--><a class="right item" href="impulse.html" title="Open Impulse"><i class="wave square icon"></i></a></nav></div></div><!--replace-end-6--><!--replace-end-3--><!--replace-end-2--><div class="ui center aligned container footer-version"><div class="ui tiny image"><a href="https://neuron.zettel.page"><img alt="logo" src="https://raw.githubusercontent.com/srid/neuron/master/assets/neuron.svg" title="Generated by Neuron 1.9.35.3" /></a></div></div></div></body></html> |