Please purchase the course to watch this video.

Full Course
Testing Go applications can be made significantly easier and more effective by utilizing the Testify package, particularly its Assert subpackage, which offers a broad array of helper functions for simplifying test assertions. By converting manual assertion checks—often repetitive and verbose—into concise and readable single-line statements, Testify enhances code clarity and reduces the likelihood of errors in testing complex logic, such as string transformations and struct comparisons. Its assertion functions, like assert.Equal
and assert.ErrorIs
, deliver detailed error messages and diffs that make debugging straightforward, even with nested data structures. Additionally, Testify supports specialized assertions for working with files, HTTP responses, and JSON, enabling versatile and reliable test coverage. Adopting these tools not only improves test maintainability but also encourages best practices such as thorough error handling and database interaction testing, whether through live test databases or mocks, setting a strong foundation for robust and trustworthy Go applications.
No links available for this lesson.
One of my favorite packages when it comes to writing production Go code is actually related to writing tests. This package is called Testify, which is what we're going to look at in this lesson. Testify provides a number of different helper functions in order to make writing tests a lot more simple. Specifically, the Assert package, which provides a number of helpful functions in order to be able to write simple assertions when it comes to working with Go.
The Assert package is pretty huge. We'll take a look at how huge it is later on. However, for this lesson, we're going to go ahead and make use of it in order to simplify some tests I already have written.
The Sluggify Package Example
These tests are part of my CMS package application, which the code is available in a link in the description down below. This package is the Sluggify package, named after Testify. But what it does is it turns a title, or the string of a title, into a sluggified string.
What is a slug? Well, a slug when it comes to kind of blog posts and actually on my course website is the part of a URL that is used as a human-friendly identifier. So it's not necessarily an ID, which can be kind of ambiguous, especially when it comes to UUIDs or just integers. Instead, it's more of a custom identifier or an identifier that can change that's a bit more user-friendly.
So as you can see here, I have the Lesson Slug of "Course Introduction" and the Course Slug of "CLI Apps Go". Both of these combined allow me to know which lesson is the lesson that the user is trying to watch, rather than just using the actual ID, which is not exactly human-readable. This means you could also kind of copy and paste this URL and somebody could see, hey, I can kind of understand what this URL is pointing to.
As you'll see, each lesson in this course has its own unique slug, which represents the lessons or based on the lesson's title. Therefore, when it comes to this CMS application, I've done a very similar thing here, where I'm creating a title or where I'm passing in a title to the sluggify.FromTitle
function and it returns a string, which produces a string that is a product of the title itself with lowercase and dashes instead of spaces.
Current Testing Approach
Of course, to go along with this, I've added in a simple test to make this a little bit easier, which you can see I have two test cases here using table-driven testing. And I'm doing the familiar if res != tc.want
that we saw before:
if res != tc.want {
t.Logf("expected %s, got %s", tc.want, res)
t.Fail()
}
If I go ahead and run this, however, using the go test
command, pointing it to the sluggify package, you can see it fails. This is because we expected "go's standard library" and got "go's apostrophe standard library". This failure is something that we implemented using the t.Logf
function, which we've seen already many times throughout this course.
However, as we saw a few modules ago, we ended up implementing our own assertion handler, because, well, implementing this over and over again can get rather tedious. Whilst writing your own assertion handler is one way to do this, as I mentioned, the way that I prefer is to use the testify assert package, which provides a number of different functions for us to be able to do this.
Installing and Using Testify
So let's go ahead and actually replace this code with testify to see how it works. In order to do so, we're going to need to first add in the testify package to our code, which if I take a quick look on the documentation, there should be an install command that we can just copy, which there is:
go get github.com/stretchr/testify
The go get command, and paste it into the terminal. I'm going to do this in a new window just for the moment. I'm also going to go ahead and do go mod tidy
, because I had a bit of an issue in my test run doing this.
Okay, so let's go ahead and replace this with the assert package from testify, which I can go ahead and call using assert
. And you see it's the github.com/stretchr/testify/assert
package. And as you can see, there's many different functions available to it. We'll take a look at what all of these functions at the end, kind of all of the different functions that we want to use.
But in this lesson, I'm going to focus on the two functions that I use the most, the first of Equal
function, which, again, I've kind of imported it. Let me go ahead and do a go mod tidy
again. So yeah, I knew I needed to do it. It's kind of a bit strange. I'm going to do an LSP restart. Your mileage may vary with this. But in my case, for some reason, I needed to do it.
Using assert.Equal
Let's go ahead and scroll up and we can take a look at what the assert.Equal
function does. As you can see, there are a number of different equal functions. We're just going to use the equal one in this lesson. But you can see it takes a type of t
, which is assert.TestingT
. This is actually an interface which conforms to the testing.T
type. Then it takes the expected
, which is an interface, and the actual
, which is an interface. It also takes some message and arguments if we want to add a format string. There's a little example here as well.
Let's use this to replace our existing assertion on line 35. So we can do assert.Equal
, passing in the testing.T
type. And then we want to go ahead and use the tc.want
, which is our expectation, followed by the result:
assert.Equal(t, tc.want, res)
Now, if I go ahead and just comment this out, and if we go ahead and test the code using the go test
command, you can see this time we get the failure as we did before. However, now we get a bit more of an output. Not only did we get kind of where the error is occurring, which we did get before, we got the slugtest.go
, but this time you actually get a full error trace, as in the file where it's actually failing from. Very cool.
Not only this, however, we also then get some more detailed error messages. So we get the "not equal", which is great. That's the assertion that we used, or the assert function that we used. And we get the expectation, which was "go's standard library". And then we get the actual. The reason this is failing is because we have an apostrophe in this, and it's causing this to fail because I'm not actually properly encoding or removing the apostrophe. I'll take a look at how to do that very shortly, however.
Additionally, we also get a diff as well, so we can kind of see what the expected was and what the output was. In this case, you can see that this is the expected line and this is the output line as well. At this stage, it may not look very useful given that we already have this on the above line, but we'll see what this looks like a little bit more when it comes to comparing structs.
Fixing the Test
For the meantime, however, as you can see, we've managed to be able to replace our assertion with this one, which is a lot simpler to do so. So let's go ahead and remove this, and I'm going to go ahead and quickly fix this. I'll show you how to fix it as well in case you're interested in it. This is to use a regular expression:
re := regexp.MustCompile(`[^a-z0-9\-]`)
return re.ReplaceAllString(res, "")
So we can do regular expression, regexp.MustCompile
, and the regular expression that I'm going to use, backticks, we're going to use a character set, and we'll do it from a
to z
and 0
to 9
, and we also want to do the backslash for the dashes as well. We want to keep those in. Then we're going to use the not operator just to kind of inverse this. Then we can just go ahead and return re.ReplaceAllString
. So we're going to replace all of these characters that don't match these characters in this set, and we'll do kind of the res
, and we'll replace it with an empty space.
Now if I go ahead and run this, it should pass, which it does.
Testing Structs with Testify
So now that we've seen how to replace a simple equality check, let's take a look at another method I have in this function, or in this package. This is the New
method, which is used to create a struct type called Slug
. This is a more advanced slug that I'm defining, which if we actually went and encoded, would have the year and the month as well. This is actually very useful when it comes to blog posts.
So in some situations, you may have like 2025-05
, which is the current month, and then you have "my new post", or something similar. You'll notice that a lot of blogs actually do this to kind of order the slugs that they have, so that the slugs come through in a calendar-based ordering, or it just informs the user kind of when this blog post was published.
Again, I've decided to try and capture this in a slug construct, but this is actually just to see how slugs work when they come through with assert or testify. Here, as you can see, I've actually created a number of test cases for this new constructor function. I think I'm actually only testing in three in total. So this is the happy path, basically what we expect the slug to be based on the current date and passing in the new blog post, as well as a couple of error case handling as well.
So if we pass in kind of an empty date, we expect to get the error missing date error, which is defined just here. We got the error missing title as well, which should be returned in the event that we don't pass a title in.
So here I'm kind of capturing all of the logic here. I actually have a bit of a bug in this case, which we'll take a look at shortly. But first, let's take a look at the kind of happy path. So here, we're checking each individual property and if it doesn't match, we're failing out:
if res.Year != tc.want.Year {
t.Logf("expected year %d, got %d", tc.want.Year, res.Year)
t.Fail()
}
if res.Month != tc.want.Month {
t.Logf("expected month %s, got %s", tc.want.Month, res.Month)
t.Fail()
}
if res.Title != tc.want.Title {
t.Logf("expected title %s, got %s", tc.want.Title, res.Title)
t.Fail()
}
You'll notice I haven't put any logs here because, well, it got a bit tedious to do so. So I just decided to fail.
If we go ahead and simulate a failure, let's go ahead and say, let's go ahead and actually fail all of this. So we can do, I'm going to comment this out actually, just so that we know what the original was. So we'll set the title to be empty. We'll set the hard code, the date to be 13, which is obviously going to be incorrect. Let's do 12. And then in this case, I'm going to go ahead and hard code the year to be 2020 because, well, it isn't 2020 anymore and it won't ever be 2020 ever again.
Now, if I go ahead and test this, you can see that I get a failure. I'm not actually logging where the failure is, which is a bit of an issue, but we can't actually see where the failure is. But if I go ahead and actually enter this in just to be a bit more professional, we can see what this looks like. So let's go ahead and say "expected month". We'll do this and then we can do actually the month in this case, I believe. And then we can do res.Month
. That should be fine, I think.
Now, if I go ahead and run this, you can see that we are printing out "expected month, May got December". Let's do this properly. And we would do the same thing for the year and the title. I'm not going to do it because it's tedious as kind of a point to this tediousness.
But let's see what happens if we go ahead and replace this using assert or the testify assert. So we can just go ahead and do assert.Equal
and we'll pass in t
, we've seen this before, pass in the tc.want.res
and we'll just pass in the result as well:
assert.Equal(t, tc.want, res)
Now, when I go ahead and run this, and this is really going to show kind of the power of testify, you can see we get a lot more information for a lot less code. Not only are we getting kind of the inline expectation and actual, as well as everything else that we saw before, but we also get that diff that we looked at earlier, which didn't really make much sense when it came to a single line.
However, this time we can see the diff is kind of showing us that the year, what we expected, the month, what we expected, and the title, what we expected. However, in this case, it shows us the individual properties of the struct that aren't matching. If we have partial properties matching, so let's go ahead and say we do have the date.Month
that matches and we go ahead and test this again. This time you can see now the month we can see is correct, but we see all of the other diffs as well. So the year, what we kind of expected, what we got, and the of the title as well.
This for me is just a little bit easier than looking at the kind of that single line expectation as it really hones in on what's changed between both your expectation and your actual result. Pretty cool.
Error Handling with Testify
So let's go ahead and actually revert this back to kind of what it was before, as we already know what comparing two structs together looks like and how this is much easier than using the reflect package or implementing this ourselves. As we can see, it's now working.
However, in this case, let's take a look at our error handling, which you can see is a little bit more complex. In this case, we're checking to see if the error we receive back is not equal to nil, but we've specified that the error should be nil in the test case want, then we're logging out a result:
if err != nil && tc.want.err == nil {
t.Logf("unexpected error: %v", err)
t.Fail()
} else if err == nil && tc.want.err != nil {
t.Logf("expected error: %v, got nil", tc.want.err)
t.Fail()
} else if err != nil && tc.want.err != nil {
if !errors.Is(err, tc.want.err) {
t.Logf("error mismatch: expected %v, got %v", tc.want.err, err)
t.Fail()
}
}
So let's go ahead and take a look at this. Let's say in this case, we return nil. You can see now we're producing an error, "expected error, missing date got nil". Pretty cool. And of course, in the other case, if the error that we get back is nil, but we specified that we want an error. Actually, that's what we've already looked at.
In this case, if the error is not equal to nil, but we've not got an error. So let's say in this case, I think empty title. Let's say we haven't added in a test case for the empty title. Let's take a look at what this looks like currently. Okay. And let's say we go ahead and just get rid of this error here and we go ahead and run this. You can see this time "unexpected error, missing title" when we were expecting to not get one.
So whilst our current tests works for these two situations, as you can see, it's kind of hard to read this code. Unfortunately, this doesn't cover all of the potential errors that could go wrong. Whilst we're checking for the absence or the existence of an error, we're not actually checking for the error value itself.
For example, let's say I accidentally got these two errors mixed up. So if we had an empty title, I was returning error, missing date. And if we had an empty date, I was returning error, missing title. If I go ahead and now run this code, you can see that it passes, which isn't good.
Fortunately, we can kind of solve this by adding in yet another if error check. So if err != nil && tc.want.err != nil
, then we need to do a simple errors
or not errors.Is
. And we want to go ahead and pass in our err
and the tc.want.err
. Now we can then go ahead and kind of do a t.Fail
and we could just do t.Logf
. I'm just going to go ahead and say "error mismatch". We would print out the actual error as well, but I'm just going to kind of keep this simple for the meantime.
Now this should fail, which it does. "Error mismatch, error mismatch". So we can add in the ability to make sure that our errors match our expectation, but suddenly we have almost nine lines of code, each of which has three conditionals inside, which starts to push the boundary of how many conditionals you should have when it comes to kind of if-else checks.
You could of course get rid of the else-ifs by just having this. But again, this is kind of verbose, and if you have a lot of tests where you're going to be checking errors, this can get tedious pretty quickly.
Using assert.ErrorIs
So let's take a look at how easy it is to perform this when it comes to Testify. Well, there are a few different ways we can actually do this. The first is to use kind of the assert.Error
function, which will check to make sure that an error actually exists. So "error asserts that a function returned an error, i.e. a not nil". And you can see if assert.Error(t, err)
assert equal. So in this case, we could kind of go ahead and do if assert.Error(t, err)
, then we could go ahead and actually check to see if the error equals our expectation.
Whilst this is fine to use, for me this doesn't actually test the case that you have an error expectation and it doesn't return one. So instead, I like to use another method from Testify, which is the assert.ErrorIs
or ErrorIs
function. This function covers all of the test cases that we've defined, checking for the existence or non-existence of an error and comparing it to our expectation, as well as actually comparing the error values themselves.
So in order to use it, we can do err
and then our tc.want.err
:
assert.ErrorIs(t, err, tc.want.err)
So a single line to replace all of this logic. Now, if we go ahead and run this, you can see it works. We're getting "target error should be in the error chain". It's expecting missing date and we've got missing title, etc, etc. So we can go ahead and quickly fix this by reverting those changes.
But if also, if we go ahead and return a nil here, this should work as well. As you can see, "target error should be in the chain". We expected missing date, but we got zero in the chain. And I believe this should also work for the other case as well. If we go ahead and just kind of comment this out. So we're returning an error, but we're not expecting one. As you can see, "expected nothing". But in our chain, we were expecting the missing title.
So by just using the assert function, we managed to change our code from kind of being multiple lines, which was obfuscating what our assertions actually were, to just two simple lines to check both the error and the result. This for me is why Testify is my favorite package when it comes to production go code, as it just makes testing a lot more simple than not using it.
Testify Sub-packages
As I mentioned, however, Testify has a number of different functions inside of it. It actually provides three sub packages as well, which is:
- The suite package, which I honestly don't recommend using. It's kind of useful, but personally, I don't really like it
- The mock package as well, which is used for mocking. We'll take a look at mocking more in the next lesson and what you can actually do with it
- The require package as well. Sorry. The require package is basically the assert package, but it also terminates the current test
In any case, the assert package is mostly going to be what you want. However, for the first bit of homework, I recommend actually reading the documentation for the assert package because there are a number of different functions that may be useful to your own situation.
Homework Assignment
And so I recommend going through and checking to see which ones will make use to your own CMS application because there are a few that are really powerful, such as:
DirectoryExists
- really good for making sure that your tests actually created a directory- HTTP ones - which are great when it comes to testing HTTP requests
- JSON ones - for comparing two JSON together, which is really difficult to do when you're using string comparison. However, again, the testify package makes it really simple to do
- etc, etc
So first bit of homework, go through kind of all of these different functions, take a look at them and see if they inspire you when it comes to your own tests.
And secondly, if you haven't written any tests already yet for your CMS application, now is a great time to do so in order to lock in that functionality and to be sure that you've got everything working.
Testing Database Interactions
When it comes to actually writing tests, you may have a couple of questions on how to test some specific components, which specifically are going to be your database interactions. This can be kind of tricky to do, and there are two ways of going about it.
The first approach which I would recommend is to spin up an actual test implementation of your database, or a test database that you can connect to. If you're using Postgres, this would be kind of a local instance of your Postgres database that you can spin up and tear down easily at the start and end of each test. And if you're using SQLite, it's actually a lot simpler. This would be connecting to an in-memory version of your SQLite database, which your driver should support.
However, in addition to being able to test against an actual implementation of your database, in some cases it's actually very useful to be able to test against a mock version, as you're better able to constrain the results that come back. So if you want to see how your application handles a connection error or something, it's a lot easier to be able to do that when it comes to using kind of a mocking system than it is to try and simulate a connection failure when it comes to a real live system.
So in the next lesson we're going to take a quick look at how we can actually use mocks when it comes to Go, although they're not used that much when it comes to CLI applications, so it's going to be a very high level overview to give you kind of an understanding of how mocks work. I will likely be doing another course on writing production Go code when it comes to kind of more back-end or API systems, which we'll take a look at mocking much more in general. But in the next lesson we'll give a quick overview anyway.
In any case, make sure to do the homework for testing and then we'll take a look at how we can add mocks in for our database interactions in the next lesson.
No homework tasks for this lesson.