As an up and coming or experienced Go dev, I am sure you are writing tests all over the place! Right? Riiiiiiiiight?
Trust, me you better be. There are a million blog posts out there talking about the value of testing your code, so I will sum up why you should test your code in one sentence. Dum dum brain make dum dum mistake, test catch dum dum mistake, you keep job.
There you go, now you know why you should test your code. That sentence applies to building new codebases, and even more when you come back to change that now super old codebase (any code that you or someone else wrote that is at least 6 months old). It will catch silly mistakes and it provides a quick feedback loop for your development. So, lets make it easier!
What are the pitfalls of writing tests?
My favourite part of writing this blog is that I get to tee up the questions I have answers for. I have a rich internal (and often, external) dialogue.
So, what about testing code is hard? Everyone is doing it. Especially in Go right, it’s all built in! Table tests make testing different situations simple, assertion libraries make the tests cleaner and creating all of the database dependencies just to test logic is a breeze. Wait, that last one might not be true. That’s lucky, I was worried I had nowhere to drive the blog post for a minute…
If you have worked in programming for any amount of time you will have run into unit tests that have more complex code that what you are testing. This is usually because of mocks, dependencies, setting up state for the situations, and this is all typically just avoid the complexity of using actual infra (a database / external service). You almost need to test your tests. This seems like an anti-pattern.
Well, we have to mock these things, unit tests should be quick and not need external dependencies!
Of course! That is what all the literature says I think? Typically it does, but maybe there is a better way?
Don’t mock your dependencies!
Clickbait right? That’s the goal at least. First of all, mocking anything is rude, they are doing their best (haha). I am not actually saying connect to a database or a service for unit tests though. I know there are people who think that is OK for certain cases, and I won’t say its wrong, but I will say its not how I like to do it and I personally find it more the domain of an integration test to work with systems not immediately in the code.
What I a think is the right approach is to test the code that you need to test. You know, instead of the code you don’t need to test…
Maybe an example would help.
Let’s look at a pretty common example of a service level function, obviously reduced down to silly simple.
Interfaces! Thats great! Someone must have been reading my blog! We can definitely unit test this, we just need to mock the ThingReader
. Oh, and the Outputter
. Oooooh and the DBThinger
, I have to mock more code than I am testing. Also when I am mocking I have to set up each variant to read in the right data to validate than do a fake func for the other two, even if using something like Mockery you have some boilerplate to prep.
Hold on though, what am I actually testing here? Does this function own the ThingReader code? No, what about the DB? Also, no. Realistically this function is supposed to DoThing and that requires some kind of business logic or validation to continue. All the other stuff is just data in and out and will be tested with integration tests / end-to-end tests. Boy, it’s a lot of set up just to fake passing in data so I can test the logic of the function, if only there were another way!
Oh ya, I already primed everyone that there is a better way. It’s pretty complex though, maybe another example:
Yes! What if we put all this stuff we want to check… Somewhere else! Suddenly I don’t have to mock data in, I can just pass info into the little function and check the results!
Now all we have to do to test this is to set up a blank process and call the function with the scenario data we want to try out. This is a lot easier to read and understand, and it is a lot easier to test. We are directly testing the logic of the function and not the data in and out.
Look, I know it seems simple, but that is good, simple solutions are easy to add in going forward! I personally think this will let you iterate faster and check your code better. It does have the downside of creating a bunch of small little functions, but hopefully some of them are reusable.
This also helps in old code that has a lot of dependencies and is a in general, uhhh, bad? Ya, we’ll keep it PG and call it bad, not a horrendous pile of shite. Given a bad (aka horrendous pile of shite) or tangled code, you can pull out some sections and test them in isolation. This will help you refactor and clean up the codebase.
That’s all, short and sweet. I hope this helps you write better tests and keep your codebase clean. If you have any questions or comments, feel free to leave a comment below or reach out to me on my socials!
Check here soon for my preferences for testing tools and testing tips in Go!
Post Cover Image from: Share Grid