- Alright, we're about to start learning
how to write unit tests in Go.
I love that unit tests are integrated
into the Go programming language and into the tooling.
And it's really a minimal solution,
which is all you need, right?
Minimize, reduce, and simplify.
Now, there are third-party packages out there.
GoConvey is probably my favorite,
but I really want to avoid using third-party stuff,
just because it adds extra dependencies
and things to your code.
So normally I'm not really a fan
of bringing your third-party package in
to provide some maybe stronger testing support,
but you don't really need it.
I'm gonna show you how it all works right now.
But, Kevin Henney said this and I really love it.
"A unit test is a test of behavior whose success or failure
"is wholly determined by the correctness of the test
"and the correctness of the unit under test."
So, one thing we've really gotta validate and make sure
is that our tests are adding value,
and also that we're not over unit testing
or under unit testing, this stuff's complicated,
but we're gonna go over really the mechanics today.
I'm gonna show you how I write unit tests,
which isn't necessarily how you need to write unit tests,
but I just want a team to be consistent
because there's nothing more frustrating,
when everybody's kind of doing things differently,
and because you just have a minimal solution here,
it's quite possible for that to happen.
Alright, so we're gonna go through a basic unit test here.
I'm gonna be using VSCode to show all this stuff off,
and we're gonna start here.
Now, there is a front-end called Go,
and we've been using go build throughout the class.
Today we're gonna start using go test quite a bit.
We're also gonna be using go tool p prof,
go tool trace, we're gonna start using more
of the go tooling outside of just the compiler.
And if you want to write tests, then your files
have to be in an underscore test file.
Remember Go is about convention over configuration,
and we use this underscore test convention here
to denote files that are gonna be really isolated
just to the testing tool, which means that these files
aren't going to be built and compiled
into your production level binaries.
Alright, but go test will run the compiler.
We'll see some of that today.
So, I've got my underscore test.
Now, once you create an underscore test file,
ideally that's going to be in the same package
as your source code for your production level product
whether it's that API or the binary,
whatever you're building, and you're gonna have to either
put the test inside the same package name,
or a lot of times what we do, and I tend
to do this more than not, is use
the same underscore test convention for the package name.
So why would you do one over the other?
If you want to be able to write tests
that hit the unexported API, you're gonna do this.
You're gonna have just the same package name
that the rest of the source code is in.
If you want to write unit tests that are more tailored
to how your users are gonna be using your API,
which is what I really believe in,
then you're gonna want to do this.
Go allows this files to kind of exist
in their own package, even though
they're in the same folder, and then,
you have to import your package
like anyone else does and work with the API.
Now, what happens to me a lot of times is
I'm writing a new package let's say, right,
and I'm starting with the unexported API quite a bit,
so I might start writing unit tests with this
because I want to start testing the unexported API,
but eventually when I get to the exported API
that's using the unexported API,
I could start ripping out the unexported tests
and just keep the exported tests,
and then eventually end up here.
I don't get hung up writing tests
that I might throw away later on
because I want to make sure as I'm coding
that my code is working all the way through.
I don't consider that a waste of time.
I actually consider it a way of being more productive.
So, during the course of my development,
I probably have a lot more unexported tests
that eventually get thrown out
because the exported tests then give me
the same code coverage that I need.
You gotta find your workflow.
That's my workflow right there,
and I always want to make sure
that those unexported APIs are very very testable.
We want to make sure the exported APIs are very very usable.
So, it's a really interesting balance.
These are my philosophies.
So let's just start with this very basic unit test in Go.
I've got the underscore test file.
I got package example, which is the name of the folder,
and I'm gonna show you again what I do for unit testing.
You don't have to do all of this.
I'm a little crazy, I like the verbosity of tests
because when they run in the CI,
I'd like to have a lot of information.
I'm really about not hiding information, right?
So I've defined these two constants,
which are these unicode characters for x and check.
You'll see how I use those, and the big thing here
is having a function named test.
So you can see here that this function starts
with the word test and it's exported.
It is critical that this function is exported
or the testing tool will not find it.
It is also critical that whatever you name the test,
which is what comes after the word test,
okay, starts with a capital letter.
If you do this, the testing tool will not find
this test function, you will go crazy.
Now some people like using an underscore here.
I'm not a big fan of that because
we don't really write functions like that in Go.
There is some conventions around examples
that I will show you, where that might become important,
but overall, I don't do that.
It doesn't mean it's wrong.
If you want to do that, that's fine.
The tester will still find these test functions,
but you're not gonna see me doing it.
Okay, the other big thing is not only does it have
to start with test, with a capital t,
and the next letter has to be capital or underscore,
is got to accept a testing t pointer.
That's your integration into the testing tool,
the t pointer.
Now, the t pointer has the API that we're gonna need to use.
And if I just quickly just talk about the API,
you can see that we've got functions called error
and fail and fatal and log and some other things here,
and we're gonna focus on what this API is doing,
but basically, the log function is a way
of adding verbosity into your test.
It doesn't mean whether tests succeed or fail.
It's just extra tracing information.
You're gonna see me using error, and error is gonna mean
that we're gonna log some information,
but the test is now marked as failed,
but we keep executing any code left
inside that test function, and then you're gonna see
me using fatal and what fatal's gonna say is
hey, this test is failed, and guess what, we're done.
There's no reason to run anything else
because we're in such a bad state
that anything else is now irrelevant.
So you're gonna see me use these three functions,
error, log, and fatal, and then we'll use
some of the other API as we get there.
Now the whole idea of a unit test is to validate
that something is working, right?
Normally, we're gonna validate
that some API that you've written,
whether it's exported or unexported,
is behaving, that if I give it this input,
it produces this sort of output.
We're trying to unit test
the expected behavior of things, right?
The semantics of what we expect this API to do.
That is what we're trying to unit test all the time.
So, look at what I'm gonna do here.
I want to unit test the HTTP get call.
I want to validate that the semantics
of the get call are working, and so, what I do
is I set up some basic input here.
We're gonna use this URL, which is the URL
to the RSS feed for my blog, and we're gonna expect
to always get back a 200, right?
Http code 200 every time I call it.
Now I like the idea of given, when, should
when I'm writing unit tests, and given is
why we're writing this test to begin with,
when is what data we're using for the test,
and should is what should happen.
And I like to break this given, when, should
into three different blocks of code.
So you see me use an artificial code block here.
You see me using the curly brackets
that defines another level of scope,
but for me it's a readability marker
that this is the given section,
and you can have multiple given sections
in a test function if that's how you want to organize it.
But given the need to test downloading content,
when, so we're doing the next block,
when we use this URL expecting this status code,
our inputs and sometimes our expected outputs,
you can also see I've labeled the test as test zero.
I'm using some tabbing, you'll see the output in a second.
And then we start doing the test code,
and you want to make sure this code
is really as close to how you would write production code,
but also more importantly, if a function fails,
like there's an error, you gotta check your errors here.
I don't want to see blank identifiers
being used in your test code.
You're going to get in trouble when you do that.
So you've gotta check those errors,
and make it part of the should.
Now I like consistency in output,
so whether it succeeds or fails,
I want to really try to see the same exact message.
Yes, there's a little duplication here.
I told you, I'm crazy, okay.
There is a little duplication here.
Not a lot of people will do this.
I like doing it.
I like consistency in that final output.
It helps with readability when things are going wrong.
This is why I like it.
So, we should be able to make the call
without an error, right?
We should be able to make the call,
whether it failed or succeeded we'll find out.
You'll see that that's being placed right there
in the beginning of the output.
I'll show you that, and if there's an error
we're also going to show what that error was.
Now we're using fatal here because if that fails,
there's no reason to do anything else,
but if it doesn't fail, then we'll defer the closed body,
just like we would do in any normal piece
of code calling get, and then we can do our checks.
In this case, I'm checking that the status code
we got back is the 200 that we expect.
And now you're seeing my pattern for when there's an error.
This is my pattern for fatal, where again,
we're always gonna use that if statement
for that negative case, so fatal we're done,
or we could just log, but since with errors
we're gonna keep going, we have to use
the if else in this case.
So, now I'm testing the positive case here.
If it does succeed, then we should get the success,
else on the failure side, we should see failure.
Okay, so I've got a test here.
So what I'm gonna do is I've already moved
into this local directory here.
I've already moved into this local directory here.
You see the example one underscore test,
so let's just look run through the tests
through a couple of little different ways,
so you can see how we're using this basic test.
So I can just say go test, nothing less nothing more,
and since everything is passing,
all it does say is pass, and it shows you
that it took almost 900 milliseconds to run the test.
But what if I want to see the output of this test
even though it's succeeding?
I could use minus v,
and minus v is giving me that verbosity.
And now you can see why I've kind of structured
the output of my test the way I did.
Look at that, it says given the need, test zero
when we use this data, you see the check marks.
I mean, for me this is really nice, right?
And if you were to bring in some third-party packages
for this, they'd give you output that's kind of similar,
and I'm showing you here how we could do it ourselves
with really minimal work.
It just takes a little bit of consistency by the whole team.
So we've got that right there as well.
Now, another thing I can do,
let's just cause this test to just fail.
This is an artificial failure here, right?
Line 32, I'm gonna say not equals,
and what I'm gonna do, let's not even do that.
Let's change, let's say we were expecting
to get a 400 back, okay.
We're getting expecting to get a 400 back.
Now what's gonna happen?
Well, let me clear it.
If I just run go test now, notice that when the test fails,
it switches into verbosity mode automatically.
Didn't have to do the minus v.
That's really nice because then when things are passing,
our output will be really clean.
There's no noise there, and then when something fails,
we get all of that work we did for the verbosity,
and this is exactly what I want you to see.
You know, you see how here visually it says x,
hey this one failed, we were expecting a 400,
we got back a 200.
So, I'm trying to really make sure that the test output
gives us a very clear indication of what's going on.
It even tells us the line code,
line 35, on where that failed.
So, this is really really nice stuff.
This is our basic testing here.
Now Go also has on here, you're gonna see me use
a minus run flag, and what's nice
about the minus run flag is it uses a regular expression
to be able to filter the test functions
that you just want to run.
So, I can just say run anything that starts with down,
and you can see that it found test download,
which is pretty cool, so how you name functions too
will be able to filter out what functions you want
in an entire test suite by using that run function.
So this is our basic unit test.
I've shown you some of the command line options
that we have already.
It's gonna be up to you about how consistent
and how much verbosity you want,
but I love just this basic API.