Mastering API Testing with FastAPI: Databases, Dependencies, and More!

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
one of the things that I see many developers struggle with is writing software tests I mean writing tests for Simple Pure functions is easy but how do you write tests for an API that interacts with the database I don't blame you for descending into complete Madness trying to figure that one out so today I'm going to show you how to do this with fast API you'll find the full code example in the git repository the link is in the description My Hope Is that once you have this template you'll be able to do this easily with your own apis as well if you want to be kept up to date whenever I do videos like this or for any other topic related to software development I have a free newsletter you can subscribe via my website ion codes.com now let's dive into API testing the application that I'm going to write test for today is a fast API application that allows you to create read update and delete items whatever that may be actually an item in the system is is really simple you can see right here it has an ID a name and a description and often apis will be built around these types of models of course you won't have very generic items in your system but you're going to have customers orders shopping cards whatever the idea is the same and you can write the test in the same manner so what do we have in this application well it's a single file so we have an item which is a pantic model so it has an ID name and description as I said descript is optional we also have some identic models for creating and updating an item so if we want to create an item then we need a name and optionally a description if we want to update an item we have an optional name and an optional description then we have the database part which is right here so now I'm using a sqlite database here which is simply a file but you can also use a URL that points to a SQL server in the cloud somewhere so what do we need to do for that well we need to create a declarative base subass so this is using SQL Alchemy and then we have an DB item class so this is the representation of the item in the database and there's an ID a name and a description and there's a table name called items ID is a primary key and an index name is a string of length 30 and description again is an optional string so that's how we set things up then we have some boilerplate code to create the database engine based on the URL and we create a session so that we can interact with the database then we create the fast API app we have an on Startup event so whenever the API launches it's going to create all the tables that's what this does and then we have our endpoints so the first endpoint is a really simple endpoint that just gets the root and simply Returns the string saying that the server is running so you can use this for a health check for example and then I have the end points for the create read update and delete operation so this is the create item endpoint so this gets an item create object so a name and description optionally so then what it does is it creates a session then creates the database item from this item create object it adds it to the database commits it refreshes this object so that it actually holds the ID that was generated then it closes the database connection and Returns the item based on the dictionary representation of the item in the database an important thing to note here is that there is difference between the database representation which is DB item so that's this class and the representation of the objects that the fast API API communicates with so these are these three actually item item create and item update and it's actually useful that these are separate things you might think hey it may why do we need a separate model for item and then we have almost the same thing here well sometimes you do want these things to be different for example if you have a user class or user model in your database then you maybe you want to store a hashed password there but you don't want to return a hash password whenever you retrieve the user information for security purposes right so it helps that these things are separate and also sometimes you may want to store things differently in a database than the way that you return the object to the user of your API so it's a good thing that these are separate things even though there is some duplication now in The Code by having to repeat that an item has an ID name and description but if you look at these endpoints well they're basically Now set up in almost the same way so reading an item so that gets an item ID and that also creates a database session then we use a query to get the item if the item doesn't exist we raise an HTTP exception and then we close it and then we return the item updating an item very similar we start a database session we get the item check that it exists if it does then we change the attributes of the item then we commit the change we refresh the item and then we return it again as an item object and deleting the item very similar we try to find the item if it exists then we delete it and then in the end we return the item that was deleted so this is a very basic setup of an API that manages items and you can copy paste this for any type of different object that is available in your API so how do you write tests for an API like this well let's start with something very simple we have this root get request here so we can start by writing a test for this particular endpoint and not yet deal with all the database stuff because obviously that's going to complicate things so what we need in order to write the test is that I create a test folder and file test API in which I'm going to write my tests so the first thing that we need to do is from Fast API test client we import the test client because that's what we need in order for py test to actually create a test line and be able to send requests to the fast API API for testing the second thing that we need to do is from the main files called main before we're going to need to import the app then we need to create a test client which we'll use for endpoint testing and now we can write a simple test so let's say test read root and you see that GitHub copad already supplies the test for me so we do a get request of the client that gives us a response we can then assert that the status code is 200 and that the Json data is the string server is running let me save this file and now let's run by test and we see that it ran one test and that test passed now just to be sure that the test is doing something let's change our rout and add an exclamation mark here and now when we run the test again we expect that the test is going to to fill which it did so great it looks like our tests are being run correctly let me remove the exclamation mark again and let's go back to the test file now the problem with the rest of the endpoints is that they're not as easy to set up as this particular test it is a good idea though that if you start writing test start with the most simple ones just to make sure your testing framework works as you expect and it's also the easiest way to get started but of course we can't simply test creating an item because we need a data base and everything and that's complicated if you want to write test for code that interacts with the database this potentially very problematic because of course you don't want to change things in your database while you're running your test that would be disastrous if that were to happen so you need a way to replace the database the production database with a fake database that you only use for testing but the problem is if you look at the main file you see that creating an item directly creates a session in the database and that means it it's hard to actually replace the database here with something else because it's directly in the endpoint code so first thing that we're going to need to do is to remove the database creation the session creation from the actual code so that we can then replace it with something else and for that fast API already has a solution which is a dependency injection framework that's built into fast API so the first step before we can write any test at all for this particular API we need to make sure that the session is injected into the endpoint instead of being created directly in each endpoint and doing that is actually pretty easy so here I have another version of this exact same API but I'm using now the dependency injection framework of fast API to provide the session to each of the endpoints so what we needed to do is a couple of things so the first thing is that we have a function that provides us with a database session so that that's this code here so in the before version this line was actually directly in each endpoint but here we added to this function and then what I do is that I'm using a generator expression to yield that database and finally after that's done I'm closing the database connection again by setting this up as a generator this means that every time we run a quest after that the database is going to be closed automatically which is what we want now if you look at for example create item the only thing we now need to do is add an extra argument to the function using fast API dependency injection mechanism so here we simply provide this as an argument so we provide a database session that depends on this get database function and now we have access to our session and we can use that to add an item commit refresh it and then return the item so it's exactly the same as before except we don't create this session anymore in the body of the endpoint we simply provide Ed as an argument and fast apis dependency injection will make sure that we do get a valid database session and I did the same thing for reading an item where we also get the session for updating an item where it's also an argument and finally for deleting the item which is what you see here and by the way if you'd like to become better at detecting issues like this in your code you might be interested in joining my free code diagnosis Workshop you can get access by going to I codes diagnos so this teaches you a three Factor framework to look at existing code and point out problems the workshop contains Don use advice lots of practical examples that you can apply right away in your own projects so iron. c/ diagnosis to get access for free link is also in description of this video so now that we have dependency injection set up we can actually start a writing test for these particular endpoints and the first thing that we're going to need to do is take this code creating an engine and local session and actually creating a version of this that we're going to use for testing now there are several ways that you can create a test database and they each have their pros and cons the first thing that you can do is actually work with the production database but hopefully right test that don't directly delete things uh this is a kind of a bad idea so I would definitely not do that you can create a test database in the cloud that serves as your well replacement for your production database but that's also not ideal because that means that if you're running your test and you're changing things in a database you can't run those tests concurrently because if one test changes something in a database while at the same time another test tries to verify something that happened in the database then you're going to run into all sorts of issues another thing you can do is not host a database in the cloud but use a sqlite database file and then use that for testing that sort of solves the problem of not being close to your production database but it's still going to be hard to run test concurrently because they're all going to be reading and writing from that same file unless you really have a different setup and each set of test has their own file you do need to make sure that after each test the database is cleared so that you don't deal with data that is not supposed to be there but this is possible another thing you can do which is even neater is use a SQL light in memory database so then the database itself is not a file at all it's just all in memory and that's all Al a pretty good option especially if you don't need a huge test database so that's what I'm going to do here but remember there's different ways that you can create a database so pick the one that suits best with what you need so the first thing that I'm going to do is copy over some of these lines so that I can then change them in order to use them in my test so we'll need a database URL and we're also going to need this code and let's also add some SQL Alchemy Imports there we go we have create engine and session maker and now instead of using this database file or the URL to your production database let's change this to a memory database and then when we create the engine we simply Supply this URL let's also rename this to testing session local so we're sure that we're using this particular session and that is different from the other session local from the actual API there few things that we need to add when we create the inmemory database in order to avoid problems with the database while testing for example we need to make sure that it's a static connection pool so that we always connect to the same in memory database and that allows us to create something in the database and then later read it which is what we need for our test and same for this particular argument you also need to add this in order to make sure that you don't get inconsistencies while you're writing your test I don't want to dive in too much detail of why this is is exactly needed this is simply how SQL light works and well we need to deal with it basically a forite test so we've created the engine we've created a testing session and since we now have our get DB function we can now replace it with another function that instead of creating this particular session that it creates a testing session that connects with our in memory database so I'm going to create a function called override get DP that creates a testing session local yelds that database and then at the end closes the database connection just like before and now the only thing we need to do in order to replace the original production database with our testing database is go to the app and then we have a dependency overwrite and then we simply say that get DB which will get from the main function equals overwrite get DB so this overrides the original get DP dependency with our testing dependency and now we can start adding test so here's an example of how you could test that an item is properly created so I'm posting to the items endpoint I provide a name and description as expected and then I'm going to assert that we get a 200 response I retrieve the data and then I check that we have a name a description and that data also has an ID before we can actually run this test we need to make sure that tables are correctly created because of course if we create an inmemory database it's going to be completely empty so the part where we have a declarative base and create all the tables so that's happening here we need to make sure that also happens in our testing database so what I'm going to do is I'm going to add two functions a setup function which is going to call this and base we need to import from the main file that's going to create all the tables in a test database and we're also going to need a tear down function which drops all the tables there we go so now we've set up our system to use the test database and make sure it's properly initialized and now these tests are going to work so let's run P test and see what happens so now you see we have two past tests here we have a version of the same file that has test for each of the five endpoints of the API I did one extra thing here because reading an item updating an item and deleting an item obviously requires us to have items in the database which originally we didn't have so what I did is in the setup function I've created a couple of test items in this case just a single one so instead of just creating the tables I create a local session I then create a database item give that an IDE of 100 so I can easily refer to it in the other testing functions and then I've added that to the session committed and closed the session so this allows me to have a basic setup that I can use in my test so when you look for example at testing reading an item so I already provide the ID I do a get request with that item ID and then I assert that we do get a 200 status code and that we get a name description and ID that matches with what I expect same thing for updating an item so here I'm changing the item name and description and then I'll just check that name and description are indeed changed after doing that deleting the item is similar so I posted delete request I just check that we do get an item id back of the item that was deleted and then I try to get the deleted item and verify that I get a response code of 4 or4 that the item isn't found so let's run these tests and we see that all five of them pass as well so this is how you can set up your basic test for an API that interacts with the database so we create an inmemory SQL database we overwrite the injection of the session using this mechanism that's built into fast API and then we write our various tests question for you have you written API test in this way have you used this method of connecting to database do you have other tips options on how to replace the database with a mock database please share them in the comments so what I've done now is basically follow how it's also shown in the fast API documentation I do have an issue with these kind of test though and my issue with this is that we're actually now mixing up two different types of test one is unit tests but actually these are not unit tests in my opinion their integration test because they test both the endpoint and that the routes work correctly and they test database operations at the same time I think you should write tests that do those things separately so we should write tests that are there to check that indeed we're reading and writing to the database correctly and then we have test to check whether the routes are working correctly and currently this is doing both and one of the problems is that if you look at the main file this is hard to do because each of these endpoints directly contain all of the code right so updating an item directly contains the code for updating an item so it's very hard at the moment to write a test that just checks the database interaction and the reason you want to write separate test for this is that you may in the future use that database interaction code in another script or another setting and not directly in an API and then you'd have to redo all the test because now uh it's all directly linked with API routes and that's not a good thing so one thing you can do is split this up a bit and split the code into the API part and into the database operations part and that's the final version that I want to show you so here I have a main file where I've split off the operations into a separate file and this is that operations file so this contains all the SQL Alchemy omm stuff so we have the database item here and then we have functions to create an item read an item update an item and delete an item and I've added a convenience function here to find an item so this checks given a session and an item id whether that item actually exist if it's non if the item doesn't exist this raises a not found error and that's a custom exception subclass that I created here myself this also provides a level of separation so here I'm not raising HTTP exceptions because that's part of the API I'm raising a simple not found error and as you can see at the top there are no Imports here from Fast API at all this completely independent from the API so you could also for example use this code in a command line interface if you wanted to but what's nice is that because this completely independent of fast API testing this is actually easier for example in this file I'm now testing the database operations separately from the routes so if you look at for example test create item I'm calling the DB create item function and then I'm simply passing whatever is needed and now I don't need to deal with responses status code what not if I just want to test that the database integration actually works as expected and what I can do now is write Separate Checks to make sure that validations happen correctly in the routes without dealing with the database for example here I'm testing that if I post an items a create item operation and I simply provide description and not a name that I'm going to get back this status code so I can still do these kind of test these route test but my test for database operations like creating an item or uh reading an item or updating or deleting an item is actually way simpler for comparison this is what the create item test looked before so I needed to get the data from the response and then I can access that using a dictionary whereas here if I look at test creating an item I can simply check that name and description of the item is what I expect it to be because this only deals with database operations and not with API routes so splitting things up the routes and the operations means that your test are going to be a bit simpler and of course if you want you can still write integration test to make sure that the whole system of routes combined with the operations works as expected and the route part of your API now actually looks a lot simpler as well so for example here is the code for reading an item so simply calling this read item function and I catch the not found error and if I find that I'm going to raise an HTTP exception error and same for updating and deleting items by the way I do get a pilent issue here that we could rease the error because we're catching and not found error and I can rease it as an HTTP exception I didn't do that here but that would be a minor Improvement but regardless the API rods are now pretty simple we have nicely separated operations to deal with anything related to the database and we can now also write our test separately final thing that I want to show you in this test is that in the previous version I had a setup and tear down method to deal with creating the tables and creating test items in the database before running the test so what I did here is something slightly different to also show you how that works so what I did here is instead of doing a setup and tear down I'm using a pie test fixture to create a session so this is a generator function just like the get database generator and what I do is I create the tables and then I create the test item so that's pretty similar to what I did before then I yield the session and then I close the session and drop the tables so basically what I'm doing here is that I'm combining the setup and tear down that we have here with the uh override get database here because we're simply testing the operations and then what we can do because this is now fixture is that I can pass the session to each of the tests so here I'm testing create item simply passing the session and then I can create the item with that particular session pent complains again that session is being renamed from the alos scope which is true because this is actually the name of the function but that's how fixtures work so this is another way to deal with the setup and tear down of your test now I've put all of this code in my git repost Story the link is in description of this video feel free to check it out copy paste whatever you need for your own projects I hope this video was helpful if you enjoyed it give it a like that helps the YouTube algorithm spread the word to more people if you want to learn more about how to set up an API using fast API check out this video next where dive into the details thanks for watching and see you soon
Info
Channel: ArjanCodes
Views: 47,729
Rating: undefined out of 5
Keywords: api testing tutorial, testing with fastapi, fastapi api testing, api testing python, api testing, api database testing, api and database, fastapi tutorial python, fastapi python 2023, fastapi python tutorial, fastapi python, fastapi, fastapi tutorial, python tutorial, python fastapi, python api, fastapi tutorial for beginners, api tutorial, fast api, python fast api, python unit test, fastapi tutorial codebasics, api python tutorial, api python, api tutorial python
Id: 9gC3Ot0LoUQ
Channel Id: undefined
Length: 24min 48sec (1488 seconds)
Published: Fri Oct 27 2023
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.