2 Jun 2019 at 01:52
We finished the last post with three tasks to tackle:
- Adding tests
- Keeping track of the score
- Adding a flag that allows the CSV file to be customised.
The priority is to add tests, as I’m a firm believer in the benefits of Test Driven Development, often abbreviated as TDD. In a nutshell, you write a test of some behaviour you want to implement. You then run the test, see it fail, and make the smallest possible change to make it pass. Once your test is green, it’s time to refactor. Thus it becomes a cycle; red green, refactor.
It helps you avoid a lot of common pitfalls when it comes to coding, as well as forces you to think about your code and domain more abstractly.
Go has a
testing package from the standard library, so let’s have a try at writing a unit test (a unit test is a test that tests a small ‘unit’ of code). Let’s pick a small bit of code that performs a single function, and write a test for it. In cases you forgot what the code looks like at the moment, here’s the entire thing
Let’s recap on what the
main function is doing.
- It reads a CSV file and loads it into memory until the
mainfunction finishes executing (
deferis a keyword that allows you to run some code at the end of a function’s execution)
- It instantiates a CSV reader, and uses
reader.ReadAll()to create a slice of slices composed of strings (the successful return value type for ReadAll is
stringwhich translates to 2 dimensional array where all elements in the inner slices are strings.
- We create a for loop which will iterate through each of the inner slices in the outermost slice (inner slice being a line in the CSV, and the outermost slice being all of the CSV lines.
- We create instantiate a
quizItemstruct from the information contained in the inner slice.
- We print the question from the
quizItemstruct, and print an instruction to the user.
- We instantiate a
bufioreader to take the input from
stdinuntil it receives a
- We print the user’s answer, and then use
TrimSuffixto remove the invisible
/ncharacter from the user’s answer.
- Finally, we compare the two, and print a message to the user, which has different content depending on whether they got the answer right or wrong.
Point 8 is something that I would like to test; so let’s imagine what the code would look like in an ideal world. I want it to take two arguments, both of type string, and I want it to return a string too. I want the return value to be determined by the arguments that are passed in, specifically if they match or not.
3 Jun 2019 at 04:05
Let’s write a test in Go! There’s three steps here
- Creating a function which tests the unit of code in question. The test function should have the format
Xxxdoes not start with a lowercase letter. (The asterisk is a pointer to the location of the testing variable in memory)
- Creating a file whose name ends in
_test.go, and placing our test inside the file.
go testand see blood (hopefully)!
It’s quite simple isn’t it? Let’s get started.
$ touch main_test.go to create the test file in our local directory, which now contains 4 files;
GopherQuiz The binary executable we compiled with
main.go The file containing our code
main_test.go Our newly created test file
problems.csv The CSV file containing our quiz
For the time being, we will keep it like this, but it’s important to recognise that as a project grows, organising your files is extremely important. It helps others find what they are looking for faster, while also providing context to the code.
pool/write.go is easy to recognise as a file related writing to a pool, while
channel/write.go would be relating to writing to a channel.
Tangent over, let’s go back to writing our test in
First we specify the package we are testing, which in this case is
main. We import the
testing package so that we can use it (remember the format of the test function requires
t *testing.T as an argument. Then we define our test.
Now I’m going to go on a small tangent about Test Driven Development (TDD). When doing TDD, you always write a test first, before ANY other code. What does this mean? It mean that before you think about how to do something, you think about what you want to do.
I want to compare two strings and see if they match, and return a different string depending on whether they match or not. So I will call my test
CompareAnswer. But is it just comparing the answers? No, it’s also returning either a success or a failure message depending on the comparison. So (without going into the rabbit hole of naming stuff), I’m going to change the name of the test to
Going back to the code; we then set the output of the function we are testing
CheckAnswer to a variable called
got. Incase you forgot,
:= is shorthand for declaring a variable with implicit type. We then make our assertion: is what we got
true? If is is, fail the test using
t.ErrorF (prints an error with formatting), in which we let the user know what they got, and what we expected. The test passes if it executes without failure. (FYI,
ErrorF is simply
LogF followed by
Fail. Had we wished to fail the test without giving a message, we could’ve just used
Pretty simple right? No-clickbait, but this one simple trick will save you hours and hours of development time; a good test suite is the difference between hating and loving your job when things go tits up and you need to apply a patch.
I left my first job because of the stress incurred from working on production web applications with zero tests; nothing is more frightening then having to change code and not knowing if you’re about to break something else.
Right, back to the test. It’s actually a bad test right now because although it’s named correctly, the assertion is wrong. Let’s think back; what do we want this code to do? We want it to compare two strings, and return one string if they match, and a different string if they do not. Let’s make those changes, to the sweet sounds of the best video game OST ever Secret of the Forest.
Right, let’s go through this step by step; we assign the return of the function we are testing to
gotCorrect, compare it to the string we expect to receive, and fail if we do not receive what we expect. We then do the same thing with an answer which is incorrect, repeat the process. Nice!
But there’s a slight issue here; something that is seemingly innocuous and innocent. Something that can quite easily make a test suite, your shield and sword against bugs, heavy and unwieldy, quickly becoming a burden that will make your fellow developers curse and can put off inexperienced developers writing tests at all.
Recall the reason we began writing this test; we wanted to take a unit of our code and test that it works independently. If it works by itself, it stands to reason that if the program isn’t working, the fault lies with another part of the program. By making small, modular units of code, we can test each part independently, and thus the surface area where bugs can hide becomes smaller. This concept of small, modular units of code that do one thing independently of all else is often referred too as the Single Responsibility Principle (SRP).
Our test violates the SRP; it has two responsibilities. An easy way to check if your code violates SRP is to ask it questions about what it does.
ME: What’s cracking
TestCheckAnswer, how’s it hanging? You up too much these days?
TestCheckAnswer: Shiiiiiiiiiiiiiiiet, they got me on some slave-type shit bro.
ME: What you mean man? I thought you were just checking answers, that’s what you do right?
TestCheckAnswer: Dude, look at my fricking name, I’m
TestCheckAnswer, but for some reason I have to check two answers. What happens when I wanna go on holiday? Y’all ain’t gonna have no one testing
CheckAnswer! In fact, I don’t even wanna be called
TestCheckAnswer, I wanna be called
TestCheckCorrectAnswerso I don’t have to be here doing the work of two people! Do me a solid and change that ASAP, cause
CheckAnswermight start getting more work to do, and then y’all are gonna have me doing more work, and ergo you are gonna have to do more work! Come on bro work with me here, I ain’t trying to end up like
main, that dude got so many problems I don’t even know how he’s still around!
(I always end up personifying my tests based on a good friend of mine who’s pretty lazy. Naturally, he’s a great programmer!)
Tests are functions, functions should have a single responsibility, ergo, we should change this test to have a single responsibility. We do that by splitting them out, which in this case is pretty simple;
The benefit of this is that it makes our code more flexible; flexible code is good because the future is uncertain, and in the face of uncertainty, we must be able to adapt to new conditions quickly.
Right! We’ve got our test, now what? We run it, and watch it fail! https://gist.github.com/7b81ad479f409203b188cf6b06dfebfa
Now is the fun part. We write the bare minimum, simplest code to make the error message change. Imagine the test error output as your hint guide; it tells us
CheckAnswer is undefined, so let’s define it!
That’s it! We make the smallest change possible to satisfy the error we received. Now we run the test again. https://gist.github.com/94fc49b46e3286640e377ab582156334
Success! We have got a different error message. A different error message from a test is always a good thing; it means that you are making progress. This one is a little different; it is telling us that
TestCheckCorrectAnswer is sending too many arguments to
CheckAnswer. It even helps us more by telling us what we have given it
have (string, string), and what it wants
Remember, we want to satisfy the test. So if the test has two strings to pass, we must make
CheckAnswer accept two strings.
Running the test again gives us a more peculiar output; https://gist.github.com/473fb61504e2fd413d1a13717756874c
This error message is a bit different, but still useful none the less! It tells us that the function call we made is used as a value. What does this mean?
If you have a look at the
CheckAnswer function above, you can see that we don’t specify a return type. This means the function itself is returned. This is pretty cool, I wonder if it means you could create functions that return other functions?
Anyways, to fix this, we add a return value.
int to show you that, although we know what our function will do (since we spiked it), there’s a lot of scenarios where you have no idea where to begin. In these scenarios, the best thing to do is to make a hypothesis, and run the test to see what happens. In this case, it’s an easy fix.
We’re getting closer! Let’s do the simplest thing possible to make one of the tests pass.
Finally! A passing test! But we’re not done yet, because one of the other tests is still failing. But this is the essence of TDD; you write small changes incrementally, and run your tests each time you go along.
When you hit an error message, you make a hypothesis as to it’s cause, make the relevant change to your code, and test it by…. Running the tests! In this way, you are protected from adding needless code that isn’t used.
I’m going to skip ahead now and show you the final
And there we have it! No more failing tests!
I hope you’ve enjoyed this blog post and found it informative, and as always, C&C is encouraged and appreciated. I’m going to leave it here since it’s gotten quite long, but in the next blog post we’ll continue the task by.
- Adding a customisable timer
- Implementing score
- Allow custom quiz’s to be pass by file name