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.