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.