Conf42 Golang 2023 - Online

Build a complex app with TDD

Video size:

Abstract

Are you tired of the tutorials out there that try to explain hard concepts by relying only on trivial examples? If you aim to master TDD by building real-world applications, this workshop is for you. We’ll build together a REST API that will make use of HTTP handlers and a database.

Summary

  • Ivan Pesenti, 27, is a software developer working for Solintlab. This session will teach you how to apply test driven development in a real world application. You will learn how to write good, clean and idiomatic go code.
  • Test driven development is an ethereum iterative cycle which involves three parts. In order to be successful with test driven development, it's better to write clean tests. A clean test should follow the acronym for fast, independent, repeatable, self validating, and through test.
  • Code coverage is how much code is covered by tests and the code covered is the production code. There are three different kinds of code coverage, the function, the statement and the branch coverage. Test driven development is not about testing but about designing.
  • The demo is realized with the gene web framework that is the de facto standard when it comes to designing and building a web service. While for the test code we are going to relying on testing package which is part of the Go standard library. Let's switch to vs code and start.
  • All right, we are in vs code. Now let's start building our application about handling the edit route of our to do app application. Create test context only which expect a response writer as the first argument. Then run our first test and see if we got an undefined error.
  • Let's introduce the package SQl mock which assists us in mocking a real database. Let's get rid of these magic numbers validation error by introducing another constant. Third scenario is when we got a database error. Both of the tests are passing. It's time for refactoring.
  • Gorm client is an abstract client upon the underlying socket connection to the database. We need to set up the mock because we need to say to SQL mock how to behave in this particular condition. We also need to assert that the mock expectations were met. Let's fix the 500 internal server error status code.
  • Gogen web framework has a lot of features built in. To do this, we need to define a DTO struct. This is a struct that is meant to transport back and forth the data from the client to the server and the way around. But the test file is not yet fixed to use the new features.
  • All right, now let's move to the fourth scenario. The assertion here is different because in this case we would like to have the 404 status code. What we can do is do a tiny refactoring by using HTTP status not found constant. Now we can improve our working solution.
  • We are going to create a new package, this package, it will be called repo package. This logic will be executed only if the GormDB client will return an error. At the end we need to have something that actually run our application. Let's wire them up in a main file.
  • All right, now we can run some tests with Postman. One request for each scenario. In the code we can mock the database error. If you'd like, you can add me on each of them.

Transcript

This transcript was autogenerated. To make changes, submit a PR.
Hi and welcome to my session about test driven development. Went with Golang. First of all, I would like to thanks the organization for giving me this amazing opportunity to be able to speak in front of you. First, let me introduce myself who I am, what I do I'm Ivan Pesenti, 27 years old and I worked as a software developer since 2014. After my school years I work remotely for a global company which is Solintlab. I'm mostly experienced in server side programming. Part of my daily tech stack involve Go postgres, SQL, AWS, platform Docker, docker compose and some other minor technologies. I strongly believe in values such as kindness, sharing and determination. In my spare time I'm involved within a lot of activities such as speaking, training, technical writing and also mentoring. I've just finished my first course about Golang and tests. I love also anime, tattoo and football in what this session will be different from others that you may joined before. This session won't rely on trivial examples, okay, such as most of the tutorial out there. But in this session you will make your handlers DRT with a real world application. So you will be able to apply test driven development in a real world application. Not only a trivial Qatar such as a simple program to sum up two numbers, or a simple Qatar such as Fitzbuds or Fibonacci or all of that stuff. Here we are going to build a real application. Plus you will be aware of the theoretical introduction about tests and code coverage, and a good part of this demo will be about Golang which will be our chosen programming languages. So you will learn how to write good, clean and idiomatic go code. Also, I'm going to share some of my experience with the tests. How do I mock a database? And how do I mock the service that made up my application? First, let's start with a small, boring introduction about test driven development that for sure most of you will already know. The test driven development is an ethereum iterative cycle which involves three parts is known also as red green refactor phases. We all start from the red phase where we have to write a failing test. Then we have to move as fast as we can out of this and enter the green area in which we have to fix our broken tests. Lastly, we'll move to the refactor phase in which we can make all of the improvements to our code that is certified by tests that are running successfully. The mantra of test driven development is made up of three simple rules. We have to follow this rule. As a rule of thumb, we have to be self disciplinate in order to be successful with test driven development. So let's deep dive into them. The first one is that we are not allowed to write any production code unless it is to make a failing unit test pass. That means that first we have to write tests and then we can write production code, not the opposite. Second rule is we are not allowed to write more of a unit test than is sufficient to fail, and compilation failures are failures. This is pretty self explanatory. We can write only one unit test failing, okay? And the first ideal test that we have to write is a compilation failure because we didn't have write the production code yet. The final rule is that we are not allowed to write any more production code than it's sufficient to pass the one failing unit test. So we have to write only the list code to make a building test running correctly. Okay. No fancy other thing, only the working code, the minimum working code. I will say a basic building blocks of unit of test driven development is test, but only clean test. Because in order to be successful with test driven development, it's better to write clean tests. That is always a good thing. What are the aspect and key characteristics of a clean test? First, it must obey to the triple h paradigm, which is the acronym for arrange, act and assert. These three parts should be distinct in our test code. In our test function, we don't have to mix and match them. Second, our test must not contain code smells that are not errors, but something that smell. For example, a function that has tons of parameters. Okay, an easy fix for this is to build a struct in order to gator and collect all of them into a structured format. Third point is the test must not contain magic numbers. With magic numbers, I also refer to magic strings, which are literals spread into our code that can bring magic into our code. Okay? Because if they don't have any meaning, they are just literals. The easy fix for this is to extrapolate these values and create constants that will be referred to throughout the rest of our program. A test must have a meaningful name. From the name we can assume, which are its input and which are its output. And the last one, taken directly from the test driven development manifesto, is that a clean test should follow the first principle, which is the acronym for fast, independent, repeatable, self validating, and through test. Okay, before going ahead, let's spend a couple of minutes talking about code coverage, which is one of the most important aspect when it comes to test driven development. Code coverage, as most of you surely knows, is how much code is covered by tests and the code covered is the production code. I mean, there are three different kinds of code coverage, the function, the statement and the branch coverage. During this demo, we are only interested at statement coverage and a common misconception is that you have to reach and aim 100% of code coverage in your code. However, this is not mandatory at all. What is important is to cover the most critical business logic part within our solution, within our program. Not everything. If we follow the test driven development approach, we'll always have this 100% code coverage because first we have to write test and then we can write production code. Let's barst another mice. This mice is test driven development is not about testing but about designing, okay? In fact, test is a very good nice to have, but it's not the main purpose of test driven development, which is to help you design your solution with the tests. Okay? So you have to hear for the feedback provided by your tests and make the needed adjustment in the solution, in the design of the solution. I know this is a common reaction. Okay, in order to be more practical in this demo, we are going to rely on the to do application. Okay, so we are going to build the edit handlers function, which is by far the most difficult one, usually because it involves a lot of different scenarios. Okay, let's focus on what we get in. We'll accept an HTTP request, which method will be the put method, which is reserved for the edit operation. Okay, the route is to DOS colonid, in which colon id is a route parameter that we expect in perequest p load will looks like something similar to the snippet of code in the right section, which has three keys. Description of type string, which is the to do description is completed. It's a boolean value that determine if the to do is completed or not, and the due date is a string value that represents the deadline for our to do to be completed. One of the most important part about test driven development is to identify the possible outcome that our code can take. Okay? So it's very important to define the possible scenarios. If you don't do this, you cannot be successfully with test driven development. I did this and I can summarize the five possible scenarios in this list. The first scenario is when we pass in an invalid id. Remember, the id is passed in as a route parameter. For example, we are going to pass in ABC instead of an integer number. So in this case we'll expect back a 400 bad request status code. The second scenario is when the request payload is not valid. Okay, for example, the request payload has the description set to string empty. That's not possible. So we would like to have back a 400 bad request status code. The third scenario is when there are some unknown database error, okay, such as connectivity issue or other stuff like that. In this case we would like to have back a 500 internal server error. The fourth scenario is when the to do is not found in our database. And in this case we would like to have back a 404 status not found error. The last one is the positive path, okay, when everything goes fine. And in this case we would like to have the 202 status accepted status code without an empty response payload. All right, the ingredients of our session, we can divide them into two categories. The first is related to production code. The demo is realized with the gene web framework that is the de facto standard when it comes to designing and building a web service. Recently the other competitor, which is GorillaMax, was publicly archived, so it's no longer maintained. So this event forced us to switch to a still maintained solution and we started to use gin which has more or less 70,000 of stars in GitHub, while Gorilla Max, it has only 20,000 of stars. So I think it will still be a valid option to follow. Second is gorm, which is the package for using an ORM with go. If you are going to build a real world application, chances are that you are going to rely on an Orm. Not always, but probably so that's why I introduced it in this demo, so you can make out the most of it. The third one is the postgres driver, which is the driver that are actually used to talk to our database. While for the test code we are going to relying on testing package which is part of the Go standard library. Conversely to other programming languages, with go we don't have to bring in any frameworks in order to be able to write tests. So that's awesome in my opinion. The second package is Go SQL mock, which is the most used package for mocking an actual database. Thanks to it we can postpone the decision about the database engine that we are going to use to the latest part of the project and program against an abstract layer, let's say. Okay, which decouple the actual underlying database. The third package is the subpackage assert provided by the testify package. Thanks to it, it's easier to do assertion in our code. Last one is the HTTP test sub package which assists us in building HTTP response and HTTP request without worrying too much about the protocol stuff. Okay, we already talked a lot. Now if you are ready like me. Let's switch to vs code and start. Make your hands dirty. All right, we are in vs code. Now let's start building our application about handling the edit route of our to do app application. First of all, I've opened a folder in my vs code. The folder is called to do app and the first thing that we have to do is to initialize a module. So we run go mode init followed by the name of the module. In our example is to do app. There we go. Then we have to define the handlers, right? So what is meant to deal the HTTP request and reply with an HTTP response? First thing to do is to create a folder called handlers. Within that folder we are going to create our first file called handlers underscore test go. We define the package name that is handlers and there we go. Now before moving ahead, let's recap in a list this scenario that we have to manage. The first one is id, not integer. The second one is validation error. The third one is db error. The fourth is to do not found, and the last one is the FP path. Well now let's start by defining our first test. The test name must must be prefixed with the right test, then followed by the name of the function that we are willing to test. In our case is update to do in the signature we have to involve the t parameter which is of type pointer to t provided in the testing package. If we save it automatically import in the import section the testing package. Now let's start by setting the mode of gene. In our case is gene test mode. Then we define an HTTP recorder for our test which is provided by the package HTTP test new recorder. And then we need a gene context for our test. Create test context only which expect a response writer as the first argument, and then an engine of gene. In this case we can use gene default if we save it automatically imports the package. Here, however, we have to actually download it. We can do in two ways. The first one is by issuing a goget followed by the path of the module of the package. The second one is by running the command go mode tidb. So we enter our folder and then we issue the command go mode, tidy it, automatically download the package and now the error goes away. The only error that we still have is that c is declared but not used. So let's moving ahead and use c in order to build our call. So the first thing that we have to do is to set the request on this context and we are going to set it of type HTTP request, and we initialize the header field by using the built in make function. Then we set the parameter for our request. In this case the params is a slice which contains all of the parameter for our handlers. But now we would like to append another one. So with the built in append function we invoke it in order to append another param which has a key called id and a value abc. Because this test is about Id not valid, so id not integer. Let's add this suffix to this test. All right, so now we prepared our request. The next step is to actually invoke our handler. We issue update to do and we provide the gene context. Then we have to do the assertion. So if code is not equal to 400, then we let the test fail with a format message. In this case expected percentage d comma got percentage d. So the code expected was 400, but we got the actual code. All right, now we can run our first test and look if we got an undefined error, a compilation error, because we didn't write this function yet. In order to run our test, we need to switch in the handlers folder, then issue the command go test v, which means verbose because we are interested in a verbose output cover in order to also include the code coverage. If we run it, we get back an undefined update to do so. Now let's define this update function. First of all, we are going to create another file called handlers go. I'm going to put the production code on the right side while the test code will be on the left side because I'm used with this setup package Android because the production code will belong to the same package of the test, at least in this demo. And then we are going to define the update to do function which expect a pointer of gene context. Now if we rerun the tests, we get back this error. Now the compiler, the compile error error goes away. Now we got 400 bad request error instead of 200. Okay, that's why if we didn't specify anything in the body of the function, this is the default behavior. So let's fix this in order to move to the green area. C writer write either 400 and then we force the writing of the eater. So c dot writer and now in order to write and force the eater to be written. If we rerun the test now we got a successful result. Now we could enter in the refactor phase. So first of all, let's get rid of these magic numbers and instead using the constant provided by the HTTP package, if we rerun it. We get still the positive result, but we can do better. We can use the assert package provided by the testify package. So with assert we can invoke equal and providing the test which is the one provided in the signature, then followed by the expected, in our case HTTP bed request, and then the actual code that we got. And here we go. Now let's install this new package by running the go mode tidy. All right, there is a warning, so let's rerun this gomode tidy and let the warning goes away. Then we reenter the handlers folder so we are good to go. The next step here is to also assert for the body, okay, because now we assert only the status code that is 400 by the request. However, we also would like also to be sure that the body contains this string. So now let's duplicate this test and let's append this suffix that is body. Let's get rid of this assertion because the scenario is the same. What change here is that we need to assert something else. So we still make use of the assert package, but this time we invoke the function contains, which expect t as the first parameter, then the string to search in, which is double view body string, and then the string to look for. So id not integer. Now if we rerun the test, we are in the red area, because our second test we return this error that is string empty does not contain id not integer. That's why we didn't manage and or handlers where spots p load. So let's do this. In order to fix this, we have to invoke c string, which expect the HTTP status code to return as the first argument, and the second argument, a string to be written on. The response in our case is id not integer. Then we can safely remove the write either now, because now we don't have to force the write of the eaters because it's already done by the string. Let's rerun the test and now we are back in the positive area. All right, let's refactor this a little bit, because now we are allowed to do refactoring. Let's introduce a constant in order to get rid of the Id not integer literals, which is a magic number, as you might remember from our slide. So id not integer error equal to id not integer. And then we switch the usage of the literals to this variable constant. I mean, if we rerun the test, everything is still fine. Now we have two tests that are challenging the same scenario. We can merge these two into one, keep in mind that it's completely safe to delete test because test as the normal code can be adjusted, deleted or improved. So it's completely fine as long as we keep testing every scenario in our code. So let's move this assert in the first test and then let's get rid of this second test. If we save and run the test, we still have the 100% code coverage because now we are only dealing with one scenario. Now let's write the second test for the second scenario, which is validation error in our case. So we are going to duplicate this just for the sake of time. And then we renamed this to validation. Then the first thing to do is to fix this param. Now we are good. So we passed in a right int id value such as one. All right, then we have to pass also the payload because now we are dealing with the payload because we are validating the incoming payload of the HTTP request. So we need a way to pass it to our handlers. So first of all, let's set the method which is put on the request. Then let's set one either. So c request either and set set is a method that expect two values, both of type string. The first one is the key and the second one is the value. So the key will be content type and the second one will be application Json. Then we have to set also the body, and the body is of type I o knob closer just to wrap our writer into a reader which has also the close method, so it's a no utility method. This one I will say inside of him we have to invoke the strings new reader function which expect in a string. The string is provided within backticks in order to not have to escape every single double quotes within it. So a pair of curly braces and then let's start by defining our p load. In this case we will leave the description empty in order to understand this validation scenario is complex equal to true and due date equal to 2023 50 five good. So now we have defined our payload, which is invalid because the description should be populated because we won't allow a to do without a description. All right, in the assertion part we can leave this assertion because it's still 400, the status quote that we are expecting here. However, the body should contain another error message, which is validation error. So let's rerun the test and check if we are in the red area or not. If we run this test we get back an id not integer, does not contain validation error. So we have to move in green. In the green area and we have to do some adjustment in this code. The adjustment that we have to do is to invoke the function etoy which is responsible for converting a string into an integer value and provided an error. If this conversion is not possible of the param that we got in, let's capture this result into a variable called error and for now blank identifier because we don't need the id right now. Okay, so let's not grab the id just for the sake of it. If we will need the id, we will grab the id. Now we only interested at understanding if the id is an integer or not. If error is not equal to nil, then we return this string and we immediately exit from the function. If this is not the case, we are going to return the validation error message because now our handler is only handling two scenario the id, not integer and validation error. So we have only 50% of probability if we rerun the test. Now we are in 100% of code coverage and both of the tests are passing. Now it's time for refactoring. Let's get rid of these magic numbers validation error by introducing another constant. This constant is named validation error. If I save my Gofund tool will format the constant in this way like the import section. Then we have to switch the magic number to the validation error constant and also do the same in the testament. All right, the tests are still passing. Now let's introduce the logic about the database because our third scenario is when we got a database error, something unexpected such as a connectivity issue or a query that takes too much to be executed or whatever, something that can be unpredictable to us. So let's duplicate again this test and now we are going to call it db error. In order to be sure, let's fix the description. Now we got invalid descriptions as Lorem ipsum. All right, now let's introduce the package SQL mock which assists us in mocking a real database. So we are going to call the package sQl mock dot new, the function new, and we capture the results into three variable. The first one is db, the second one is mock, and the third one will be the error. But for the sake of demo we are not going to handle it and we leave the blank identifier. All right, we have to import this SQl mock. So let's navigate up of one level and run the go mode tidy command. This way we successfully imported our mock go SQl mock package. In the new function we can provide the criteria based on which it has to match the query because now we are defining the behavior of our mock. So which query we are going to perform, which commands we are going to perform, with which parameter and which result we have to expect. Okay, in order to match this query or SQL statement in general, we have various options. We can match them exactly, which will be our option for today's session, or we can match them with a regular expression, for example. So we have to specify the behavior in this case is SQL mock query matcher option and within the brackets we are going to write SQl mock query matcher equal. Then we have to defer a close request to the DB. All right, and then we have to define our gorm dB connection because to our handler we are going to pass a gorm client, not a row SQl stuff. So we need a way to retrieve it. The first thing is to define a new variable called connection which represents the underlying connection to our fake database. So we are going to invoke db.com and we provide a context. In this case we have the gene context that fits well here. Then we defer a close invocation and now we are ready to instantiate a gorm client. The gorm client is an abstract client upon the underlying socket connection to the database. In this case we can define the Gormdb variable and another blank identifier and invoke a method of the Gorma package. This method is called open. The first thing to do is to import our package always with the Gomode tidy we are going to download it from GitHub. All right then here we expect a dialector and we use the driver of postgres and we invoke the function new. Again we have to install this brighter. So let's switch back to our terminal and rerun the go mode tidy. Thanks to this command we have downloaded the postgres driver. You can see here the latest two packages installed gorm and postgres driver. Now within it we are going to invoke a postgres config struct and we set the con field to the con that we declared up here. Here we go. Now we can pass this to our request context. In order to do this we need to invoke the set method on our gene context. The first parameter is a key which is a DB and the second one is the value that is a GormDB. All right, now we have to set up the mock because we need to say to SQL mock how to behave in this particular condition. So let's do this. On mock we are going to issue an expect query method which expect a string which represents the query that we are going to perform on the database. In this case, we use a backtick in order to not escape the double quote within it. So select star from our table that is called to dos where todos id is equal to dollar one, which is a placeholder for the first argument. The arguments will be replaced in a positional way. Then order by todos id, limit one to close the query. Then we invoke also the ARG with the with arcs method to specify that we are calling this query with this argument and it will return an error. To create an error we use the package font together with the error f function and we provide the string db error. All right, now let's switch to the assertion part. In the assertion part we are going to get rid of this and leave only an assertion on the status code that should be equal to 500 in this case because it's an unexpected error. However, what we have to do is also assert that the mock were actually used as we expect to do this, we are going to invoke mock expectations were met. It will return an error if some of the expectation were not met. If we got an error, we let the test fail with this string. Not all expectations were met, followed by percentage v and the error. Now we should be able to run the test and see if we are in the red area. And there we go. We got two errors, the expected status code that is different and the expectations because we didn't run this query on our db. That's why we didn't retrieve the GorMDB client in our Android. So let's fix them. Let's start by fixing the 500 internal server error status code. All right, first of all, we have to involve a third scenario. So we are going to have another if checking for the validation scenario. To do this, we need to define a DTO struct. A DTO struct is a struct that is meant to transport back and forth the data from the client to the server and the way around. So type to do DTO struct it will have three fields description of type string with this gzone annotation. Then binding with required annotation is completed. A boolean value with this gzone annotation and the binding is required. Due date of type string JSOn due date binding required one of the beauty of Gogen web framework is that it has a lot of features built in. This one is a great example. We can do the validation in this way. Just add an annotation here and then invoke a method over the gene context. Let's do this. First of all we are going to define a variable called to do of type to do DTO. Then we run the method should bind over the gene context and it automatically understand that it has to try to unmarshall the body of the request onto our struct and we have to pass the pointer to our variable. Then we capture the error and if the error is not nil we are going to return this. Otherwise we are going to set the HTTP internal server error status code c writer write either 500 and then we force to write the eater now. Now we can run the test to see if the first assertion goes away. If we run go test b cover. Now the complaint about the status code has gone away. Now we can try to fix the second assertion which is the assertion about the query. In order to do this, we have to define a model. The model is the actual model that will be mapped as a table in our database because the Orm its purpose is to map object that are code stuff such as struct over relation that are table in the database. So let's define the to do struct. The to do struct will have an id of type unsigned integer, then a description of type string, then the is completed field of type bool and then the due date of type string. All right here, first of all we have to get the db and to get the db we are going to read this value from our context. So c dot mustget which is a method that expecting a DB key in our case is db. And then we typecast it because it's a pointer to a GormDB. Then we have our db. Now let's actually invoke our query. First of all we need the id here because now we will use the id so DB model and then we pass in to do struct the model method is to indicate on which model we are going to perform our action. Then we will invoke the first method, the destination. We don't care about it for now, so we leave an empty to do and then we provide the id parameter because we are going to search by the primary key. That's all. Let's see if we manage also the second assertion. And there we go. We got printed on our terminal the DB error which is actually returned by our fake database. So now we are talking over a database to a database, but we didn't have it yet because in the EDD cycle we first needed to make sure to design the behavior correctly. Then we can choose on which database actually talk to. Now we can do some improvements. First of all, let's create another package which is called model. In our terminal go up of one level and create a folder called models. Then in our models folder place a file called models go which belong to the package models. Then we cut the constant from here and we put them here. Now visual studio code start to complain because id, not integer is undefined. All right, so let's import our new created package to do app models. In the import section to do app is the name of the module, models is the name of the package. Then we prepend the package name to the constant in order to be successfully recognized. Here we go. The file handlers is okay. However the test file is not fixed. We have to do the same here. So let's start by importing the package and then switch the variable to use the new package it. Let's save the file and rerun our test. The test is still successfully. Another thing that we should do is to cut away from here this to do definition because it's a model. It's not something related to the HTTP protocol, so its house is not here. It's within the model's file. So let's cut this from here and paste it here we save the file and the Android go file is complaining because it doesn't recognize to do let's prepend also this to do with the model's package named and rerun the test. The last two small things that we have to do is define a constant called tb key. This constant will be used to set the key instead of these magic numbers. So let's switch it and also in the test code. And then let's switch also the HTTP status code to HTTP internal server error. Let's rerun the test and check if everything is still okay. And that's the case. Now what we have to do is to assert also for the body of it. So let's duplicate this test. The scenario is the same, so we don't have any reason for change anything here. However, the assertion is different. Now we would like to assert over the body. So we're going to use the contains function with the body string method and we expect a dB error issued. We have to add a suffix here which is body. All right, then we can see if we are back in the red area, and indeed we are in the red area because string empty does not contain db error. Let's fix this in our production code. Let's replace these two lines here. We are going to invoke string which expect status code as the first parameter. And then we provide the string TBR and we get rid of the line to force if we issue the same command. Now we are back in the green area, but let's refactor this magic number by introducing another constant here. And this constant is db error. Its label is dB error. Here we are going to switch to this new constant which is model dB error and also in our test code. Let's rerun the test and everything is still good as before. Let's cut this assertion and put this assertion within the first dB error test method because we are testing the same scenario here. So it's completely fine to have a single test for it with two assertion plus another for the mock. If we rerun the test now, everything is still good and we have 100% of code coverage. All right, now let's move to the fourth scenario. That is the to do not found situation. So we are going to duplicate the latest file, the latest test, I mean, and we are going to rename it with not found. The assertion here is different because in this case we would like to have the 404 status code. So let's switch the assertion. First we'll like the 404 status code. However, now the mock is behaving in a little different way. It's not going to return a generic error, but is returning a specific error which is provided by the gorm package and the error is called error record not found. All right, everything else is still the same except the mock behavior because if the mock we reply with this specific error, we would like to reply to our client with the 404 not found status code. All right, now let's rerun the test and see if we are in the red area. And there we go. Because 404 is not equal to 500, so we are allowed to write some production code. Now first of all, let's grab the result of this transaction in a variable called error. In this way we can inspect this error if errors is, which is a function introduced by the GO 113 version that can assert if an error is of a specific type and it return a boolean representing this match. The second parameter is the actual error, that is error record not found. If so, we are going to write the 404 not found status code and we force it to be written and we return. Otherwise, the old logic still relying if we rerun the test now we have in the red, in the green area because we got a positive outcome. What we can do is do a tiny refactoring by using HTTP status not found constant provided by the HTTP package and everything is still good. Now let's do something more. On this scenario we would like also to test the body. So let's duplicate this test and then let's append the body suffix. All right, and let's change this assertion because now we would like to write an assertion for the body. So we are going to use the contains function of the assert package and in the body string we expect this particular message to do not found because in this scenario the to do is not found in our database. For example, we have only ten to dos and we request for the to do with the id number 20. So in this scenario we would like to return and inform the client that the to do is unknown on our system. All right, let's rerun the test and we are in the red area because string empty does not contain to do not found. So we are going to change it a little bit with c string and then to do not found we no longer need the force of the eder and then we can rerun our test and our tests are successfully. Now let's introduce another constant to get rid of this magic number. So let's duplicate the latest variable constant defined here and add a not found there with the label to do not found here we switch it to the variable and we switch also in our test, if we rerun our tests, everything is okay. Now we can merge these two files into one's, rerun our tests to be sure that we still are with 100% of code coverage. And there we go is exactly in this way. Now we can do another refactoring because it's very unlikely that we are going to reply with a string, a plain text response payload. Usually when designing a rest API, it's pretty common to reply with the JSON format, maybe a common format for all of the error. So let's define this because now we can improve our working solution. So I'm going to switch to models. I'm going to define a new struct called to do error. Sorry for the naming, but it's only a session so you can choose for sure better names for your real world project. And we are going to add two fields, one code of type string with the JSON annotation code. This code will contain only a short description of the error and it will be represented by our case by our constants that map one to one with our scenario. And then we will have a message with a more verbose explanation of what was wrong. So from the message you can highlight and spot the error. Hopefully it here we go. Now instead of returning a string, let's return a G's zone, the JSON expect as the string, the status code as the first parameter and then an object to be serialized on the response. In our case is models to do error and we are going to specify the code and also the message because here we have the message, we we replicate the same behavior also for the validation error, models to do error and also the message also for the latest two which are JSON. All right, now the message with the error. Here we go. So we refactor everything with the usage of the JSOn format. So we are going to reply instead of textplane type of response with the application JSON. Now let's rerun the test and everything is still okay. So now we are ready to face the last scenario which is the EPIPH scenario, the scenario in which everything goes well. No id, poorly formatted, no invalid request type. Vdb is behaving correctly, no unexpected error and the to do is present is existent within our code base. So everything goes okay and we have to reply with the 202 accepted status code. So let's rename it to EPIPH and then let's refactor a little bit our code first of all we have to define a new variable called rows. These rows are the rows returned by our query because now we actually found our record. So sql mock new rows. It will expect a slice of string which are the column to return. We set up the description, sorry, the id, the description is completed and the due date. Then we invoke the method row which expect the actual value to return. Here we are going to return one as id sample to do as description false as is completed and 2023 215 as the date. Then in our expect query, instead of returning an error, we actually return the rows and the rows are the one that we just defined. Furthermore, here we have also to forecast a command. So we have to issue an expect begin and then we expect also a commit. So we wrap the command between a begin and the commit. If we expect something wrong instead of commit we are going to expect the rollback inside. We expect an exec provided within backtick and the command is update to DOS set description equal to dollar one, then is completed equal to dollar two, then due date equal to dollar free. Where to dos Id equal to dollar four. Keep in mind that we have to add this condition, otherwise we end up in editing or updating all the records of the table. Another quick note that I would like to mention. If we specify the query matcher equal. You have also to keep attention to the spaces in our query because the query should be exactly the same. Now we have to specify the arguments, all right, and the arguments can be found here in the payload that we accept from our client. So the description is lorem ipsum. The is completed is true. The due date is 2023 50 five and the id is one. This will return a result and this result with zero as last insert id and rows affected one. So we are good to go. The assertion that we have to change is we no longer assert on the body because in this scenario we won't have a body. We only have a status code and then we put 202 as the expected status code. Here we go. We can run our handlers now we can run our test and we got an error because we are trying to access a nil value. If we look closer to the stack trace we can see that the line involved is the 39. If you can see from this line it says that is the 39. That's why here we are trying to access this error. All right, but this error is nil because now the mock is not returning any error. All right, so let's fix this by avoiding to invoke this. In this case, if we rerun the test we can see that there are this actual error. The error is we expect 202 but we got 500 and also there are unmatched expectation because we expect a begin, we expect a command and we didn't write the command yet, right? So let's fix this. The first thing that we have to do is to declare a variable of type to do and this to do is the one that will be updated on the database. So we are going to instantiate a to do to save variable of type models to do and we set only the column that we are going to change description to do DTO description when is completed from the to do DTO is completed, then the due date from the to do DTO due date. Keep in mind that this logic is the mapping logic between the DTO and our model. In an even more complex application it can be extrapolated into a service and we can write tests against this service. However, for the sake of the time we are not going to deal with such complex scenario. Then with this to do we are going to invoke the updates, the updates method and we pass a pointer to this variable to do to save. Then we check for the error. So we prepend an if and we check for the error. We move this logic within the dave. So this logic will be executed only if the GormDB client will return an error, otherwise we can skip it at all. Now in this case we are sure that the error is always populated so it's safe to invoke error error and return from it. If no error is encountered, we are going to return our header and we have to force it's if we run go test v cover. Now everything is okay and we meet all of the expectation of the mock. Let's do a small refactor. All right, here we go. Now, the last part that I would like to mention is that usually this db logic is not placed here because this method is becoming too much bloated. So let's define a new package, this package, it will be called repo package. We are going to create a repo, a repo file within the repo package. All right, let's define the package which is a repo, and also define the function that is updated. To do it will expect adb as Gorm db, an Id as integer, and also a to do to save of type models to do and it will return an error. Let's put return nil in order to satisfy his complaint and then let's copy this logic over there's all right here we had a couple of refactoring to do. First of all, let's assign the error. Then let's add the eros package and also add the net HTTP. Then we have to return an error model, only the model. All right, and the same applies here. Now go is complaining about the fact that to do error is not a valid error. That's why in order to be recognized as an error, it has to implement an interface. So we are going to implement this interface. The interface method is called error without any arguments and it will return a string. Here we go. Now we satisfied, we satisfied his complaint. However, we can do even better because now we typecast it. We typecast the error here and then we have to typecast also the error here in order to understand which status code actually returned to the client. A smart way of achieving this is to add another field here, maybe a field called status code of type int. And we add a JSOn annotation dash, which means in your it. So when you are about to marshall it, you can ignore this field. So in this way we can assert and use the status code and set the HTTP response status code by reading that value, and that value will not be copied again within the response payload. So let's do this HTTP not found and then also for this HTTP internal it. Now let's switch the code here because now what we have to do here is to define a to do error which is typecasted from the models. To do error in this way, to do error is our error and we can use its value and its fields such as the status code. And we actually are going to do this. The gzone status code is the status code within the to do error variable, and then we simply reference it this way if we rerun the tests. Oh my God, let me redo this. All right, let me redo. All right, here we could try to run our test. However, we need to invoke the new function created. So repo update to do and we actually pass the db, the id and the to do to save it when if we run it, everything should be okay. Now we have successfully designed our handler, our models, our repo. Let's wire them up in a main file because at the end we need to have something that actually run our application. So let's define a main method. So here in the workspace root we are going to create a package main with the funk main. But before running this we have to run a postgres, a docker command in order to spin up a postgres instance. So the command is docker run d for detached mode, p for the port mapping, and we map this port from my host machine to the container port. Then we set an environment variable called postgres password equal to postgres and then the image to base our container. In this case it successfully spin up a docker container. Now we are ready to connect. First of all, let's define a DSN which involves host equal to localhost, then port equal to 54322, a user which is postgres password which is postgres db name which is still postgres, and the SSL mode equal to disable. This is for sure for the sake of demo. I'm not expecting that any of you actually pass a connection string or DSN in this way, but it's completely fine for local development. Then we are going to instantiate a gormdb and the error by invoking the gorm open function which expect in a dialector which is provided by the postgres open with DSN. If the error is not nil, we immediately stop the application because the connectivity is not working properly. Then we have to run the auto migration in order to auto create the tables. In this case, if we don't have the table, the table will be created if we have other column. This column will be added and so on and so forth. It will keep automatically synchronized our structure to our tables. Then we add a start, let's say an initial to do just for the sake of demo models to do and we specify the id equal to one, the description equal to sample to do where is completed equal to false, and the due date equal to 2023 215. Then we have to take care of gain. We have to set up the mode which is gin debug mode. And then we have to create a router by invoking the default function. Then we instrument the router to reply to the put action at this route and then we invoke Android update to do error run to run our application. The last thing that we missed is to actually provide the GormDB. In this case we are going to use a middleware for injecting the Gormdb in each HTTP request session that we are going to manage. The first thing when you are about to inject some code in a middleware is to remember to call the next middleware. So I suggest you to do it first and then leave this invocation at the bottom of this code snippet here we are going to invoke set as we did in our test and we provide Android db key which is the constant we defined in the Android package. And then we pass the Gormdb as a best practice. Keep in mind to always use the timeout context in order to cancel and avoid to do unnecessary operation on IO resources such as a database. So let's do this. Let's define a new concepts called timeout context and a cancel function. We invoke the context with timeout function and we passed to it the request context together with the expiration time. For our demo we can do 5 seconds. Keep in mind to defer the invocation of the cancel function. Then we are going to use the with context in order to give a context timeout to our GormdB. And we passed the timeout context. All right, now we can run some tests with Postman. So in order to run our application, let's switch back to the main and issue a go run. As you can see here, the server is starting and is waiting for incoming request to be managed. Here we go. We are in postman now and I prepared the scenario. One request for each scenario. The first one is the epipath. So let's run it and we got a 202 status accepted. The second one is with a wrong integer ABC. So if we still run it, we have a bad request. The table not found scenario is not easy to test because we had to actually change the code and maybe we can do it later. The validation error instead doesn't have a description set and we have the 400 build request. The last one is the to do not found which actually reply 404 status not found. Now we can try to test this scenario with a small tweaks, a small gotcha that I'm going to show you in the code. All right, in the code we can mock the database error. In this way you have to go to the repo, define a variable called test of type string and then issue a sample command, a command that emulate some intensive workload on the database. In our case it will be a simple weight. So in the row let's add select PG underscore sleep and we told him to sleep for 10 seconds. In this way we are going to wait for the connection for 10 seconds and this should return a concepts exceeded error. Then we invoke the scan in order to actually materialize the query to the database and we passed in the point the address of the test variable. All right, now we should be able to rerun our application again. In the postman we can try to issue okay, let me redo this again. In postman we can test for this newly created scenario. Now if we run any of our query it will be post for 10 seconds and it should cause an error 500 error. So let's invoke it. It hangs for a while and then we got back this message. DB error and concepts deadline exceeded 500 so we would able to test also this scenario. All right, this finish my session about test driven development with go on the screen you can see my social. If you'd like, you can add me on each of them. I will be more than happy. Thanks to all for their attention.
...

Ivan Pesenti

Ninja, Speaker & Trainer @ Sorint Lab

Ivan Pesenti's LinkedIn account Ivan Pesenti's twitter account



Awesome tech events for

Priority access to all content

Video hallway track

Community chat

Exclusive promotions and giveaways