{"id":25260,"date":"2022-11-28T11:25:45","date_gmt":"2022-11-28T19:25:45","guid":{"rendered":"https:\/\/coderpad.io\/?p=25260"},"modified":"2023-06-05T13:49:18","modified_gmt":"2023-06-05T20:49:18","slug":"how-to-test-python-rest-apis","status":"publish","type":"post","link":"https:\/\/coderpad.io\/blog\/development\/how-to-test-python-rest-apis\/","title":{"rendered":"How to Test Python REST APIs"},"content":{"rendered":"\n<p>There are many types of tests, and they all seem great in theory, but how do we test a REST API? In order to do that, let\u2019s first break down a REST API into parts, so we can focus on testing one part at a time.<\/p>\n\n\n\n<p>A REST API is an interface that accepts connections via the internet, executes some business logic, and then returns a result. Fundamentally, that means that an API has input, business logic, and output. The business logic should be tested using traditional <a href=\"https:\/\/coderpad.io\/blog\/development\/testing-in-python-types-of-tests-and-how-to-write-them\/\">unit test techniques<\/a>. After that, what\u2019s left is testing the input and output.<\/p>\n\n\n\n<p>APIs over HTTP (an internet protocol), called HTTP APIs\u2019 have input and output which both have metadata (data about the requests) called <em>headers<\/em> that are used to give certain information, like how to interpret the data sent (is it encrypted? Is it text? Is it JSON? etcetera).\u00a0<\/p>\n\n\n\n<p>HTTP API output also has a special piece of metadata called the status code, which is an integer, meant to be machine readable, not human readable, which is used to give an overall status of the request.<\/p>\n\n\n\n<p>The next step that you see in most (but not all!) APIs, is some way of controlling what the user can and cannot do. You could call it user validation or permissions, but it\u2019s usually just called authorization and authentication.<\/p>\n\n\n\n<p>In order to break down how to actually test these pieces, let&#8217;s start off with an example Python Flask API that we&#8217;ll be testing against:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-1\" data-shcb-language-name=\"Python\" data-shcb-language-slug=\"python\"><span><code class=\"hljs language-python shcb-wrap-lines\"><span class=\"hljs-keyword\">from<\/span> flask <span class=\"hljs-keyword\">import<\/span> Flask, jsonify, request, views\n\napp = Flask(__name__)\n\n\n<span class=\"hljs-class\"><span class=\"hljs-keyword\">class<\/span> <span class=\"hljs-title\">CustomFlaskAPI<\/span><span class=\"hljs-params\">(views.MethodView)<\/span>:<\/span>\n    <span class=\"hljs-function\"><span class=\"hljs-keyword\">def<\/span> <span class=\"hljs-title\">get<\/span><span class=\"hljs-params\">(self)<\/span>:<\/span>\n        headers = request.headers\n        <span class=\"hljs-keyword\">if<\/span> headers.get(<span class=\"hljs-string\">\"should_error\"<\/span>, <span class=\"hljs-literal\">False<\/span>):\n            <span class=\"hljs-keyword\">return<\/span> jsonify({<span class=\"hljs-string\">\"error\"<\/span>: <span class=\"hljs-string\">\"ERROR\"<\/span>}), <span class=\"hljs-number\">400<\/span>\n\n        <span class=\"hljs-keyword\">return<\/span> jsonify({<span class=\"hljs-string\">\"a\"<\/span>: <span class=\"hljs-string\">\"b\"<\/span>}), <span class=\"hljs-number\">200<\/span>\n\n    <span class=\"hljs-function\"><span class=\"hljs-keyword\">def<\/span> <span class=\"hljs-title\">post<\/span><span class=\"hljs-params\">(self)<\/span>:<\/span>\n        content_type = request.headers.get(<span class=\"hljs-string\">\"Content-Type\"<\/span>)\n        <span class=\"hljs-keyword\">if<\/span> content_type != <span class=\"hljs-string\">\"application\/json\"<\/span>:\n            <span class=\"hljs-keyword\">return<\/span> jsonify({\n                <span class=\"hljs-string\">\"error\"<\/span>: <span class=\"hljs-string\">\"Unknown Content Type: {}\"<\/span>.format(content_type)\n            }), <span class=\"hljs-number\">415<\/span>\n        <span class=\"hljs-keyword\">return<\/span> jsonify({}), <span class=\"hljs-number\">200<\/span>\n\n\n<span class=\"hljs-comment\"># Add Endpoints:<\/span>\napp.add_url_rule(<span class=\"hljs-string\">'\/api\/'<\/span>, view_func=CustomFlaskAPI.as_view(<span class=\"hljs-string\">'api'<\/span>))\n\n\n<span class=\"hljs-keyword\">if<\/span> __name__ == <span class=\"hljs-string\">\"__main__\"<\/span>:\n    app.run()<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-1\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">Python<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">python<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<h3 class=\"wp-block-heading\">HTTP status codes are important<\/h3>\n\n\n\n<p>In an HTTP API, you\u2019re always going to have one, and they can have a lot of value, as they offer a programmatic quick summary of the status of a request. However, remember that the developers behind each API determine what HTTP codes they send back, so sometimes a misleading or incorrect HTTP status code can be sent back. That\u2019s good to keep in mind just because a common mistake is assuming that the <strong>OK<\/strong> status means everything went well.<\/p>\n\n\n\n<p>An example of this, is that in many asynchronous processes over APIs, the <strong>OK<\/strong> status received from an API may just mean it received the request successfully, not that the API actually did anything with that data. The process could fail later down the line, and the HTTP status code won\u2019t magically change to reflect that.<\/p>\n\n\n\n<p>The first thing to know about HTTP status codes is that they are intended to be read by machines, so they\u2019re really just numbers like 200, even though those numbers map to human readable values, like OK.<\/p>\n\n\n\n<p>The really important thing to know about HTTP codes is that the range of <strong>200<\/strong>&#8211;<strong>299<\/strong> means everything went well, <strong>300<\/strong>&#8211;<strong>399<\/strong> means some kind of redirect occurred, <strong>400<\/strong>&#8211;<strong>499<\/strong> means some kind of understandable error occurred, and <strong>500<\/strong>+ means something, most likely catastrophic, happened. Personally, I love to reference <a href=\"https:\/\/http.cat\/\" target=\"_blank\" rel=\"noopener\">HTTP Cats<\/a> for what each code means, but there is also an <a href=\"https:\/\/www.rfc-editor.org\/rfc\/rfc9110.html#name-status-codes\" target=\"_blank\" rel=\"noopener\">official spec you can read<\/a>.<\/p>\n\n\n\n<p>So, how do we test for these status codes?&nbsp;<\/p>\n\n\n\n<p>Let&#8217;s first think about what kinds of tests you can build for a program. There are:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>&#8220;Happy&#8221; paths, where everything goes well<\/li>\n\n\n\n<li>&#8220;Sad&#8221; paths, where errors occur<\/li>\n\n\n\n<li>&#8220;Edge&#8221; cases or &#8220;Corner&#8221; cases, which are scenarios that don&#8217;t occur very often<\/li>\n<\/ul>\n\n\n\n<p>Let&#8217;s apply these different paths to our API status code testing. Let&#8217;s test that:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>We get a 200 &#8220;OK&#8221; when everything goes likes we want it to<\/li>\n\n\n\n<li>We get 400s when we have various errors that we expect<\/li>\n<\/ul>\n\n\n\n<p>It&#8217;s worth mentioning that 500 errors are inherently errors that aren\u2019t expected or handled, so if you can come up with a test case for a 500+ error then the error itself should probably be a 400-499 error instead.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-2\" data-shcb-language-name=\"Python\" data-shcb-language-slug=\"python\"><span><code class=\"hljs language-python shcb-wrap-lines\"><span class=\"hljs-keyword\">import<\/span> unittest\n<span class=\"hljs-keyword\">from<\/span> unittest <span class=\"hljs-keyword\">import<\/span> TestCase\n<span class=\"hljs-keyword\">import<\/span> requests  <span class=\"hljs-comment\"># pip install requests<\/span>\n\n\n<span class=\"hljs-class\"><span class=\"hljs-keyword\">class<\/span> <span class=\"hljs-title\">IntegrationTests<\/span><span class=\"hljs-params\">(TestCase)<\/span>:<\/span>\n    <span class=\"hljs-function\"><span class=\"hljs-keyword\">def<\/span> <span class=\"hljs-title\">test_status_codes_200<\/span><span class=\"hljs-params\">(self)<\/span>:<\/span>\n        result = requests.get(<span class=\"hljs-string\">\"http:\/\/localhost:5000\/api\/\"<\/span>)\n        <span class=\"hljs-keyword\">assert<\/span> result.status_code == <span class=\"hljs-number\">200<\/span>\n\n    <span class=\"hljs-function\"><span class=\"hljs-keyword\">def<\/span> <span class=\"hljs-title\">test_status_codes_400<\/span><span class=\"hljs-params\">(self)<\/span>:<\/span>\n        result = requests.get(\n            <span class=\"hljs-string\">\"http:\/\/localhost:5000\/api\/\"<\/span>,\n            headers={<span class=\"hljs-string\">\"should_error\"<\/span>: <span class=\"hljs-string\">\"error\"<\/span>},\n        )\n        <span class=\"hljs-keyword\">assert<\/span> result.status_code == <span class=\"hljs-number\">400<\/span>\n\n    <span class=\"hljs-function\"><span class=\"hljs-keyword\">def<\/span> <span class=\"hljs-title\">test_status_codes_404<\/span><span class=\"hljs-params\">(self)<\/span>:<\/span>\n        result = requests.get(<span class=\"hljs-string\">\"http:\/\/localhost:5000\/does_not_exist\/\"<\/span>)\n        <span class=\"hljs-keyword\">assert<\/span> result.status_code == <span class=\"hljs-number\">404<\/span>\n\n\n<span class=\"hljs-keyword\">if<\/span> __name__ == <span class=\"hljs-string\">\"__main__\"<\/span>:\n    unittest.main()\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-2\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">Python<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">python<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<h3 class=\"wp-block-heading\">Headers are the metadata of requests<\/h3>\n\n\n\n<p>The next piece of an API request to focus on is the headers of your HTTP calls. Headers contain all types of metadata about the request itself. For example, a common header is the <code>content-type<\/code> which tells the requester what format the data is in (for example <code>application\/json<\/code> means it\u2019s in JSON). There are some standard headers, but custom ones can also be added so all of the above should be tested.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-3\" data-shcb-language-name=\"Python\" data-shcb-language-slug=\"python\"><span><code class=\"hljs language-python shcb-wrap-lines\"><span class=\"hljs-keyword\">import<\/span> unittest\n<span class=\"hljs-keyword\">from<\/span> unittest <span class=\"hljs-keyword\">import<\/span> TestCase\n<span class=\"hljs-keyword\">import<\/span> requests\n\n\n<span class=\"hljs-class\"><span class=\"hljs-keyword\">class<\/span> <span class=\"hljs-title\">IntegrationTests<\/span><span class=\"hljs-params\">(TestCase)<\/span>:<\/span>\n    <span class=\"hljs-function\"><span class=\"hljs-keyword\">def<\/span> <span class=\"hljs-title\">test_header_invalid<\/span><span class=\"hljs-params\">(self)<\/span>:<\/span>\n        result = requests.post(\n            <span class=\"hljs-string\">\"http:\/\/localhost:5000\/api\/\"<\/span>,\n            headers = {<span class=\"hljs-string\">'Content-Type'<\/span>: <span class=\"hljs-string\">'random\/content'<\/span>},\n            json = {\n                <span class=\"hljs-string\">\"username\"<\/span>: <span class=\"hljs-string\">\"fake\"<\/span>,\n                <span class=\"hljs-string\">\"password\"<\/span>: <span class=\"hljs-string\">\"fakse\"<\/span>,\n            },\n        )\n        <span class=\"hljs-keyword\">assert<\/span> result.status_code == <span class=\"hljs-number\">415<\/span>\n        <span class=\"hljs-keyword\">assert<\/span> result.headers.get(<span class=\"hljs-string\">\"Content-Type\"<\/span>) == <span class=\"hljs-string\">\"application\/json\"<\/span>\n\n    <span class=\"hljs-function\"><span class=\"hljs-keyword\">def<\/span> <span class=\"hljs-title\">test_header_correct<\/span><span class=\"hljs-params\">(self)<\/span>:<\/span>\n        result = requests.post(\n            <span class=\"hljs-string\">\"http:\/\/localhost:5000\/api\/\"<\/span>,\n            headers = {<span class=\"hljs-string\">'Content-Type'<\/span>: <span class=\"hljs-string\">'application\/json'<\/span>},\n            json = {\n                <span class=\"hljs-string\">\"username\"<\/span>: <span class=\"hljs-string\">\"fake\"<\/span>,\n                <span class=\"hljs-string\">\"password\"<\/span>: <span class=\"hljs-string\">\"fakse\"<\/span>,\n            },\n        )\n        <span class=\"hljs-keyword\">assert<\/span> result.status_code != <span class=\"hljs-number\">415<\/span>\n        <span class=\"hljs-keyword\">assert<\/span> result.headers.get(<span class=\"hljs-string\">\"Content-Type\"<\/span>) == <span class=\"hljs-string\">\"application\/json\"<\/span>\n\n\n<span class=\"hljs-keyword\">if<\/span> __name__ == <span class=\"hljs-string\">\"__main__\"<\/span>:\n    unittest.main()<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-3\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">Python<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">python<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<blockquote class=\"wp-block-quote\">\n<p>\u2139\ufe0f A very important note about headers is that many of them are very easy to change, so if you have your API be dependant on them for things like permissions you could be leaving your API open to man in the middle attacks.<\/p>\n<\/blockquote>\n\n\n\n<h3 class=\"wp-block-heading\">Authentication versus Authorization<\/h3>\n\n\n\n<p>Now that we have tests for our REST API\u2019s status codes and headers, we want to test that a user can only do what we expect them to be able to do.<\/p>\n\n\n\n<p>In the industry you\u2019ll sometimes hear people say things about \u201cauth\u201d-ing or \u201cauth\u201ded, but what does that mean? It is jargon for saying both authenticated and authorized. But what is the difference?<\/p>\n\n\n\n<p>This boils down to two questions: Are you who you say you are, and are you allowed to perform the action you\u2019re trying to do. In other words \u2013 are you really my housemate, and even if you are, are you really allowed in my room? These two questions are the core of authentication and authorization. In the vernacular they\u2019re often used interchangeably, but they actually have separate meanings.<\/p>\n\n\n\n<h4 class=\"wp-block-heading\">Authentication<\/h4>\n\n\n\n<p>Authentication is checking if you are who you say you are. In other words, when you log in to a website and you give it your username and password you are <em>authenticating<\/em> &#8211; saying you are a real user. Authentication is <em>who you are<\/em>.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-4\" data-shcb-language-name=\"Python\" data-shcb-language-slug=\"python\"><span><code class=\"hljs language-python shcb-wrap-lines\"><span class=\"hljs-keyword\">import<\/span> unittest\n<span class=\"hljs-keyword\">from<\/span> unittest <span class=\"hljs-keyword\">import<\/span> TestCase\n<span class=\"hljs-keyword\">import<\/span> requests\n<span class=\"hljs-keyword\">import<\/span> sqlalchemy\n<span class=\"hljs-keyword\">from<\/span> sqlalchemy.orm <span class=\"hljs-keyword\">import<\/span> sessionmaker\n<span class=\"hljs-keyword\">from<\/span> flask <span class=\"hljs-keyword\">import<\/span> Flask, jsonify, request, views\n\napp = Flask(__name__)\n\n<span class=\"hljs-comment\"># Connect to the DB and reflect metadata.<\/span>\nengine = sqlalchemy.create_engine(<span class=\"hljs-string\">\"postgresql:\/\/coderpad:@\/coderpad?host=\/tmp\/postgresql\/socket\"<\/span>)\nconnection = engine.connect()\nSession = sessionmaker(bind=engine)\nsession = Session()\nmetadata = sqlalchemy.MetaData()\nmetadata.reflect(bind=engine)\n\n\n<span class=\"hljs-class\"><span class=\"hljs-keyword\">class<\/span> <span class=\"hljs-title\">CustomFlaskAPI<\/span><span class=\"hljs-params\">(views.MethodView)<\/span>:<\/span>\n    <span class=\"hljs-function\"><span class=\"hljs-keyword\">def<\/span> <span class=\"hljs-title\">post<\/span><span class=\"hljs-params\">(self)<\/span>:<\/span>\n        content_type = request.headers.get(<span class=\"hljs-string\">\"Content-Type\"<\/span>)\n        <span class=\"hljs-keyword\">if<\/span> content_type != <span class=\"hljs-string\">\"application\/json\"<\/span>:\n            <span class=\"hljs-keyword\">return<\/span> jsonify({\n                <span class=\"hljs-string\">\"error\"<\/span>: <span class=\"hljs-string\">\"Unknown Content Type: {}\"<\/span>.format(content_type)\n            }), <span class=\"hljs-number\">415<\/span>\n\n        username = request.json.get(<span class=\"hljs-string\">'username'<\/span>)\n        password = request.json.get(<span class=\"hljs-string\">'password'<\/span>)  <span class=\"hljs-comment\"># NEVER PASS THIS IN PLAIN TEXT<\/span>\n\n        <span class=\"hljs-keyword\">if<\/span> username <span class=\"hljs-keyword\">is<\/span> <span class=\"hljs-literal\">None<\/span> <span class=\"hljs-keyword\">or<\/span> password <span class=\"hljs-keyword\">is<\/span> <span class=\"hljs-literal\">None<\/span>:\n            <span class=\"hljs-keyword\">return<\/span> jsonify({\n                <span class=\"hljs-string\">\"error\"<\/span>: <span class=\"hljs-string\">\"Missing username or password\"<\/span>,\n            }), <span class=\"hljs-number\">400<\/span>\n\n        users_table = metadata.tables&#91;<span class=\"hljs-string\">\"users\"<\/span>]\n        result = connection.execute(\n            users_table.select().where(\n                (users_table.c.username == username)\n                &amp; (users_table.c.password == password)  <span class=\"hljs-comment\"># Never actually store in plain text<\/span>\n            )\n        ).fetchall()\n\n        <span class=\"hljs-keyword\">if<\/span> (len(result) &lt;= <span class=\"hljs-number\">0<\/span>):\n            <span class=\"hljs-keyword\">return<\/span> jsonify({\n                <span class=\"hljs-string\">\"error\"<\/span>: <span class=\"hljs-string\">\"Invalid username or password\"<\/span>,\n            }), <span class=\"hljs-number\">400<\/span>\n\n        <span class=\"hljs-keyword\">return<\/span> jsonify({}), <span class=\"hljs-number\">200<\/span>\n\n\n<span class=\"hljs-comment\"># Add Endpoints:<\/span>\napp.add_url_rule(<span class=\"hljs-string\">'\/api\/'<\/span>, view_func=CustomFlaskAPI.as_view(<span class=\"hljs-string\">'api'<\/span>))\n\n\n<span class=\"hljs-class\"><span class=\"hljs-keyword\">class<\/span> <span class=\"hljs-title\">IntegrationTests<\/span><span class=\"hljs-params\">(TestCase)<\/span>:<\/span>\n    <span class=\"hljs-function\"><span class=\"hljs-keyword\">def<\/span> <span class=\"hljs-title\">test_login<\/span><span class=\"hljs-params\">(self)<\/span>:<\/span>\n        result = requests.post(\n            <span class=\"hljs-string\">\"http:\/\/localhost:5000\/api\/\"<\/span>,\n            headers = {<span class=\"hljs-string\">'Content-Type'<\/span>: <span class=\"hljs-string\">'application\/json'<\/span>},\n            json = {\n                <span class=\"hljs-string\">\"username\"<\/span>: <span class=\"hljs-string\">\"real_user\"<\/span>,\n                <span class=\"hljs-string\">\"password\"<\/span>: <span class=\"hljs-string\">\"real_encrypted_password\"<\/span>,\n            },\n        )\n        <span class=\"hljs-keyword\">assert<\/span> result.status_code == <span class=\"hljs-number\">200<\/span>\n\n    <span class=\"hljs-function\"><span class=\"hljs-keyword\">def<\/span> <span class=\"hljs-title\">test_invalid_login<\/span><span class=\"hljs-params\">(self)<\/span>:<\/span>\n        result = requests.post(\n            <span class=\"hljs-string\">\"http:\/\/localhost:5000\/api\/\"<\/span>,\n            headers = {<span class=\"hljs-string\">'Content-Type'<\/span>: <span class=\"hljs-string\">'application\/json'<\/span>},\n            json = {\n                <span class=\"hljs-string\">\"username\"<\/span>: <span class=\"hljs-string\">\"fake_user\"<\/span>,\n                <span class=\"hljs-string\">\"password\"<\/span>: <span class=\"hljs-string\">\"fake_encrypted_password\"<\/span>,\n            },\n        )\n        <span class=\"hljs-keyword\">assert<\/span> result.status_code == <span class=\"hljs-number\">400<\/span>\n\n<span class=\"hljs-keyword\">if<\/span> __name__ == <span class=\"hljs-string\">\"__main__\"<\/span>:\n    app.run()\n    unittest.main()<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-4\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">Python<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">python<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<h4 class=\"wp-block-heading\">Authorization<\/h4>\n\n\n\n<p>Authorization is when you then try to view your friend\u2019s private profile. Because you are a real user that is marked as a friend you are allowed to see the profile. But a random other valid user may not be allowed to see it. Authorization is <em>what you can do<\/em>.<\/p>\n\n\n\n<p>Another example of authorization would be if you\u2019re an admin or not \u2013 if you\u2019re an admin you may be <em>authorized<\/em> to do things like delete accounts, whereas a regular user is not allowed to do such things. A website has to constantly check if you\u2019re <em>authorized<\/em> to do certain actions, while it may only check now and then that you are <em>authenticated<\/em> &#8211; that you are who you claim to be.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-5\" data-shcb-language-name=\"Python\" data-shcb-language-slug=\"python\"><span><code class=\"hljs language-python shcb-wrap-lines\"><span class=\"hljs-keyword\">import<\/span> unittest\n<span class=\"hljs-keyword\">from<\/span> unittest <span class=\"hljs-keyword\">import<\/span> TestCase\n<span class=\"hljs-keyword\">import<\/span> requests\n<span class=\"hljs-keyword\">import<\/span> sqlalchemy\n<span class=\"hljs-keyword\">from<\/span> sqlalchemy.orm <span class=\"hljs-keyword\">import<\/span> sessionmaker\n<span class=\"hljs-keyword\">from<\/span> flask <span class=\"hljs-keyword\">import<\/span> Flask, jsonify, request, views\n\napp = Flask(__name__)\n\n<span class=\"hljs-comment\"># Connect to the DB and reflect metadata.<\/span>\nengine = sqlalchemy.create_engine(<span class=\"hljs-string\">\"postgresql:\/\/coderpad:@\/coderpad?host=\/tmp\/postgresql\/socket\"<\/span>)\nconnection = engine.connect()\nSession = sessionmaker(bind=engine)\nsession = Session()\nmetadata = sqlalchemy.MetaData()\nmetadata.reflect(bind=engine)\n\n\n<span class=\"hljs-class\"><span class=\"hljs-keyword\">class<\/span> <span class=\"hljs-title\">CustomFlaskAPI<\/span><span class=\"hljs-params\">(views.MethodView)<\/span>:<\/span>\n    <span class=\"hljs-function\"><span class=\"hljs-keyword\">def<\/span> <span class=\"hljs-title\">patch<\/span><span class=\"hljs-params\">(self)<\/span>:<\/span>\n        content_type = request.headers.get(<span class=\"hljs-string\">\"Content-Type\"<\/span>)\n        <span class=\"hljs-keyword\">if<\/span> content_type != <span class=\"hljs-string\">\"application\/json\"<\/span>:\n            <span class=\"hljs-keyword\">return<\/span> jsonify({\n                <span class=\"hljs-string\">\"error\"<\/span>: <span class=\"hljs-string\">\"Unknown Content Type: {}\"<\/span>.format(content_type)\n            }), <span class=\"hljs-number\">415<\/span>\n\n        <span class=\"hljs-comment\"># Token to show user has already Authenticated<\/span>\n        login_token = request.json.get(<span class=\"hljs-string\">'login_token'<\/span>)\n        <span class=\"hljs-keyword\">if<\/span> login_token <span class=\"hljs-keyword\">is<\/span> <span class=\"hljs-literal\">None<\/span>:\n            <span class=\"hljs-keyword\">return<\/span> jsonify({\n                <span class=\"hljs-string\">\"error\"<\/span>: <span class=\"hljs-string\">\"Unauthenticated Request\"<\/span>,\n            }), <span class=\"hljs-number\">400<\/span>\n\n        token_table = metadata.tables&#91;<span class=\"hljs-string\">\"tokens\"<\/span>]\n        result = connection.execute(\n            token_table.select().where(token_table.c.token == login_token)\n        ).fetchall()\n        <span class=\"hljs-keyword\">if<\/span> len(result) &lt;= <span class=\"hljs-number\">0<\/span>:\n            <span class=\"hljs-keyword\">return<\/span> jsonify({\n                <span class=\"hljs-string\">\"error\"<\/span>: <span class=\"hljs-string\">\"Unauthenticated Request\"<\/span>,\n            }), <span class=\"hljs-number\">400<\/span>\n        username = result&#91;<span class=\"hljs-number\">0<\/span>]\n\n        <span class=\"hljs-comment\"># Check if user is Authorized to make this change<\/span>\n        permissions_table = metadata.tables&#91;<span class=\"hljs-string\">\"permissions\"<\/span>]\n        result = connection.execute(\n            permissions_table.select().where(\n                (permissions_table.c.username == username)\n                &amp; (permissions_table.c.patch_permission == <span class=\"hljs-literal\">True<\/span>)\n            )\n        ).fetchall()\n        <span class=\"hljs-keyword\">if<\/span> len(result) &lt;= <span class=\"hljs-number\">0<\/span>:\n            <span class=\"hljs-keyword\">return<\/span> jsonify({\n                <span class=\"hljs-string\">\"error\"<\/span>: <span class=\"hljs-string\">\"Unauthorized Request\"<\/span>,\n            }), <span class=\"hljs-number\">400<\/span>\n\n        <span class=\"hljs-keyword\">return<\/span> jsonify({}), <span class=\"hljs-number\">200<\/span>\n\n\n<span class=\"hljs-comment\"># Add Endpoints:<\/span>\napp.add_url_rule(<span class=\"hljs-string\">'\/api\/'<\/span>, view_func=CustomFlaskAPI.as_view(<span class=\"hljs-string\">'api'<\/span>))\n\n<span class=\"hljs-class\"><span class=\"hljs-keyword\">class<\/span> <span class=\"hljs-title\">IntegrationTests<\/span><span class=\"hljs-params\">(TestCase)<\/span>:<\/span>\n    <span class=\"hljs-function\"><span class=\"hljs-keyword\">def<\/span> <span class=\"hljs-title\">test_login<\/span><span class=\"hljs-params\">(self)<\/span>:<\/span>\n        result = requests.post(\n            <span class=\"hljs-string\">\"http:\/\/localhost:5000\/api\/\"<\/span>,\n            headers = {<span class=\"hljs-string\">'Content-Type'<\/span>: <span class=\"hljs-string\">'application\/json'<\/span>},\n            json = {\n                <span class=\"hljs-string\">\"username\"<\/span>: <span class=\"hljs-string\">\"real_user\"<\/span>,\n                <span class=\"hljs-string\">\"password\"<\/span>: <span class=\"hljs-string\">\"real_encrypted_password\"<\/span>,\n            },\n        )\n        <span class=\"hljs-keyword\">assert<\/span> result.status_code == <span class=\"hljs-number\">200<\/span>\n\n    <span class=\"hljs-function\"><span class=\"hljs-keyword\">def<\/span> <span class=\"hljs-title\">test_invalid_login<\/span><span class=\"hljs-params\">(self)<\/span>:<\/span>\n        result = requests.post(\n            <span class=\"hljs-string\">\"http:\/\/localhost:5000\/api\/\"<\/span>,\n            headers = {<span class=\"hljs-string\">'Content-Type'<\/span>: <span class=\"hljs-string\">'application\/json'<\/span>},\n            json = {\n                <span class=\"hljs-string\">\"username\"<\/span>: <span class=\"hljs-string\">\"fake_user\"<\/span>,\n                <span class=\"hljs-string\">\"password\"<\/span>: <span class=\"hljs-string\">\"fake_encrypted_password\"<\/span>,\n            },\n        )\n        <span class=\"hljs-keyword\">assert<\/span> result.status_code == <span class=\"hljs-number\">400<\/span>\n\n<span class=\"hljs-keyword\">if<\/span> __name__ == <span class=\"hljs-string\">\"__main__\"<\/span>:\n    app.run()\n    unittest.main()<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-5\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">Python<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">python<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<h5 class=\"wp-block-heading\">Permission levels<\/h5>\n\n\n\n<p>Testing what a user is <em>authorized<\/em> to do also includes testing the different levels of users, like how there are admins that can see different parts of websites than regular users. Or, like how a friend may be authorized to see your profile while a stranger may not be.<\/p>\n\n\n\n<p>If you\u2019re a valid, logged in user is either true or false (<em>authenticated<\/em>), but you <em>may <\/em>be allowed to view <em>this<\/em> profile but not <em>that<\/em> account. What exactly a user is <em>authorized<\/em> to do depends on the permissions unique to one user.<\/p>\n\n\n\n<p>The primary way that permissions are doled out to each user account is using tokens. For example, an admin user may actually just be a user with all the tokens:<\/p>\n\n\n<pre class=\"wp-block-code\"><span><code class=\"hljs shcb-wrap-lines\">view_your_account, view_account_global, delete_account, add_account, post_comment, delete_your_comment, delete_comment_global<\/code><\/span><\/pre>\n\n\n<p>Whereas a non-admin user may only have:<\/p>\n\n\n<pre class=\"wp-block-code\"><span><code class=\"hljs shcb-wrap-lines\">view_your_account, view_friend_account, post_comment, delete_your_comment<\/code><\/span><\/pre>\n\n\n<p>Each user would have a set of tokens representing if a user should be allowed to do whatever they\u2019re trying to do. For example, a non-admin user can only delete comments they\u2019ve posted, while an admin account may be able to delete any comment.<\/p>\n\n\n\n<p>These tokens are often then combined with other information. For example, when a non-admin user is attempting to view an account, if the account is a friend then it can be viewed, otherwise the account cannot be viewed.<\/p>\n\n\n\n<p>In other words, the flow of logic may look like this:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-6\" data-shcb-language-name=\"Python\" data-shcb-language-slug=\"python\"><span><code class=\"hljs language-python shcb-wrap-lines\"><span class=\"hljs-function\"><span class=\"hljs-keyword\">def<\/span> <span class=\"hljs-title\">allowed_to_view<\/span><span class=\"hljs-params\">(user, account_to_view)<\/span>:<\/span>\n    <span class=\"hljs-keyword\">if<\/span> (user.has_permission(\u201cview_account_global\u201d)):\n        <span class=\"hljs-keyword\">return<\/span> <span class=\"hljs-literal\">True<\/span>\n    <span class=\"hljs-keyword\">elif<\/span> (user.has_permission(\u201cview_your_account\u201d) <span class=\"hljs-keyword\">and<\/span> user.account == account_to_view):\n\t<span class=\"hljs-keyword\">return<\/span> <span class=\"hljs-literal\">True<\/span>\n    <span class=\"hljs-keyword\">elif<\/span> (user.has_permission(\u201cview_friend_account\u201d) <span class=\"hljs-keyword\">and<\/span> account_to_view.user <span class=\"hljs-keyword\">in<\/span> user.friends):\n\t<span class=\"hljs-keyword\">return<\/span> <span class=\"hljs-literal\">True<\/span>\n    <span class=\"hljs-keyword\">else<\/span>:\n        <span class=\"hljs-keyword\">return<\/span> <span class=\"hljs-literal\">False<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-6\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">Python<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">python<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>In reality you\u2019d want to condense the if statements, but I left it split out for clarity.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Always have input validation<\/h3>\n\n\n\n<p>After you\u2019ve authenticated that the user is real and authorized that they\u2019re allowed to do whatever they\u2019re trying to do, you then need to check that you have all the information you need in order to perform that action. In other words, you need to check that the user has given you all the information you need. For example, if you are taking in a credit card number, but the user only gives you one digit then that\u2019s <strong>invalid input<\/strong>, and your API should reject it.<\/p>\n\n\n\n<p>Invalid input of all types is always something to test for. This is partially because this is a common way to attack applications. For example, a very common type of attack is an SQL Injection attack: this type of attack relies on an API putting user input directly into a database, without making sure it\u2019s safe. The user embeds some kind of SQL into the user input, giving them the ability to do something they otherwise wouldn\u2019t be allowed to do, and the API executes the command because the input wasn\u2019t validated.<\/p>\n\n\n\n<p>A common SQL Injection attack is, in the username field, entering <code>;DROP TABLE users<\/code>. This, if not properly validated or cleaned, <em>could<\/em> lead to the deletion of user data. Ergo input validation is very important!<\/p>\n\n\n\n<h4 class=\"wp-block-heading\">Common input validation test cases<\/h4>\n\n\n\n<p>Beyond testing that there is no sneaky SQL hidden in your user input, you also want to make sure to test the data types of given input and the bounds of that data (for example, the number of characters in the given string).<\/p>\n\n\n\n<p>If you\u2019re expecting an integer, you want to make sure the user gives you an integer. After all, if you don\u2019t validate that the input is an integer, your code could do some funky things, including erroring out, if it acts on the assumption that the data is of the expected type. Running <code>len<\/code> won\u2019t work on an integer (<code>len(3)<\/code>), leading to an exception, but running <code>len<\/code> on a string will (<code>len(\u201cstring\u201d)<\/code>) work just fine.<\/p>\n\n\n\n<p>There are a couple of standard tests to always remember when testing input that is an integer<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Positive numbers<\/li>\n\n\n\n<li>Decimal numbers (for example, 3.14)<\/li>\n\n\n\n<li>Negative numbers<\/li>\n\n\n\n<li>Data of different, unexpected forms, like strings, potentially like numbers in strings (so \u201c3.14\u201d)<\/li>\n<\/ol>\n\n\n\n<p>Point number 4 \u2013 testing the data given the wrong data type is something you\u2019ll always want to do.<\/p>\n\n\n\n<p>Aside from all of that, user input is usually specified and validated in the code using a schema (description of what is expected), so your tests can re-use that same schema by running all your bogus input data through the schema and confirming it fails the way you expect it to.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-7\" data-shcb-language-name=\"Python\" data-shcb-language-slug=\"python\"><span><code class=\"hljs language-python shcb-wrap-lines\"><span class=\"hljs-keyword\">import<\/span> unittest\n<span class=\"hljs-keyword\">from<\/span> unittest <span class=\"hljs-keyword\">import<\/span> TestCase\n<span class=\"hljs-keyword\">from<\/span> flask <span class=\"hljs-keyword\">import<\/span> Flask, jsonify, request, views\n<span class=\"hljs-keyword\">from<\/span> marshmallow <span class=\"hljs-keyword\">import<\/span> Schema, fields, validate\n<span class=\"hljs-keyword\">from<\/span> flask_api_4 <span class=\"hljs-keyword\">import<\/span> CustomFlaskAPI\n<span class=\"hljs-keyword\">from<\/span> parameterized <span class=\"hljs-keyword\">import<\/span> parameterized\n\napp = Flask(__name__)\n\n\n<span class=\"hljs-class\"><span class=\"hljs-keyword\">class<\/span> <span class=\"hljs-title\">UserPostInput<\/span><span class=\"hljs-params\">(Schema)<\/span>:<\/span>\n    name = fields.Str(required=<span class=\"hljs-literal\">True<\/span>)\n    children = fields.Int(\n        required=<span class=\"hljs-literal\">True<\/span>,\n        strict=<span class=\"hljs-literal\">True<\/span>,  <span class=\"hljs-comment\"># Must be a whole number<\/span>\n        validate=validate.Range(min=<span class=\"hljs-number\">1<\/span>),  <span class=\"hljs-comment\"># Must be positive<\/span>\n    )\n\n\n<span class=\"hljs-class\"><span class=\"hljs-keyword\">class<\/span> <span class=\"hljs-title\">CustomFlaskAPI<\/span><span class=\"hljs-params\">(views.MethodView)<\/span>:<\/span>\n    <span class=\"hljs-function\"><span class=\"hljs-keyword\">def<\/span> <span class=\"hljs-title\">post<\/span><span class=\"hljs-params\">(self)<\/span>:<\/span>\n        errors = UserPostInput().validate(request.json)\n        <span class=\"hljs-keyword\">if<\/span> len(errors) != <span class=\"hljs-number\">0<\/span>:\n            <span class=\"hljs-keyword\">return<\/span> jsonify({\n                <span class=\"hljs-string\">\"errors\"<\/span>: errors,\n            }), <span class=\"hljs-number\">400<\/span>\n\n        <span class=\"hljs-keyword\">return<\/span> jsonify({}), <span class=\"hljs-number\">200<\/span>\n\n\n<span class=\"hljs-comment\"># Add Endpoints:<\/span>\napp.add_url_rule(<span class=\"hljs-string\">'\/api\/'<\/span>, view_func=CustomFlaskAPI.as_view(<span class=\"hljs-string\">'api'<\/span>))\n\n<span class=\"hljs-class\"><span class=\"hljs-keyword\">class<\/span> <span class=\"hljs-title\">UnitTests<\/span><span class=\"hljs-params\">(TestCase)<\/span>:<\/span>\n    <span class=\"hljs-function\"><span class=\"hljs-keyword\">def<\/span> <span class=\"hljs-title\">test_valid_input<\/span><span class=\"hljs-params\">(self)<\/span>:<\/span>\n        <span class=\"hljs-keyword\">with<\/span> app.test_request_context(\n            <span class=\"hljs-string\">\"\/api\/\"<\/span>, method=<span class=\"hljs-string\">\"POST\"<\/span>, json={\n                <span class=\"hljs-string\">\"name\"<\/span>: <span class=\"hljs-string\">\"real_name\"<\/span>, <span class=\"hljs-string\">\"children\"<\/span>: <span class=\"hljs-number\">1<\/span>,\n            },\n        ):\n            result, status_code = CustomFlaskAPI().post()\n            <span class=\"hljs-keyword\">assert<\/span> status_code == <span class=\"hljs-number\">200<\/span>\n            <span class=\"hljs-keyword\">assert<\/span> result.json == {}\n\n<span class=\"hljs-meta\">    @parameterized.expand(&#91;<\/span>\n        &#91;<span class=\"hljs-number\">1<\/span>, <span class=\"hljs-literal\">False<\/span>, <span class=\"hljs-number\">1<\/span>, <span class=\"hljs-literal\">True<\/span>],\n        &#91;<span class=\"hljs-string\">\"name\"<\/span>, <span class=\"hljs-literal\">True<\/span>, <span class=\"hljs-string\">\"not_a_number\"<\/span>, <span class=\"hljs-literal\">False<\/span>],\n        &#91;<span class=\"hljs-string\">\"name\"<\/span>, <span class=\"hljs-literal\">True<\/span>, <span class=\"hljs-number\">-1<\/span>, <span class=\"hljs-literal\">False<\/span>],\n        &#91;<span class=\"hljs-string\">\"name\"<\/span>, <span class=\"hljs-literal\">True<\/span>, <span class=\"hljs-number\">3.14<\/span>, <span class=\"hljs-literal\">False<\/span>],\n    ])\n    <span class=\"hljs-function\"><span class=\"hljs-keyword\">def<\/span> <span class=\"hljs-title\">test_invalid_input<\/span><span class=\"hljs-params\">(\n        self, name, valid_name, children, valid_children,\n    )<\/span>:<\/span>\n        <span class=\"hljs-keyword\">with<\/span> app.test_request_context(\n            <span class=\"hljs-string\">\"\/api\/\"<\/span>, method=<span class=\"hljs-string\">\"POST\"<\/span>, json={\n                <span class=\"hljs-string\">\"name\"<\/span>: name, <span class=\"hljs-string\">\"children\"<\/span>: children,\n            },\n        ):\n            result, status_code = CustomFlaskAPI().post()\n            <span class=\"hljs-keyword\">assert<\/span> status_code == <span class=\"hljs-number\">400<\/span>\n\n            errors = result.json.get(<span class=\"hljs-string\">\"errors\"<\/span>, {})\n            <span class=\"hljs-keyword\">assert<\/span> valid_name == (<span class=\"hljs-string\">\"name\"<\/span> <span class=\"hljs-keyword\">not<\/span> <span class=\"hljs-keyword\">in<\/span> errors)\n            <span class=\"hljs-keyword\">assert<\/span> valid_children == (<span class=\"hljs-string\">\"children\"<\/span> <span class=\"hljs-keyword\">not<\/span> <span class=\"hljs-keyword\">in<\/span> errors)\n\n\n<span class=\"hljs-keyword\">if<\/span> __name__ == <span class=\"hljs-string\">\"__main__\"<\/span>:\n    app.run()\n    unittest.main()<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-7\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">Python<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">python<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>You should also be sure to test receiving more or fewer fields than you expect, as the user may forget something or add something you don\u2019t need. Some APIs choose to ignore extra fields, and some choose to throw an error when receiving extra fields. You should write the test based on the expected behavior from your API.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Output validation enables testing<\/h3>\n\n\n\n<p>When your API is returning output to the user it\u2019s less intuitive to do output validation; after all, your API <em>made<\/em> the output, so it\u2019s not going to be wrong, right? Well, there are lots of reasons to do output validation. One of them harkens back to <a href=\"https:\/\/www.ibm.com\/garage\/method\/practices\/code\/contract-driven-testing\/\" target=\"_blank\" rel=\"noopener\">contract testing<\/a>: during development, if you test what your API is returning, then you\u2019re never going to be surprised at what the customer receives. And thus your <em>customer<\/em> is never going to be surprised, so you won\u2019t break their code that calls your API. Happy customers mean good business. In other words, your tests can let you know if you ever accidentally change the output keys or data types coming from your API.<\/p>\n\n\n\n<p>A more general reason is that having output validation is more testable; you have a schema to use as the correct output in your tests.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-8\" data-shcb-language-name=\"Python\" data-shcb-language-slug=\"python\"><span><code class=\"hljs language-python shcb-wrap-lines\"><span class=\"hljs-keyword\">import<\/span> unittest\n<span class=\"hljs-keyword\">from<\/span> unittest <span class=\"hljs-keyword\">import<\/span> TestCase\n<span class=\"hljs-keyword\">from<\/span> flask_api_5 <span class=\"hljs-keyword\">import<\/span> CustomFlaskAPI, PostOutput\n<span class=\"hljs-keyword\">from<\/span> flask <span class=\"hljs-keyword\">import<\/span> Flask, jsonify, request, views\n<span class=\"hljs-keyword\">from<\/span> marshmallow <span class=\"hljs-keyword\">import<\/span> Schema, fields, validate\n\napp = Flask(__name__)\n\n\n<span class=\"hljs-class\"><span class=\"hljs-keyword\">class<\/span> <span class=\"hljs-title\">PostOutput<\/span><span class=\"hljs-params\">(Schema)<\/span>:<\/span>\n    answers = fields.Int(\n        required=<span class=\"hljs-literal\">True<\/span>,\n        strict=<span class=\"hljs-literal\">True<\/span>,  <span class=\"hljs-comment\"># Must be a whole number<\/span>\n        validate=validate.Range(min=<span class=\"hljs-number\">1<\/span>),  <span class=\"hljs-comment\"># Must be positive<\/span>\n    )\n\n\n<span class=\"hljs-class\"><span class=\"hljs-keyword\">class<\/span> <span class=\"hljs-title\">CustomFlaskAPI<\/span><span class=\"hljs-params\">(views.MethodView)<\/span>:<\/span>\n    <span class=\"hljs-function\"><span class=\"hljs-keyword\">def<\/span> <span class=\"hljs-title\">post<\/span><span class=\"hljs-params\">(self)<\/span>:<\/span>\n        result = {\n            <span class=\"hljs-string\">\"answers\"<\/span>: request.json.get(<span class=\"hljs-string\">\"children\"<\/span>, <span class=\"hljs-number\">0<\/span>) + <span class=\"hljs-number\">1<\/span>,\n        }\n        errors = PostOutput().validate(result)\n        <span class=\"hljs-keyword\">if<\/span> len(errors) != <span class=\"hljs-number\">0<\/span>:\n            <span class=\"hljs-comment\"># Errors instead of returning malformed data<\/span>\n            <span class=\"hljs-keyword\">return<\/span> jsonify({\n                <span class=\"hljs-string\">\"errors\"<\/span>: errors,\n            }), <span class=\"hljs-number\">400<\/span>\n\n        <span class=\"hljs-keyword\">return<\/span> jsonify(result), <span class=\"hljs-number\">200<\/span>\n\n\n<span class=\"hljs-comment\"># Add Endpoints:<\/span>\napp.add_url_rule(<span class=\"hljs-string\">'\/api\/'<\/span>, view_func=CustomFlaskAPI.as_view(<span class=\"hljs-string\">'api'<\/span>))\n\n<span class=\"hljs-class\"><span class=\"hljs-keyword\">class<\/span> <span class=\"hljs-title\">UnitTests<\/span><span class=\"hljs-params\">(TestCase)<\/span>:<\/span>\n    <span class=\"hljs-function\"><span class=\"hljs-keyword\">def<\/span> <span class=\"hljs-title\">test_valid_output<\/span><span class=\"hljs-params\">(self)<\/span>:<\/span>\n        <span class=\"hljs-keyword\">with<\/span> flask_app.test_request_context(\n            <span class=\"hljs-string\">\"\/api\/\"<\/span>, method=<span class=\"hljs-string\">\"POST\"<\/span>, json={\n                <span class=\"hljs-string\">\"name\"<\/span>: <span class=\"hljs-string\">\"real_name\"<\/span>, <span class=\"hljs-string\">\"children\"<\/span>: <span class=\"hljs-number\">1<\/span>,\n            },\n        ):\n            result, status_code = CustomFlaskAPI().post()\n            <span class=\"hljs-keyword\">assert<\/span> status_code == <span class=\"hljs-number\">200<\/span>\n\n            <span class=\"hljs-comment\"># Can check result validity by hand<\/span>\n            <span class=\"hljs-keyword\">assert<\/span> result.json == {\n                <span class=\"hljs-string\">\"answers\"<\/span>: <span class=\"hljs-number\">2<\/span>,\n            }\n\n            <span class=\"hljs-comment\"># OR can re-use schema here<\/span>\n            <span class=\"hljs-keyword\">assert<\/span> PostOutput().validate(result.json) == {}\n\n    <span class=\"hljs-function\"><span class=\"hljs-keyword\">def<\/span> <span class=\"hljs-title\">test_invalid_output<\/span><span class=\"hljs-params\">(self, children)<\/span>:<\/span>\n        <span class=\"hljs-keyword\">with<\/span> flask_app.test_request_context(\n            <span class=\"hljs-string\">\"\/api\/\"<\/span>, method=<span class=\"hljs-string\">\"POST\"<\/span>, json={\n                <span class=\"hljs-string\">\"name\"<\/span>: <span class=\"hljs-string\">\"name\"<\/span>, <span class=\"hljs-string\">\"children\"<\/span>: <span class=\"hljs-number\">-1<\/span>,\n            },\n        ):\n            result, status_code = CustomFlaskAPI().post()\n            <span class=\"hljs-keyword\">assert<\/span> status_code == <span class=\"hljs-number\">400<\/span>\n\n            <span class=\"hljs-comment\"># Can check result validity by hand<\/span>\n            errors = result.json.get(<span class=\"hljs-string\">\"errors\"<\/span>, {})\n            <span class=\"hljs-keyword\">assert<\/span> <span class=\"hljs-string\">\"answers\"<\/span> <span class=\"hljs-keyword\">in<\/span> errors\n\n            <span class=\"hljs-comment\"># OR can re-use schema here<\/span>\n            errors = PostOutput().validate(result.json)  <span class=\"hljs-comment\"># Can re-use schema here<\/span>\n            <span class=\"hljs-keyword\">assert<\/span> <span class=\"hljs-string\">\"answers\"<\/span> <span class=\"hljs-keyword\">in<\/span> errors\n\n\n<span class=\"hljs-keyword\">if<\/span> __name__ == <span class=\"hljs-string\">\"__main__\"<\/span>:\n    app.run()\n    unittest.main()\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-8\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">Python<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">python<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<h2 class=\"wp-block-heading\">API testing has many facets<\/h2>\n\n\n\n<p>REST APIs have many moving pieces, but, just like with any other software development problem, they just need to be broken down into pieces to be effectively tested. REST API calls over HTTP are made up of status codes, headers, user input (further separated into request parameters and request body), and output (what the API returns).<\/p>\n\n\n\n<p>You should also always consider if your API should have some kind of security &#8211; and if it does you\u2019ll need both <em>authentication<\/em> and <em>authorization<\/em>.<\/p>\n\n\n\n<blockquote class=\"wp-block-quote\">\n<p>\ud83d\udca1 Don\u2019t forget to utilize both unit tests and integration tests to make sure you thoroughly test all parts of your API.<\/p>\n<\/blockquote>\n\n\n\n<p>And, after you\u2019ve gone through this exercise and identified all your dependencies, it might be a good time to hop over to <a href=\"https:\/\/coderpad.io\/blog\/development\/open-source-software-dependency-security\/\">CoderPad\u2019s blog on open source dependency security<\/a> to make sure you aren\u2019t introducing security risks into your API.<\/p>\n\n\n\n<p><em><a href=\"https:\/\/www.linkedin.com\/in\/jennifer-vannier\/\" target=\"_blank\" rel=\"noreferrer noopener\">Jennifer<\/a>\u00a0is a full stack developer with a passion for all areas of software development. He loves being a polyglot of programming languages and teaching others what he\u2019s learned.<\/em><\/p>\n","protected":false},"excerpt":{"rendered":"<p>There are many types of tests, and they all seem great in theory, but how do we test a REST API? This article will teach you how to conduct unit and integration tests on your Python REST APIs.<\/p>\n","protected":false},"author":1,"featured_media":25271,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[9],"tags":[],"persona":[29],"blog-programming-language":[37],"keyword-cluster":[],"class_list":["post-25260","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-development"],"acf":[],"_links":{"self":[{"href":"https:\/\/coderpad.io\/wp-json\/wp\/v2\/posts\/25260","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/coderpad.io\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/coderpad.io\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/coderpad.io\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/coderpad.io\/wp-json\/wp\/v2\/comments?post=25260"}],"version-history":[{"count":52,"href":"https:\/\/coderpad.io\/wp-json\/wp\/v2\/posts\/25260\/revisions"}],"predecessor-version":[{"id":25316,"href":"https:\/\/coderpad.io\/wp-json\/wp\/v2\/posts\/25260\/revisions\/25316"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/coderpad.io\/wp-json\/wp\/v2\/media\/25271"}],"wp:attachment":[{"href":"https:\/\/coderpad.io\/wp-json\/wp\/v2\/media?parent=25260"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/coderpad.io\/wp-json\/wp\/v2\/categories?post=25260"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/coderpad.io\/wp-json\/wp\/v2\/tags?post=25260"},{"taxonomy":"persona","embeddable":true,"href":"https:\/\/coderpad.io\/wp-json\/wp\/v2\/persona?post=25260"},{"taxonomy":"blog-programming-language","embeddable":true,"href":"https:\/\/coderpad.io\/wp-json\/wp\/v2\/blog-programming-language?post=25260"},{"taxonomy":"keyword-cluster","embeddable":true,"href":"https:\/\/coderpad.io\/wp-json\/wp\/v2\/keyword-cluster?post=25260"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}