[Backend #5] Write Golang unit tests for database CRUD with random data

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
Hi guys, welcome back! In the previous lecture, We have learn how to generate golang CRUD code to talk to the database. Today we will learn how to write unit test for those CRUD operations. Let’s start with the CreateAccount function. I’m gonna create a new file account_test.go inside the sqlc folder. In golang, we have a convention to put the test file in the same folder with the code, and the name of the test file should end with the test suffix. The package name of this test file will be db, the same package that our CRUD code is in. Now let’s define function TestCreateAccount. Every unit test function in Go must start with the Test prefix (with uppercase letter T) and takes a testing.T object as input. We will use this T object to manage the test state. The CreateAccount function is defined as a method of Queries object, and it requires a database connection to talk to the database. So in order to write the test, we have to setup the connection and the Queries object first. The right place to do that is in the main_test.go file. I will define a testQueries object as a global variable because we’re gonna use it extensively in all of our unit tests. This Queries object contains a dbtx, which can either be a db connection or a transaction. In our case, we’re gonna build a db connection and use it to create the Queries object. Alright, now I’m gonna declare a special function called TestMain, which takes a testing.M object as input. By convention, the TestMain function is the main entry point of all unit tests inside 1 specific golang package, which, in this case, is package db. OK, now to create a new connection to the database, We use sql.Open() function, and pass in the db driver and db source string. For now, I’m just gonna declare them as constants. In the future, we will learn how to load them from environment variables instead. The db driver should be postgres. And the db source, we can copy from the migrate command that we’ve written in the previous lecture. Alright, The sql.Open() function returns a connection object and an error. If error is not nil, We just write a fatal log saying we cannot connect to the database. Else, we use the connection to create the new testQueries object. The New() function is defined in the db.go file that sqlc has generated for us. Now the testQueries is ready, All we have to do is to call m.Run() to start running the unit test. This function will return an exit code, which tell us whether the tests pass or fail. Then we should report it back to the test runner via os.Exit command. OK let’s try to run it We’ve got an error: cannot connect to db: unknown driver “postgres”. This is because the “database/sql” package just provides a generic interface around SQL database. It needs to be used in conjunction with a database driver in order to talk to a specific database engine. We’re using postgres, so I’m gonna use lib/pq driver. Let’s open its github page, and copy this go get command. Run it in the terminal to install the package. Now if we open the go.mod file, we can see lib/pq is added here. It says “indirect” because we haven’t imported and used it in our code yet. So let’s go back to the main_test.go file and import the lib/pq driver. This is a very special import because we don’t actually call any function of lib/pq directly in the code. So if we just import like this, the go formatter will automatically remove it when we save the file. To tell go formatter to keep it, we must use the blank identifier by adding an underscore before the import package name. Now if we run the TestMain() again, There’s no errors any more. And if we open the terminal and run go mod tidy to clean up the dependencies, we can now see that the require lib/pq in go.mod file is no longer indirect, since we have imported it in our code. Alright, now the setup is done, we can start writing our first unit test for CreateAccount function. First we declare a new arguments: CreateAccountParams. Let’s say owner’s name is tom. The account balance is 100, And the currency is USD. Then we call testQueries.CreateAccount() Pass in a background context, and the arguments This testQueries object is the one we declared in the main_test.go file before. And the CreateAccount() function returns an account object or an error as result. To check the test result, I recommend using the testify package It’s more concise than just using the standard if else statements. So let’s copy this go get command and run it in the terminal to install the package. Alright, Now to use this package, we need to import it first. Testify contains several sub-packages, but I’m just gonna use one of them, which is the require package. With this import, we can now call require.NoError() Pass in the testing.T object and the error returned by the CreateAccount function. Basically, this command will check that the error must be nil and will automatically fail the test if it’s not. Next, we require that the returned account should not be an empty object. After that, we would want to check that the account ownber, balance and currency matches with the input arguments. So we call require.Equal() Pass in t, the expected input owner, and the actual account.Owner Similarly, we require arg.Balance to be equal to account.Balance and arg.Currency to be equal to account.Currency. We also want to check that the account ID is automatically generated by Postgres, So here we require account.ID to be not zero. Finally, the created_at column should also be filled with the current timestamp, The NotZero() function will assert that a value must not be a zero value of its type. And that’s it! The unit test is completed. Let’s click this button to run it. We see an ok here, so it passed. Let’s open the simple_bank database with TablePlus to make sure that a record has been inserted. Here it is, We have 1 account with id 1, the owner, balance and currency values are the same as we set in the test, and the created_at field is also filled with the current timestamp. Excellent! We can also click Run package tests to run the whole unit tests in this package For now it just has only 1 test, but the nice thing is the code coverage is also reported. At the moment, our unit tests cover only 6.5% of the statements, If we look at the account.sql.go file, We can see the CreateAccount function is now marked with green, which means it is covered by the unit tests. All other functions are still red, which means they’re not covered. We will write more unit tests to cover them in a moment. But before that, I’m gonna show you a better way to generate test data instead of filling them manually as we’re doing for this create account argument. By generating random data, we will save a lot of time figuring out what values to use, the code will be more concise and easier to understand. And because the data is random, it will help us avoid conflicts between multiple unit tests. This is specially important if we have a column with unique constraint in the database, for example. Alright, let’s create a new folder util And add a new file random.go inside it. The package name is util, same as the folder containing it First we need to write a special function: init() This init() function will be called automatically when the package is first used. In this function, we will set the seed value for the random generator by callling rand.Seed(). Normally the seed value is often set to the current time. As rand.Seed() expect an int64 as input, we should convert the time to unix nano before passing it to the function. This will make sure that every time we run the code, the generated values will be different. If we don’t call rand.Seed, the random generator will behave like it is seeded by 1, so the generated values will be the same for every run. Now we will write a function to generate a random integer This RandomInt() function takes 2 int64 numbers: min and max as input And it returns a random int64 number between min and max The formula is min + rand.Int63n(max - min + 1) Basically the rand.Int63n() function returns a random integer between 0 and n-1 So rand.Int63n(max - min + 1) will return a random integer between 0 and max-min Thus, when we add min to this expression, the final result will be a random integer between min and max. Next, let’s write a function to generate a random string of n characters. For this, we will need to declare an alphabet that contains all supported characters. To be simple, here I just use the 26 lowercase English letters. Now in the RandomString function, We declare a new string builder object sb Get the total number of characters in the alphabet and assign it to k. Then we will use a simple for loop to generate n random characters We use rand.Intn(k) to get a random position from 0 to k-1 And take the corresponding character at that position in the alphabet, assign it to variable c. We call sb.WriteByte() to write that character c to the string builder Finally we just return sb.ToString() to the caller. And the RandomString() function is done. We can now use it to generate a random owner name. Let’s define a new RandomOwner() function for this purpose. And inside, we just return a random string of 6 letters. I think that’s long enough to avoid duplication. Similarly, I’m gonna define another RandomMoney() function to generate a random amount of money. And let’s say It’s gonna be a random integer between 0 and 1000. We need one more function to generate a random currency as well. This RandomCurrency() function will return one of the currencies in this list. Here I just use 3 currencies: EUR, USD and CAD. Similar to what we’ve done to generate a random character from the alphabet, Here we compute the length of the currency list and assign it to n. Then we use rand.Intn() function to generate a random index between 0 and n-1. And return the currency at that index from the list. Alright, Now get back to the account_test.go file In this CreateAccountParams, we can replace this specific owner name with util.RandomOwner(), this balance with util.RandomMoney(), and USD with util.RandomCurrency() And that’s it! Now if we rerun the unit test And refresh TablePlus, We can see a new record id 3 with random values. The first 2 records are fixed values because we had run the test twice before we use random functions. So it works! Now I’m gonna add a new test command to the Makefile so that we can easily run unit tests in the terminal. The command is simple. We just call go test Use -v option to print verbose logs, and -cover option to measure code coverage. As our project is gonna have multiple packages, we use this ./… argument to run unit tests in all of them. Now if we run make test in the terminal, We can see it prints out verbose logs whenever a test is run or finished. It all so reports the code coverage of the unit tests for each package. Now let’s refresh TablePlus to see the new record It’s a completely different value from the previous record. So the random generator is working well. Next I will show you how to write unit tests for the rest of the CRUD operations: Let’s start with the GetAccount function. You know, to test all of other CRUD operations, we always need to create an account first. Note that when writing unit tests, we should make sure that they are independent from each other. Why? Because it would be very hard to maintain if we have hundred of tests that depends on each other. The last thing you ever want is when a simple change in a test affects the result of some other ones. For this reason, each test should create its own account records. To void code duplication, Let’s write a separate function to create a random account. Paste in the codes that we’ve written in the TestCreateAccount function Then for this test, we just need to call createRandomAccount() and pass in the testing.T object. Note that the createRandomAccount() function doesn’t have the Test prefix, so it won’t be run as a unit test. Instead, it should return the created Account record, So that other unit tests can have enough data to perform their own operation. Now with this function in hand, we can write test for the GetAccount function. First we call createRandomAccount() and save the created record to account1. Then we call testQueries.GetAccount() with a background context and the ID of account1. The result is account2 or an error. We check that error should be nil with this require.NoError function. Then we require account2 to be not empty. And all the data fields of account2 should equal to those of account1. We use require.Equal() function to compare them. First the ID, Then the account owner, the balance, and the currency. For the timestamp fields like created_at, beside require.Equal(), you can also use require.WithinDuration() to check that 2 timestamps are different by at most some delta duration. For example, in this case, I choose delta to be 1 second. And that’s it! The unit test for GetAccount() operation is done. Let’s run it. It passed! Now let’s write test for the UpdateAccount() function. The first step is to create a new account1. Then we declare the arguments, which is an UpdateAccountParams object, where ID is the created account’s ID, and balance is a random amount of money. Now we call testQueries.UpdateAccount(), Pass in a background context and the update arguments. Then we require no errors to be returned. The updated account2 object should not be empty. And we compare each individual field of account2 to account1. Almost all of them should be the same, Except for the balance, which should be changed to arg.Balance Alright, let’s run this test. It passed! The TestDeleteAccount() can be easily implemented in the similar fashion First we create a new account1. Then we call testQueries.DeleteAccount(), And pass in the background context as well as the ID of the created account1. Require no errors. Then to make sure that the account is really deleted, We call testQueries.GetAccount() to find it in the database. In this case, the call should return an error. So we use require.Error() here. To be more precise, we use require.EqualError() function to check that the error should be sql.ErrNoRows. And finally check that the account2 object should be empty. Now let’s run the test. It passed! Excellent! The last operation we want to test is ListAccount. It’s a bit different from other functions because it select multiple records. So to test it, we need to create several accounts. Here I just use a simple for loop to create 10 random accounts. Then we declare the list-accounts parameters. Let’s say the limit is 5 And offset is 5, which means skip the first 5 records, and return the next 5. When we run the tests, there will be at least 10 accounts in the database, So with these parameters, we expect to get 5 records. Now we call testQueries.ListAccounts() with a background context and the parameters. Require no errors. And require the length of the returned accounts slice to be 5. We also iterate through the list of the accounts and require each of them to be not empty. And that’s it! Let’s run this test. It passed. Now let’s run all unit tests in this package. All passed. If we look at the account.sql.go file, We can see that all Account CRUD functions are covered. But why the total coverage of this package is only 33.8%? That’s because we haven’t written any tests for the CRUD operations of Entry and Transfer tables. I leave it as an exercise for you to practise. I hope this video is useful for you. Thanks for watching and see you in the next lecture.
Info
Channel: TECH SCHOOL
Views: 11,917
Rating: undefined out of 5
Keywords: golang test, golang unit test, golang test database, golang test db, golang test crud, golang database, golang db, golang random, golang testify, backend course, backend master class, backend tutorial, golang tutorial, coding tutorial, programming tutorial, tech school, tech school guru, techschool, techschoolguru
Id: phHDfOHB2PU
Channel Id: undefined
Length: 20min 5sec (1205 seconds)
Published: Sun Jul 26 2020
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.