# Python FastAPI Tutorial (Part 17): Testing the API - Pytest, Fixtures, and Mocking External Services

## Метаданные

- **Канал:** Corey Schafer
- **YouTube:** https://www.youtube.com/watch?v=SO7m7nod0ts
- **Дата:** 17.04.2026
- **Длительность:** 1:22:25
- **Просмотры:** 3,362
- **Источник:** https://ekstraktznaniy.ru/video/48959

## Описание

In this Python FastAPI tutorial, we will learn how to test our FastAPI application using Pytest, HTTPX's AsyncClient, and mocking tools like Moto. We'll start by setting up our test structure and fixtures in conftest.py, including a transactional rollback pattern for fast and isolated database tests. From there, we'll write tests for our API routes covering authentication, CRUD operations, file uploads, ownership checks, and background tasks. We'll also learn how to mock external services like AWS S3 and email sending, so our tests don't depend on real infrastructure. By the end of this video, you'll have a solid set of real-world testing patterns that you can apply to your own FastAPI projects. Let's get started...

The code from this video can be found here:
https://github.com/CoreyMSchafer/FastAPI-17-Testing

Full FastAPI Course:
https://www.youtube.com/playlist?list=PL-osiE80TeTsak-c-QsVeg0YYG_0TeyXI

✅ Support My Channel Through Patreon:
https://www.patreon.com/coreyms

✅ Become a

## Транскрипт

### Segment 1 (00:00 - 05:00) []

Hey there. How's it going everybody? In this video, we're going to be learning how to test our fast API application. So, if you've been following along with this series, then at this point, we have a pretty complete blog application. So, we have authentication and authorization. We switched over to Postgress with Olympic migrations. We moved our image uploads into AWS S3. And we've been using async patterns throughout the entire app. So, really, we've built something that looks a lot more like a real production application than just a demo project. Now, that's a lot of moving parts. There are a lot of spots for something to break, especially if we're adding features and making frequent updates. So, the best way to make sure our app works as expected, is to have tests that we can run after every update and commit. And testing is what gives us that confidence that our app actually works the way that we think it will work. It can catch bugs that we didn't know were there. It allows us to refactor and update our code without worrying that things have silently broken in the background. And it's also just a great skill to learn because once you start working on real projects professionally and with teams, testing isn't really optional. It'll be expected on just about any team that you work with. So in this tutorial, I want to show some practical patterns that you would actually use on a real fast API project. So with that said, let's go ahead and get started. So, first we need a couple of test dependencies. So, I'm going to pull up my terminal here and I am navigated into my project here. Now, if you're following along with a more standard Python setup, then you could install this with pip with something like pip install piest. But I have been using uv throughout this series. So, I am going to use uv here. So I will say uv add piest and also since these are development dependencies meaning that we only need them during development and not in production. I'm also going to use this dev flag here for our UV ad. And I will hit enter there. And we can see that is installed. And what that did when it installed it as a dev dependency, if I pull up my project here and I look at my pi project. toml file here, we can see that we have our dependencies, but then we have our dev dependencies down here. And piest is listed there. It's not actually listed in our production dependencies here. Okay, so let me shut that down. Now, another thing that we're going to need to install here is going to be a package called Moto, which is a library that mocks AWS services. We only need the S3 portion. So, we install it with those bracket extras. So, I'm going to say uv dev and then modto and then within these brackets here, S3 as those extras, I will install that. Whoops. And I spelled dev incorrectly there. Let me fix that. Okay, let me clear the terminal. And now we have both piest and modto in our dev dependencies. Now, a couple of notes before we move on. Uh first, we don't need to install httpx separately. When we installed fast API with the standard extras back in our first tutorial, uh that included HTTPX which gives us the async client that we'll be using for our test requests. And HTTPS comes with any io which has a built-in piest plugin for running async test. So we don't need a separate pieest any package either. And just one quick warning here. If you happen to have uh piest async io installed, then check your pi project. toml or piest. ini for a setting called async IO mode set to auto. If that's present, then you'll want to either remove it or uninstall piest async IO entirely. Auto mode conflicts with any IO's built-in piest plugin. And we don't need Piest Async io at all since any IO's plugin handles everything for us. All right, so with that said, let's now organize our test files here. So back in my project, we need to create a test directory in our project root along with the files that we'll need. So up here, I'm going to create a new directory here. And I'm going to call this test. And within test here I am going to create a new file. First I will do a double_anit. py file. Next I am going to do a file called cf test. p py. And I will explain all of these here in just a second. And now our actual test files. So we want to test post. So I'll do test_post. py and then I will also do test users. py. So those will be for our post

### Segment 2 (05:00 - 10:00) [5:00]

routes and for our users routes. So we have an init. py file to help with imports. A confest py for our shared test fixtures. And if you're not familiar with fixtures, don't worry. We'll see plenty of examples in just a bit. They are basically reusable pieces of test setup that piest can automatically inject into our test. And then we have separate test files that mirror our routers structure here. So by that I mean in our routers we have a post router and a users router. So here we have a test post and a test users and that test underscore naming convention there prefix that's important because that's a convention that piest uses to discover test files automatically. Now since our app uses file uploads and does some image processing. I'm going to want a test image for that as well. You can use whatever simple image you want. I have an image file in the root of my project called test image. jpeg. It's literally just a tiny one pixel image that I created for testing. If you want the same files that I'm using, then as always, all the code and files will be available in the description section below. So, I want to also move this test image from our project route into our test directory. So, let me move that into here. And we will say move. Okay. Now, I have that small little test image there. Since we're only using that image for testing, it's cleaner to just keep test assets together with the test code. All right. Now, before we jump straight into the asynchronous setup that we're actually going to use, I want to briefly show the synchronous testing approach as well. Uh because you're going to see this in the Fast API docs and in other tutorials. Fast API provides a test client that you can import from fast API. estclient. And for simple applications, it works really well. So, let me create a temporary file just to show you what this looks like. So, here in our test, I'm going to create a new file. And I'm going to call this test_demo. py. And now, let me close down the sidebar here. And also clean up our user interface here a little bit. And I will only have this uh test demo open here. And now, let's make a couple of imports. So I'll say from fast API let's import fast API and I also want to do from fast API test client and we want to import that test client. Now I'm going to use a throwaway app here instead of importing our real app on purpose. We haven't set up our test environment variables yet and we'll see that in a moment. Our app loads settings from the environment at import time. Uh importing it now would use our development settings which is exactly what we want to avoid in test. So we'll get to our real app just here in a second. But for this demo, we'll just create a tiny fast API app right here in this test file. So I'm going to say demo app is equal to fast API and we will create an instance of that. And now let's do a very simple route here. So I'll say demo app. get and we'll just do a home route here. And I will call this demo home. And we won't take in any parameters here. And we will just return a simple JSON message here. So I will say message and for the message we will just do a string of hello. So just a simple app with one route that returns a message. Now we wrap it with a test client. So I'm going to say client is equal to and we will say test client and then wrap our demo app within that test client there. And then we can write a test function. So I can say okay I want to uh create a function called test homepage here. And the response we will say client. get get and we will just get that home route and we will assert that the response and we will check the status code and make sure that is equal to 200. So let me save that and notice that this is a regular defaf function here. It is not async. So we make a get request and then we use Python's built-in assert statement to check that we got a 200 status code back. And the way assert works is pretty simple. If you give it an expression and if that expression is true, then nothing happens and the test continues. But if it's false, then it raises an assertion error. And Piest catches that and marks the test as failed. So this is how Piest knows

### Segment 3 (10:00 - 15:00) [10:00]

whether a test passed or failed by whether any of its assertions raised an error. And one thing to mention here, if you've used Python's built-in unit test module before, maybe from an older testing tutorial, this is different from how that works. Unit test uses methods like self. assert equals or self. assert where you have to remember which assertion method matches what you're checking. Piest just lets you use the plain assert statement which I personally really like because it's just regular Python and it makes the test much cleaner and more readable in my opinion. So now let's run this test. Now if you're using pip and have your virtual environment activated then you can just run piest directly. So let me show you what I mean. Let me close down my server there that was running. If you have your virtual environment active, then you could just do a pi test and then run tests and then that test demo. py that we just created. And I'm going to tack on this -v here to be verbose. That's what that means. Now, since I'm using UV, instead of running it like this, I'm instead going to put UV run here in front of all of this. And now let me go ahead and run this and make this a little larger here so we can see this output. And we can see that runs just fine. That test passed and we see that it uh passed here as well. So this is a synchronous approach to testing here. And for a lot of apps, this synchronous approach works perfectly uh fine and is a good place to start. The reason that we're not going to use it for our project is that our app uses an async database stack. So we have an async engine, async sessions, async route handlers. And if we try to build out all of our database setup around a synchronous client, then we end up having to do some awkward bridging between synchronous tests and an asynchronous setup. So instead of dealing with that, let's use async client from httpx, which gives everything in the async world uh and makes things much cleaner. So this test demo file here uh was just a temporary file for that quick demo. So I'm going to go ahead and delete that. So here in our test folder, I'm going to delete that test demo. And let's actually uh yeah, I will delete that. I was going to keep it in there. uh but I don't want it showing up in our later tests. All right, so now let's actually start building out tests for our specific application. So first let's build our confest py file. And compest py is a special file that piest recognizes automatically. It's where you put fixtures and other shared setup that you might want available to all your test in that directory. You don't need to import anything from this in your test. Piest handles that for you. So we're going to do this incrementally uh so you can see how each piece fits together. And the very first thing that we need to do in this file is set our environment variables. And this part is extremely important. So place pay close attention to the order here. So let me close down that sidebar there. So our settings class for our application that uses paidantic settings and reads environment variables when it's instantiated. So if we import our app first, it'll load the production settings from ourv file, including our real database URL and our real AWS credentials. We definitely don't want that for testing. So at the very top of this conf. py file before any imports for our application, I'm going to set these environment variables. We'll need the OS module to set environment variables and also async generator for type hinting our fixtures. So here at the top I'm just going to say import OS. And then I'm also going to say from collections ABC let's import async generator. And I'll save that. And now for setting these environment variables. This is a bit of typing. So I'm going to grab these from my snippets. but then we'll go over exactly what we're doing here. So in my snippets, let me grab where I'm setting these environment variables and paste these in. And now let's go over this. So right here, os. environ database URL. We are pointing to a completely separate test database called test blog. Our production database that we are using for our application is just called blog. So we'll create this database in just a bit. We haven't created it yet. And we're also using a fake S3 bucket name that will work with our moto mocking. And we're also setting a test secret key, which is fine because this is just for testing. Now, we're also

### Segment 4 (15:00 - 20:00) [15:00]

going to need to import some dummy S3 and AWS credentials. Again, this is a bit of typing. So, let me grab this from my snippets here and then we'll go over these because we do not want anything from uh that is sensitive information from our actualv file to leak into our test here. So, we want to overwrite all of those here. So, that's what we're doing here. We're overwriting our S3 access key ID, our a secret access key, the region, the AWS access key, uh the AWS secret access key, and the default region here. And all of these should match what we are using for our actual environment variables here in our enenv file. So you can see we're overwriting our database URL, our secret key, and then all of these things here. So let me close that down and go back. So all of those should match up. Now you might be wondering why I'm setting both uh these S3 versions of access key ID and also this AWS access key ID. Uh why we have an S3 and AWS version for both of those. Uh our app uses the S3 variables uh through paidantic settings for its configuration. But boto3 which is the AWS SDK has its own credential chain that looks for these AWS environment variables. When we create a boto3 client directly in our test fixtures, it uses those AWS variables here. So we need to set both just to make absolutely sure that everything stays inside our mocked test environment. So now that we're overwriting these environment variables, now we can safely import from our app because all those environment variables are set and our settings class will pick them up. So now let me paste in the rest of my imports here and then we will go over these here. So let me save that. So here we're importing boto3 for interacting with our mocked s3 and our test. Uh piest for our test fixtures asgi transport and async client. Here this is how we'll make async test requests to our app. We have mock AWS from modto which intercepts AWS calls and redirects them to an in-memory mock. Then the SQL Alchemy imports we need to set up the test database engine and sessions. And finally, we import base and git DB from our database module and the app from our main module. And again, just to reiterate, the order is really important here. The OS environment lines up here have to come before the database and the main imports. This is a common gotcha in testing with Pyantic settings. So just be aware that the import order here matters a lot. All right. So now let's enable the NE IO plugin for async test support. So at the module level here I will just say uh piest plugins and we will set this equal to any io and we can see that we just have a list here of one item. Now this is a standard way to register piest plugins. You can also enable plugins through your pi project. toml or command line flags, but putting it in the conf test. py is the most common approach because it keeps the configuration right next to the code that uses it. Now, what this plugin does is it lets us write async test functions. Normally, piest only runs regular synchronous functions, but since our app is async, we want our test to be async as well. So this plugin gives us a piest neio decorator that we can put on our test functions to tell piest, hey this is an async function, so run it on an event loop. And we'll see this in action here in just a second when we start writing our test. Now this any plugin actually supports multiple async backends like async io and trio. Since our app uses async io, we need to tell any to use that. So we'll create a fixture that returns which backend to use. And this is also our first example of writing a piest fixture. So let me explain the syntax as we go here. So I'm going to say piest dot fixture and then I will set the scope here equal to session. And now I'm going to say defaf any io backend will be the name of this function here. And then we will just return async io. So this piest fixture decorator here is what turns a regular function into a fixture. And this scope equals session parameter here means that

### Segment 5 (20:00 - 25:00) [20:00]

this fixture runs once for the entire test session rather than once per test. We'll see why that matters when we set up our database engine in just a second. Now, before we write our database fixtures, let me take a step back and explain the strategy here. So, first, we do not want our test touching our development or production database. That would be terrible for a lot of reasons. These tests are going to be creating and deleting a lot of data from the database and frequently deleting everything altogether. So, we need a separate test database. So we're going to use a separate Postgress database, not SQL Lite because using the same database type as our production app means that our tests behave the same way our real application does. SQL light has different behavior in some areas like we've seen earlier in this series. So tests could pass locally but still break in production. So we'll prevent that by using a Postgress test database. So we need to create that test database. We saw how to do this in the Postgress video. Uh but let me hop over here to my terminal. And before we create the database, just make sure that your Postgress server is actually running. If your application is running up to this point in the series and you've switched over to Postgress, then it should be running. And we covered how to start Postgress back in our Postgress tutorial. Without it running, all of these commands and our test will fail to connect. So with Postgress running we can run uh create db and let's create that test blog database and that is what we specified in our test file and then I'll set the owner of this to blog user that is the user that we created back in the Postgress video. If you called this user something else then you would just use that. So let me create that. So now let's go ahead and go back to our testing here. So up here at the top we have already set that test blog database as our database URL when we set the environment variables up here at the top. Now the second part of our strategy is what's called the transactional roll back pattern. So this is the industry standard approach for fast test isolation. So here's the idea behind that. So we create all of our database tables once at the start of our test session. Then each individual test runs inside a database transaction. After each test completes, we roll back that transaction which instantly undoes everything that the test just did. And then at the very end of the test session, we drop all of the database tables. So this is much faster than the alternative of creating and dropping tables for every single test. and it means that you don't have to worry about tracking table order for foreign key constraints when cleaning up. Okay, so let's build the fixtures for this. So first a session scoped test engine is what we will create here. We've already written one of these out. Now for the sake of time here, let me grab these from my snippets and we will explain exactly what's going on here. So let me paste this in. So we can see that this is another piest fixture with a session scope. So this creates our test database engine. Pretty simple. We're just using that create async engine function here just like we did in our database. py file, but we're setting it to that test database that we set up at the top of our file instead. And since this is session scoped, uh so it's created only once. And this is why our NEIO backend fixture also needed to be session scoped. Since NEIO backend is session scoped, NEIO creates one event loop that lives for the entire test session. If it were function scoped instead, a new event loop would be created for each test. And that's a problem because this engine would get created on the first test event loop and then that loop closes after the first test. So subsequent tests would get a new loop but the engine is still bound to the old closed ones. Uh so if you ever run these tests and get those confusing event loop is closed errors then that's probably what that is. So keeping these scopes matched up uh prevents that. Now we're also using this null pull here which disables connection pooling entirely. Without it, pulled connections can cause issues between test like connection already closed errors or sale connection problems. Okay, so the next fixture here is going to be a setup database fixture that creates and drops our tables. And I will grab this from our snippets and then explain in detail what is going on here. So let me grab this and I will

### Segment 6 (25:00 - 30:00) [25:00]

paste this under our engine fixture there. So again we have this fixture called setup database. This is also session scoped at the start. It creates all of our tables. We can see here that we're taking our test engine and we are running connection. run sync with base metadata create all. Now this runs sync call is needed because SQL alchemy's createall uh this is a synchronous operation. So we have to bridge it through this run sync function here. So after we create all of our tables, then we yield here, which is where all of the tests will run. And after are done, then it comes down here and runs the rest of this. And here is where we drop the tables and dispose of the engine to clean up connection resources. Okay, so let's keep going here. Now, I know that this is a lot of setup, but this is just what it's like testing a real application with all of the features that we've added. Now, ideally, we would have been writing these tests incrementally as we built the application, but I didn't want to add testing to each individual video uh while we were already covering new topics along with covering the backend and front-end API stuff. So, we saved all of this for a single testing video. So, it's just going to seem like a lot at once, but trust me, with a real application, you'll be happy to set all of this up properly instead of taking shortcuts. Okay, so now we're going to add a key fixture for our test here. Uh, so we're going to add a database session fixture that implements the transactional roll back pattern that we talked about. So let me grab that from my snippets and then we will go over this in detail. So let me grab this and I will paste this in here. And now let me walk through this carefully because there's some important stuff happening here. So you can see we aren't specifying a scope here at the top. That means that this fixture is function scoped which is the default. So it runs for each test. We can see that it's taking in test engine and setup database here as parameters. And first we manually create a connection and then we begin a transaction. And then what we are doing here is we create a session that's bound to this specific connection not to the engine. And that's important because it means that all operations uh the test perform will go through this one connection and this one transaction. Now the line that really makes this work is this join transaction mode is equal to create safe point. This is the fake commit magic. So when our app code calls session. comit, SQL Alchemy intercepts that call. Instead of doing a real commit, it creates a save point. So the data looks committed to the app code. Everything works as expected, but nothing actually has been committed to the database. This is why we can roll back everything at the end because the real transaction was never committed. And then we use this async with uh test async session to open up a session from that session maker. And then we yield it here to that test. So when a test requests the database session fixture, this is the session that it receives. Then the test runs, does whatever it needs to do with the database, and when it's done, we hit this finally block down here. And there we close the session. But we explicitly roll back the transaction which undoes everything that the test did and then we close the connection. So we always want to be explicit about rolling back here so that there's no chance of data leaking between tests. All right. So we've done some good database setup here for our test. Now let's deal with S3 because our app uploads profile pictures to AWS and we definitely don't want our test touching the real Amazon Web Services. For that, we're going to use Modto, which intercepts all calls to Bodto 3 and redirects them to a virtual in-memory AWS environment. So, let me grab this next fixture from our snippets and go over that. So, this one's a little shorter here and let me paste this in. And we can go over this. Now, notice here, uh, these other fixtures here are asynchronous functions. We can see with async defaf here. Uh this one is a regular defaf function here, not asynchronous. And that's totally fine. Moto's mock AWS is a synchronous context manager that patches boto3 globally. Our async app code still works inside of the mock here. So we create our test bucket

### Segment 7 (30:00 - 35:00) [30:00]

that our app expects. Well, first we're actually uh creating that client and then we are creating that bucket that our app expects with our uh mocked environment variable there and then we yield that S3 client so that test can use it to verify side effects uh like checking that a file was actually uploaded and since this is function scoped each test gets a fresh mock state with an empty bucket. Okay, so now let's create uh the client fixture that ties everything together. And this is where we use one of Fast API's best features for testing and that is dependency overrides. Uh again, this is a bit of typing. So I will grab this from my snippets and then we will go over all of this here. So let me grab this and we will explain what is going on here. Okay. So here we can see that we have a piest fixture that is function scoped called client. And inside here we are creating this asynchronous function here called override get db that just yields db session that we created earlier. And then we're using this app. dependency overrides get db is equal to override get db. So remember in our actual application this get DB function that provides database sessions to our route handlers through the depends system. Well this app dependency overrides is a dictionary that lets us swap out any depends function for testing. So instead of our routes getting a normal database session they'll get our transactional test session that rolls back after each test. And this is so much cleaner than trying to mock things manually. So fast API's dependency injection system makes testing really nice in this case. So then after we override that git db dependency, then we create our async client here. Now this is the part that ties our test client to our actual application. So async client is the standard HTTPX client that you'd normally use to make real HTTP requests over the network. But for testing, we don't want to actually start a server and make real network calls. That would be slow and adds a lot of complexity. So instead, we tell async client to use a different transport mechanism here. And that's what this ASGI transport does. Fast API is an ASGI application. ASGI stands for asynchronous server gateway interface and it's basically the standard that defines how async web servers like Uvicorn talk to async Python web apps like fast API. Don't worry too much about those details. The main thing to know is that it's a common interface that any ASGI compatible tool can plug into. So ASGI transport takes our app and lets the async client send requests directly to it in memory without ever touching the network. So when we call client. get, you know, slapi/post in a test, that request goes straight into our fast API app, runs through all of our middleware and route handlers, and returns a response, all without a real HTTP server. It's simple. It's fast and it gives us realistic behavior because it's running our actual app code. Now, the base URL here is required. Without it, relative URLs and redirects could behave oddly. The URL doesn't matter. Uh we're not actually making network calls. It just needs that to work properly. So, I've just set that to test. So, we are yielding that client for every test. After the test is done, we clear the dependency overrides so that they don't leak between tests. Now, one thing I should note here, uh, Async client with ASGI transport, uh, doesn't run your app's lifespan events. So, the startup and shutdown handlers won't fire for our app. That's totally fine because our lifespan only disposes of the production engine, which our tests don't use anyway. But if you had an app with important startup code, like populating a cache or something like that, you'd want to use the ASGI lifespan package. That's lifespan manager to run those events in your test. But that's going to be outside the scope of what we're doing here. All right, so we're just about finished up with our test setup. Uh, but the last piece of our shared test setup is authentication helpers. Now, since many of our endpoints require authentication, then we're going to be creating test users and logging them in a lot. So, let's make some helpers for this. Again, I know that this is a lot for setting up our test, but you have to know how to mock these kinds of things for an application like this. You never want to create any of this stuff for real in our tests. And after getting it

### Segment 8 (35:00 - 40:00) [35:00]

all set up once, then you can reuse them in all of our tests. Okay. So, let me grab this from my snippets and we can see what this looks like. So, I'm going to grab both actually all three of these helpers here for authentication. And let's paste this in and I will go over all of these helpers here. So, like I said, we have three helpers. Let's walk through each one individually. So, this first one is create test user. So this creates a test user through our API and returns the response data. So you can see it takes in uh some default values here. We have client, username, email and password. We have default values of a test user with a test email and a test password. So that lets us call this without arguments for a common use case. But we can also pass in custom values when we need multiple users. And then we are just posting this user data to our API users route to create this test user and then asserting that the user was created. So you can see here that we are asserting that the response status code is 2011. And you'll notice that this assert here has a comma followed by a string. So this second argument is an optional failure message. So this assert is basically saying I expect this status code to be 2011 which is a successful uh creation response and if it's not raise an assertion error and raise it with this message here. That message can be really helpful when a test fails because it includes the actual response text so that you can see exactly what went wrong. So our next helper here is login user. So this logs in a user and returns the access token. Now notice something important here. When we are logging in our user, we are doing this post to API users token. And we are using data here and not JSON for the login request. That's because ooth 2 password request form expects form data. We saw this in our video where we set up all the password stuff, but this is just a reminder. If you use JSON here, then the login will fail. That's a common mistake when testing off endpoints for the first time. So, just remember login uses form data, not JSON. And then we're just asserting that was successful and then returning that user's access token if they get logged in. And then the last function off header here creates the authorization header dictionary that we can pass to our request. Super simple, just one line there. So these helpers are just going to make our test much cleaner and more readable. All right, so that was a lot of setup, but now we have a really solid testing foundation here. So now let's start writing actual test. So let's begin with testing our post. So, I'm going to open up test post. py here. And let me close down the sidebar there. Okay. So, first we need some imports. So, I'm going to import piest. And I'll go over these after I get everything imported here. So, from httpx we will import async client and also we need to import our helpers. So from test we want to import and we had o header we also had create test user and we also had login user. Okay so we import piest for the neio marker uh async client for typent and our helper functions from conf test. So let's start with the simplest possible test here. So, we want to test that a get request to our API post route returns an empty list when the database is fresh. Remember, thanks to our transactional rollback, each test starts with a clean database. So, to do this, I'm going to say piest. mmark. I io. So, this is that piest any decorator that I was telling you about that lets us run async test functions without it. Piest wouldn't know how to handle async before our function definition. So now we can create that async function definition and we'll just call this test uh get posts empty and we will take in client and that is going to be an async client and let me go ahead and just write out this test and then we will uh discuss it in more detail. So first I'm going to uh get that route. So I'll say client. get and the route that we want is going to be for slashappi

### Segment 9 (40:00 - 45:00) [40:00]

slashpost and then we can assert that the response status code is equal to 200. And let's also test that response data. So I can say data is equal to response. json. And now let's test a couple of things with that JSON. So we can say assert that the data post is going to be an empty list. So we will just say that it's equal to an empty list. We also want to know that the total that we get back is going to be equal to zero. And also if you remember for our pageionation we have that has more flag and it is set to true if there is more post to load. Since this is empty has more should be equal to false. Okay. So that's our entire test. Now there's one part here that might feel a little magical the first time that you see it. So look how we are getting our client. We have a parameter on our text test function here called client and we never import it from anywhere. We don't pass it in when we call the test but somehow it just shows up with our fully configured async client. So how does that work? So this is piest fixture injection in action. When piest runs a test, it looks at the test functions parameters and tries to match each parameter name to a fixture with the same name. It searches the current test file first and then the conf test. py in the same directory and then any compest. py files in parent directories. So because we have a fixture called client. So if I go back to conf test here, we can see that we have our fixture here called client. I think I passed it up. It is right here. So back in our test, piest sees this client parameter, finds that matching fixture, runs the fixture to get its value, and then passes that value into our test with no imports needed. And remember our client fixture also depended on DB session and mocked AWS. So Piest runs those two in the right order and everything uh wires together automatically. That's actually one of the things that makes Piest so nice to work with. You declare what you need by name and Piest just gives it to you. And then what we're doing here is we just make an HTTP request and assert on that response. So it's nice, simple, and clean. Okay. So now let me save this and run it right away to make sure our entire test setup is working with this very simple test here. So let me clear out my terminal. So let me hit up here uh where we tested our demo test before, but instead of test demo, I'm going to do test post. py instead. So let's run this and hopefully oh and we got an error here. Uh it is saying that we have a key error here. Uh data post uh instead of post it should be post is equal to an empty list. This is why I use a lot of snippets because when I type I make a lot of uh typing mistakes. So let me rerun that. Okay. Now we are getting a passing test here. So this is great. So we have our first test passing. That green passed there is always satisfying to see. And more importantly, this confirms that our conf test. py, our test database, and our fixture chain are all wired up correctly. Now, this is what a passing test looks like, which is what we want. Uh I was going to show you what a failing test looks like. Uh but we've already seen that. But just to show you again, if I instead say that total is equal to one here, which would be a failing test, then let's see again what a failure looks like. So I'll run that and we can see that fails. And it tells us the exact file and the exact test and what failed. It's saying that assert zero equals 1 was not true. Okay. So now let me set that back to zero so that we have a passing test there. And I'll rerun that. Okay. And we're good to go. All right. So now let's just add some more tests here. So we could add whatever test we'd like. So for example, let's test that uh we get a 404 when we try to fetch a post that doesn't exist.

### Segment 10 (45:00 - 50:00) [45:00]

So I'm just going to copy and paste that other test there. And now let me uh rename this here. So I will say uh test get post not found and the route that I want to test here. Let's test for a post that we know doesn't exist. So we'll just search for post 999. And we will assert that the status code here. I'll get rid of these others here is equal to 404. And let's also assert here that the response JSON and it should come with a detail message here and that detail message should be post not found. Now remember even if you have a popular application and you have over a thousand posts we are starting with a fresh database a fresh test database each time. So, we know that this post 999 doesn't exist. So, you can kind of get a feel for what these tests look like and what we're doing here. So, let me pull back up the terminal, rerun that. And now we can see that we have two passing tests there. Okay, great. All right, so we've covered reading some data. Now, let's move on to creating post. And we'll start with the successful case which is creating a post when a user is authenticated. Now this one's a little longer. So let me grab it from the snippets and then we will walk through this. So this is a little bit longer of a test here but we will go through all of this here. So let me paste this in and save it. So we can see here same decorator. We're calling this test create post success. Now these first three lines here the this is our authentication setup and you're going to see this pattern a lot in our test. So we call our create test user helper to create a user through the API. Then we call login user to log that user in and get back an access token and we grab that in a variable. And then we wrap that token in our off header helper to build that author authorization header. So get used to seeing this threeline setup here at the top of any test that needs an authenticated request from a loggedin user. And remember when we wrote those helpers in confest. py, we gave them default values. So create test user creates a user with the username of test user. The email was testacample. com and the password that was the default value was test password 123. And login user uses those same values uh those same defaults for logging in. And that's important to remember because we'll be checking against those values in our assertions uh throughout the rest of our test. So now instead of making a get request, we're making a post request to API-post with a title and content and we are passing in our O header and then we verify that we are getting a 2011 created status code and then we check that the response has all the fields that we expect. So the title and content should match what we sent. The user ID should match the ID of the user that we created. And we check that an ID and date posted exist in the response since those are generated by the database. And we also check that the author object is populated with the correct username since our endpoint eager loads the author relationship. And remember, all of this runs inside of our transactional rollbacks. So even though we created a user and post in this test, the next test starts with a completely empty database. So there's nothing to clean up here. So now instead of running these tests after every one that we add in, let's just keep adding in some tests here and we'll run them all at the end. So now let's test the unsuccessful case. So, what happens when someone tries to create a post without being authenticated? Well, let me grab that from my snippets as well. So, let me grab this and now paste this in. And we can see that this is fairly similar here. We're making a post request to API post but we didn't create a test user and we are not passing any authorization header and we expect that we will get a 401 unauthorized response. 401 means that the user is not authenticated. They haven't provided a valid token and the not authenticated detail message is actually the default that fast API's ooth 2 passwordbearer returns when

### Segment 11 (50:00 - 55:00) [50:00]

there's no token. So this test is pretty simple. We just hit the endpoint with no credentials and verify that we get rejected. Now if you remember in our authentication video, I said that there is a difference between a 401 response and a 403 response. Uh because we're going to test both of these here. So a 401 means that you're not authenticated. So either no token or you have a bad token. A 403 means that you are authenticated, but you're not allowed to do the thing that you're trying to do. So, we'll test the 403 case in just a second. But first, let's test the successful case for updating a post. So, the owner of a post should be able to update their own post. So, let me grab this from my snippets here, and we will walk through this. And I'm hoping that by showing you a bunch of different tests here, you can kind of get the different strategies that each test involves. So a similar beginning here. Uh at the top we have our authentication three lines here. So we're creating a test user. We are logging them in and then we are creating a post here. So here is where they create that post by posting to the API post route. After that is created then we are sending in a patch to try to update that post with updated title. So since that is the post that they created we are asserting that the uh response from that update is a 200 and then we just check that the title was actually updated. So now let's test that 403 case that I was talking about. So this is testing ownership and authorization which is one of those things that matters a lot in a real application. Just because someone is logged in doesn't mean that they should be allowed to update someone else's content. So for this test uh let me grab this and we will walk through it here. This is going to be a little different than what we've seen so far. So let me paste this in. So for this test we're creating two users. So right here we have a create test user with a different username and email. We're using the default password and then we grab the token one from that user one and then we are using that user one token to create a post here and now we are creating a user two still using that default password. So we create a user two here. Log in that user two. And then we try to update with a patch that first post that user one created. And since user two is trying to update that post that they do not own, we should get a response status code here of 403. And the detail should be not authorized to update this post. Okay. So for our last test in this users file, let's write a longer one that tests pageionation. So far we've been creating one post at a time, but our endpoints support skip and limit query parameters and we want to verify that those work correctly. So we'll create a user, log in, create several post, and then test the pageionation parameters. So again, let me grab this from my snippets here. This one is a little longer here, but not too bad. So, let me paste this in and we will walk through it here. So, let's go ahead and walk through what's happening here. So, at the top, we have our usual threeline authentication setup here. We create a user, log them in, build that authentication header, and then we have a for loop that creates five posts here. So we loop through a range of five and for each iteration we make a post request to API post with a title and content and we're using frings to give each post a unique title and unique content and inside the loop we assert that each one of those returns a 2011 status code which is the status code for created. So that just ensures that if any post fails then we know right away rather than having a confusing failure later on. Now once we have those five posts we can actually test the pageionation. So the first check is the default behavior with no query parameters. So we're just doing a get request here to our API post route and we expect to get all five posts back. So we're asserting here that the total number of posts is five. The length of those posts is also five and has more is equal to false because we got all the post and there's nothing more to fetch. Now this next test we're testing the limit parameter. So we hit the client. get API post and we set this

### Segment 12 (55:00 - 60:00) [55:00]

limit equal to two. So we're saying give me atmost two posts. And then we verify a few things here. So the total uh should still be five because total is the to total number of posts in the database not the number returned. And the length of the posts here should be two because that's what we asked for. And then has more should also be equal to two because there are more posts beyond what we got back. The has more flag is what the front end uses to know whether to show a next page pageionation button. And then finally this last test here we are testing both skip and limit together. So we're doing a get request to API post. Skip is equal to two limit is equal to two. Basically that means skip the first two then give me two more. And we should get post three and four back. So here I'm testing that the total is still five. uh the length of the post is two and we also verify that skip and limit come back in the response so that the front end knows where it is in the pageionation and if you wanted to you could add to this test and test the specific title and content of the post that you get back but I think just testing that uh skip and limit are in that response is good enough for what we're doing here so I think I called these individual tests but they're actually not there are three different responses that we're running assertions on. But this is all one test here uh called test get post with pageionations. Uh it's covering a lot of ground. It tests the default case, the limit parameter and the skip parameter. Uh it tests the has more flag and the response shape. So we've got run a good bit of uh test there. We've put in a lot of tests for our test post test. So now let's run that and make sure that everything is working. So again, let me run that. We can see that it went through those one at a time and every one of those passed. So all seven tests passed in about less than a second. Okay, so all of our post tests are passing. Now let's move on to the user test. And there's going to be a few more things to test with users than there were for post. So let me go ahead and close down our test post there and let me open up test users. py. Now for our imports, we're going to need a few extra things for this file. So I'm going to say from IO, let's import bytes io. And also from pathlib, we want to import path. And also from unit test, we want to import async mock and also patch. And let's also import piest from httpx let's import async client and also let's import our helpers. So from test we want to import uh let's see we had o header we had create test user and we had login user. So I'll save that. So we need bytes io for file uploads. We need path for reading the test image and async mock and patch for mocking our email function. And we'll use all of these as we go. Okay. So first let's start with testing uh validation errors. So when someone tries to create a user uh with missing required fields, they should get a 422 status code. Now, I think we have a pretty good feel for the flow of our test at this point. So, for the t sake of time, I'm going to grab the rest of our test from snippets, but we'll go over each one in depth. So, this one is going to be our test user validation error. So, let me grab this one and let's go over it. So in here we are trying to create a user by posting to the API users route but we're sending a request with only a username and missing both the email and the password fields. So we get a 422 unprocess unprocessable entity status code and that just means that the data format is wrong. So pyantic validation failed because required fields are missing. So we can see here that I'm asserting that the status code is 422. I'm also asserting that email and password both appear in response. ext somewhere. Now response. ext is just the raw string version of the JSON response that came back. And since paidantic includes the field names of any missing fields in its error message, we can do a simple substring check to make sure that

### Segment 13 (60:00 - 65:00) [1:00:00]

the error mentions both fields. We could parse the JSON and dig into the error structure, but for what we're testing, uh, this is way simpler and easier to read in my opinion. Now, when you're thinking of test to write, you want to think of different ways that people could hit your routes. So, here's an important distinction. So, 422 here means that the data format is wrong, like missing fields or wrong types. But what about when the data is valid but it breaks one of our apps rules. So for example, trying to register with an email that's already taken. That's not a schema validation problem. That's a case where we've broken a rule that we laid out in our application that emails have to be unique. So let me grab from our snippets and we will test this use case. So let me grab this and paste this in here and we can test that this works here. So here at the top we call our create test user helper to create a user with default values which remember includes the email of test@acample. com and then we try to create another user with a different username here but the same email. So we can see test email@gmail. com and the password is different as well. Now what we would expect is a 400 response if someone attempted to do that not 422. So the data is perfectly valid and all the fields are there and they're the right types but it breaks one of our app's rules which is that you can't have duplicate emails. So 422 is for format errors and 400 is for when the data is fine but breaks one of the rules that we've laid out in our app. So that's an important distinction in API design. We've discussed that a bit earlier in the series, but just wanted to reemphasize that here. And we can see that we're testing that the detail that we get back is that the email is already registered. Okay. So now let's test a successful user creation. So let's see what this would look like. So I'll grab this from my snippets here and go over this. Okay. So we see that we have a test here called test create user success. So this time we're not using our create test user helper function. Uh we're making the request directly so that we can use custom values and verify the response in detail. So, we post to our API users route with a brand new username, email, and a password. And then we assert that the response that we get back is 2011. Then we're reading in that data. We are saying that the username that we got back should be new user that we set up here. The email should be what We're also asserting that ID is in the data. Image path is in that data. password is not in the data and password hash data. And I really want to point out those last two assertions there. So we're verifying that the password is not in the response and that the password hash is not in the response. So that is a security check that we're doing. So you should never send passwords back to the client. And this is exactly the kind of thing that tests are great at protecting. So if we accidentally changed a response schema later and started linking sensitive information, then a test like this would catch that. All right. So at this point, I think you have a pretty good feel for the basic patterns. We've created users. We've tested successful and unsuccessful requests. We've checked status codes and response data. So instead of writing a bunch of tests for things like login and the forward slashme endpoint which would just be variations of what we've already done, let's jump to some tests that show some new techniques that we haven't seen yet. So I'll list a few of those other tests as practice exercises at the end of the video that you can write on your own using the pass uh patterns that we've already covered. But let's move on to some tests that are using some different techniques. So, the next test that I want to write is for uploading a profile picture. And this one is interesting because it's our first file upload. And it's also the first time that we're going to use our mocked AWS fixture to verify that something was actually written to S3. So, let me grab this from my snippets and then we will walk through this. So, let me grab this snippet here and paste this in and let's walk through all of this here. Okay. So the first thing to notice here is in the parameters that we are taking in along with the client fixture that we've been

### Segment 14 (65:00 - 70:00) [1:05:00]

using, we're also requesting the mocked AWS fixture as a parameter. This is the S3 client that our fixture yields which we'll use to verify that the upload actually happened. And then we have some usual O setup here. So we're creating a test user and logging them in. But this time we are grabbing the user object and a variable so that we can get their ID. And then we also grab the token here. Now we don't have our o helper here like usual in this test. We'll use our o helper in line in the request rather than building it ahead of time just because we only need it once. So the next thing that I'm doing here is I am reading our test image from disk. So I'm using this path file. p parent to get the path to the current test file. So that's what this does here. This path with this double file that gets the path to the current test file and then we get the current files parent. So basically we're just getting our test directory here with these lines here. And then we grab that test image. jpeg from our test directory. And then we are reading the bytes and that gives us the raw bytes of the file and we are saving that as image bytes there. And then we make the actual upload request. So we are doing a patch to this API users and then a this user ID in the URL forward slashpicture. So this is the URL that we're sending a patch request to. Now this part is different here. So we're using a files equal and then within here we have a file and this file is a tupil. So we can see here a tupil of three different values. We have a file name. We have the file content which is our bytes io image bytes and then the content type which is image JPEG. So this files equal to this file here that tells httpx to send this as multi-art form data just like a real file upload from a browser and then passing in our image bytes to bytes io acts like a an actual file. Now this file name of profile. jpeg that doesn't have to match our actual file name on disk. We could put any name in there. It's just a file name that gets sent to the server as part of the multi-art form. Kind of like simulating what a browser would send if a user picked a file called profile. jpeg. The actual file content is the image bytes that we read in. So after the upload, we are checking that the response has the 200 status code there. And then we check that it has the expected image data. So the image file should not be none. It should end with JPEG and the image path should contain S3 since it's an S3 URL. And now here's a really cool part. We then use the mocked S3 client to verify that the file actually exists in the mocked S3 bucket. So we list the objects in the bucket and check that there's actually one object uh by checking the length here and that its key uh matches the image file name in the response. So by mocking S3 here we're not just testing the response, we're also testing the actual side effect of the upload. And all of that happens without touching uh the real Amazon Web Services. So the thing to take away from that is that even complicated things like this that have a lot of interactions and side effects, they can still be properly tested as long as we set this up correctly. And that's going to make updates to our application much easier because we'll know if any part of this upload chain gets broken. Okay, now let's test one more thing. So let's test our background task for sending password reset emails. So, we don't want to actually send emails in test. So, we'll mock the email function. So, again, I'm going to grab this from my snippets here. Let me grab this. And we can see that this is the last snippet for this video. Let me paste this in and we'll go over this. So, first again, we are creating a test user with the defaults since our forgot password endpoint needs an actual user in the database to send the reset email to. And then we are using this patch context manager here as a mock send. Now if I go back up to my imports here, this patch actually comes from unit

### Segment 15 (70:00 - 75:00) [1:10:00]

test. mmock. So let me scroll back down here. So what we're doing here is we are using unit test. mmox patch as a context manager to temporarily replace this send password reset email function with an async mock instead. So even though we're using piest, it's common to reach for Python's built-in unit test mock for things like this. Piest does have its own monkey patch fixture for simply swapping things out, but unit test mock gives us mock objects that track how they were called. Uh, which is what we need here. So, we can verify that the function was actually awaited and with what arguments. And we're using async mock here specifically because our email function is asynchronous. So, we need async mock instead of a regular mock. Now, another thing to notice here. Notice the path that we're patching. We are patching routers do users send passwordreset email. Now this is one of the most common gotchas with mocking in Python. So let me explain this here. Now this send password reset email this actually lives inside of our email utils module here. So it is actually right here. So if it lives in our email utils, then you might wonder why we're patching routers. Send passwordword email when send password reset email actually lives in our email utils module. Why not patch it there instead? Well, here's the key thing to understand. So when routers users. py PI uh does from email utils import this send password reset email function that doesn't create some kind of live link back to our email utils module. It instead grabs a reference to that function and creates a new name for it inside of this routers users namespace. So now at that point there would essentially be two names pointing to the same function the original in email utils and a local reference at this routers do users namespace here. So when our route handler runs and calls this send password reset email function it looks up that name in its own module namespace which is routers do users. If we patched email utils send password reset email, uh, we'd be replacing the original, but our route handler would still use its local reference and call the real function. Our mock would never actually get touched. Now, I know that explanation might be a little confusing, but basically the rule is when you are mocking something, you patch where the name is looked up, not where the function is defined. So our route handler looks it up here in routers do users. So that's where we're going to patch it. So basically mock this where it's used and not where it's defined. Okay. So with that said, we are patching send password reset email with async mock. And then inside of this context manager, we are making a post request to API users forgot password with a JSON body containing the email of the user that we just created. And then we verify a few things here. First, we verify that we get a 202 accepted response. And then we check that the mock was awaited once using assert. awwaited once. Now you might be wondering how this works with background task. Well, background task runs tasks after the response is sent. But our test async client actually waits for background task to complete before returning. And when background task calls an async function, it awaits it. So our async mock gets awaited and assert awaited once passes here. And then we're also verifying our call arguments here uh just to make sure that we have the right email username and that the token were passed to the function. So I hope that all made sense. You might have to go over that a few times. Uh that's the most complicated test for sure uh using that mock. But that is how we would do that. So now we've written a good bit of tests here. So now let's run all of our tests together and make sure that everything is working. So instead of just running a single test file, I'm actually going to run piest on the entire test directory and it will

### Segment 16 (75:00 - 80:00) [1:15:00]

automatically find files that have that test underscore prefix. So what I can do here is instead of running a single file, I can say run piest just on that test directory. So let's run that and it will find uh both of those files. So we can see here that we have our test post. py and our test users py and all of those tests pass. Okay, awesome. So now we have a lot of tests that are passing for our post and for our users. So now let me also show you a few useful piest options here. So like we saw before, we can run a single test file just by specifying that exact file right there. So if I run just test post, then it only runs those uh post test. Now let me run that again just because it will show me the individual tests that passed within here. Now let's say that I just wanted to run a single specific test. So, for example, let's say that we wanted to test this te test get post empty here. So, I could just grab all of that there. And now, if I just wanted to run that one test, then I could paste that in. We can see that it's using these double colons here after the single test file. If I run that, then it's just going to run that one single test. Now, I've been using this verbose flag here, this option that gives us verbose output. If we wanted to see if the test just passed without much more info. So, I'll run all of our tests but without that dashv option there. If I do that, then you can see that now it just puts little dots here for all of these passing tests and just get basically gives you a summary. Now, another option that's pretty popular is if you want to see print output from your test, then you can use this - S flag here. We don't have any print output in our test, but you might want to use print statements for certain debugging purposes. Uh, so to get those to actually display, you would just use that - S option and it would put the print output there in your test output. Now, there are obviously some other piest options, but these are good to know as you're getting started with writing and debugging your own test. Now, before we wrap up here, I want to mention that we definitely didn't hit 100% test coverage in this video, and we're not going to write a test for every single endpoint in our app. This tutorial is already getting long, but what I tried to do here was show you all of the unique patterns and techniques that you're likely going to run into when testing a real fast API application. So things like setting up your test database with transactional rollbacks, mocking external services like AWS, uh dependency overrides, file uploads, background task mocking, authenticated requests, ownership checks, all of the stuff like that. Once you understand those patterns, then applying them to your own routes and endpoints is mostly just a matter of repetition. So my recommendation here is to take what we covered here in this video and try writing tests for some of the other endpoints for our app on your own. That's going to be the best way to actually internalize and learn this stuff. Now I know that AI is a bit of a controversial topic in programming right now. Some people love it, some people hate it, and some people are a little in the middle. But I think that one of the best use cases for AI right now actually is unit testing. Uh you can have 100% test coverage on paper but still be missing a lot of functional test and edge cases that you didn't even think to test. So I think it's worth spinning up something like cloud code and just saying hey I have this fast API application. Are there any test cases that I'm not properly testing or edge cases that could break my application that I'm not testing? When I've done that on my own projects, I've gotten some good answers from AI when I use it in that way. Okay, so let's quickly recap what we've built. So in our confest py, we set up environment variables that override our production settings before any app imports. We created session scope fixtures for the engine and database setup. We implemented the transactional rollback pattern for fast and isolated test. We added a mocked AWS fixture that provides a virtual S3 environment. And we created an async client fixture with dependency overrides and also built those helper functions for authentication. And once

### Segment 17 (80:00 - 82:00) [1:20:00]

we had that solid setup in place, then we tested our users and post routes using different techniques and mocking that you'll see all the time in the real world. Now, as I've said several times throughout this series, we're mainly focusing on the backend API here, even though we do have a front-end templates that we're using as a convenience layer for users to access that API. So, we could add end toend tests here using something like Playright or Selenium that actually goes out and navigates through our front end to make sure things are being displayed correctly and that button clicks do what we think that they should do, but that's going to be outside the scope of this video. I do plan on doing a dedicated video on Playright and Selenium in the near future. Uh so be sure that you're subscribed for those, but they won't be included here in the fast API series. Okay. So now that we've covered testing, we've basically covered everything leading up to a production ready application. In the next video, we're going to learn about taking this application and deploying it on a server so that it's accessible to everyone. So the next tutorial will specifically cover deploying to a VPS, which is a virtual private server, but the video after that will learn how to use Docker to containerize our application and deploy it to a cloud service as well. So all of these pieces finally get to come together to deploy a real application. But I think that's going to do it for this video. Hopefully now you have a good idea how to test your fast API application using Piest and Async client and tools like Modto for mocking AWS services. In the next video, we'll be learning about deploying our application. But if anyone has any questions about what we covered here, then feel free to ask in the comment section below and I'll do my best to answer those. And if you enjoy these tutorials and would like to support them, then there are several ways you can do that. The easiest way is to simply like the video and give it a thumbs up. Also, it's a huge help to share these videos with anyone who you think would find them useful. And if you have the means, you can contribute through Patreon or YouTube. And there are links to those pages in the description section below. Be sure to subscribe for future videos, and thank you all for watching.
