Let's look at how to create interfaces and mock them for tests
Welcome back. At the end of the last video we acknowledged that our tests were broken and then we need to fix them. There’s a few different ways we could go about fixing them.
The first is we could implement very similar logic to that we have in main.go where we have created a database, an actual database, and we’ve connected to it and we’ve passed that through. We could pretty much copy those lines to here and that would work. There are some situations where you may want to do that.
And there is also a whole category of people out there who think they’re the most valuable sort of tests. We call those integration tests. Integration tests are where we really test our code against a real database in this instance to make sure it works as expected.
Whilst I also agree they’re really valuable, generally, they’re a little bit harder to run, they’re a little bit slower, and we’d also need to delete the state of our database or make a new database each time to ensure that all of our tests remain pure. And by pure, I mean given an input, they give the same output.
If we had other things in the database we may start to see conflicts between our tests which makes them flaky and we’d be unable to reproduce the same result. So what we’re going to do is we’re going to try and keep ours as unit tests.
To do that we’re going to have to mock our database in that we don’t really want to connect to a real database we want to connect to something that looks an awful lot like a database. To do that we’re going to introduce a new concept in Go called interfaces.
Interfaces constitute a sort of contract. It tells us that we can expect a struct to have certain methods but not explicitly how those methods are implemented. Lots of different structs can implement interfaces which is really important for how we’re going to get our unit tests passing again.
Go has implicit interfaces. This is different from Java where you may have something like some object implement some interface. In Go it doesn’t work like that and we’ll see in a second.
So what we’re gonna do is we’re gonna make a fake database. I quite like using mocks in my tests but there are a lot of people who believe it’s a bad pattern. I want to ensure you can make your own decision on this so I’ve linked to some blogs giving other points of views below.
Get to it if anything I’ve said about interfaces don’t make sense I think once you see the implementation it’ll make a little bit more sense. So the first thing I’m going to do is I’m going to go to todo.go and what I’m going to do is you’ll see here in our service we have a dependency on a concrete struct which is the database instead we’re going to swap this to be an interface.
So what we’re going to do is we’re going to jump into this db and we’re going to look at the functions that we know we can from a db because this is our contract. So we get two functions that we really care about we get insert item and we get get all items.
So we could say that the only thing we care about our database having at least at this layer is we care about it having a insert item function that returns an error and we care about it having a get all items function that returns slice of items in error. So let’s define what that might look like as an interface so type manager interface and I’m going to copy the function signatures from here.
And this needs to be a db.item and let’s take this one too and this is a db.item too so let’s revisit what I said we want our interfaces to form a sort of contract so what that means is we want to know that definitely get an insert items function and we want to make sure that we get all items function but we don’t necessarily care about how they’re implemented.
The power of this is we can swap out implementations without our business logic needing to be updated and this is going to be really important for making our test pass so what I’m going to do here is I’m going to swap the type of db to be a manager instead I’m going to make the same change here.
And if you look nothing else in my code broke if I scroll down all of these things still compile because we still are matching the contract of that function we’ve swapped it out to be an interface which gives us a little bit more flexibility but up to now we haven’t caused any other issues in our code.
The other thing you’ll have noticed is on the left-hand side in the gutter here we’ve got this I from GoLand now and if I click this it will take me to all the places that implement the interface and if I click it from here it will take me to the interface definition this really useful for just figuring out exactly what things you have that do implement an interface.
To my point before as well you notice we don’t type here type db implements manager it’s just implicit the fact that we have the functions that we defined in the interface means that we satisfy it and therefore we’ve demonstrated Go’s implicit interfaces here.
Let’s just check main.go as well and the same is true here no changes necessary here so you can see the power of interfaces hopefully quite quickly. So now what we need to do is we want to have a mock version of our database that we can use to help our test pass.
So I’m going to scroll up in the file to here I’m going to use a feature of GoLand here which is just to collapse this for now just to give me a little bit more space so you can see everything on the screen and I’m going to make something called a mock DB struct.
What I’m going to do is I’m going to put this here and I’m going to come over to this in GoLand and we click on it and I’m going to press options and enter and I’m going to put implement interface it brings me up the ability to search for an interface so I’m going to type in manager and it brings up this one I hit enter and it’s generated me the methods necessary to satisfy the interface.
So I’m just going to fast import all the things we need and we’re going to add a definition of the interfaces here so what I’m going to do for mockdb is I’m just going to store a little bit of state inside it which will help us out in a second so it’s going to have a db.item on it for our insert item function we’re going to do is do m.items equals append m.items.
I’m going to make this a pointer as well just because I want to be able to update the state of it and we just need to return nil here and in get all items we just need to do return m.items and nil and get rid of that format our code.
And as you can see we’ve made a very very simple implementation of a database it looks an awful lot like the one we started with at the beginning of this activity but in this instance we don’t care that it’s not going to persist our items longer than we need them to so now we need to fix our tests.
So inside here we can do m equals mock db hopefully I can pass that into all these places and we should all be good and search is upset because the search function returns two things it returns the slice of strings and an error so we’re gonna change this a little bit.
So let’s get this and do error put this down a line and put if and then let’s have one more case here which is if error not equal nil then we can do that looks good t.error looks good to me we don’t expect to get an error so we’ll fail the test if we do now all of this compiles again no errors anywhere so let’s run these tests and hopefully they should all pass.
And they do the final thing to call out before we close out this video you’ll notice that mock DB also has this eye in the gutter even if I click that it takes me to the interface and now if I click on the interface it gives me two options because it shows me two things that implement the interface DB and mock DB.
So hopefully this has helped you to understand the power of interfaces and how we can quickly refactor and change the implementation details of the underlying database without having to make crazy changes to our code.
This can be really, really useful in certain situations. For example, let’s imagine we didn’t want to use Postgres anymore and we wanted to use Mongo as our database. As long as Mongo satisfied the manager interface and had these two methods, we wouldn’t need to change our business logic at all. This can be really, really useful depending on the type of application you’re building.
So I hope you found this useful. We’re actually finished with this first API of ours now. So congratulations, you’ve built your first pretty complex Go application and it’s got tests with reasonable test coverage.
As a challenge try and improve the test coverage and try and add some more advanced features to this API but I think we’ve learned a ton already.
We’re going to close out this section by just looking at some additional things that GoLand can do to help us. The first one is looking at Go tools and the second is going to be how we can push all the code that we wrote to Git so that we can use it as part of our portfolio. So I’ll see you in the next video where we’ll do both.