Skip to content

Instantly share code, notes, and snippets.

@adilw3nomad
Last active June 3, 2019 08:30
Show Gist options
  • Save adilw3nomad/6c0a96752c0116566e4c79ed1e47f731 to your computer and use it in GitHub Desktop.
Save adilw3nomad/6c0a96752c0116566e4c79ed1e47f731 to your computer and use it in GitHub Desktop.

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

https://gist.github.com/3b6ac24b9c148fc0101f1d9eff366129

Let’s recap on what the main function is doing.

  1. It reads a CSV file and loads it into memory until the main function finishes executing (defer is a keyword that allows you to run some code at the end of a function’s execution)
  2. 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 [][]string which translates to 2 dimensional array where all elements in the inner slices are strings.
  3. 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.
  4. We create instantiate a quizItem struct from the information contained in the inner slice.
  5. We print the question from the quizItem struct, and print an instruction to the user.
  6. We instantiate a bufio reader to take the input from stdin until it receives a /n keypress.
  7. We print the user’s answer, and then use TrimSuffix to remove the invisible /n character from the user’s answer.
  8. 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

Chrono Trigger: Main Theme, a song by London Philharmonic Orchestra on Spotify

Let’s write a test in Go! There’s three steps here

  1. Creating a function which tests the unit of code in question. The test function should have the format function TestXxx(*testing.T) where Xxx does not start with a lowercase letter. (The asterisk is a pointer to the location of the testing variable in memory)
  2. Creating a file whose name ends in _test.go, and placing our test inside the file.
  3. Run go test and 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 go buildmain.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 main_test.go

https://gist.github.com/0979eeb41e6c9c955cb9f3ff274a41ed

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 CheckAnswer.

https://gist.github.com/725229655d9a30f58f1d9fac9e473270

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 Fail.)

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.

https://gist.github.com/dd8263799da965f8068eba6f0d110cd8

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 TestCheckCorrectAnswer so I don’t have to be here doing the work of two people! Do me a solid and change that ASAP, cause CheckAnswer might 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;

https://gist.github.com/2877e82cc8df4daf2d408550e0584287

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!

https://gist.github.com/869e8ab725bcedb6bc2ef5bf25874944

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 want ().

Remember, we want to satisfy the test. So if the test has two strings to pass, we must make CheckAnswer accept two strings. https://gist.github.com/b8bc694eae7370d95315099e0aedb59f

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.

https://gist.github.com/f4fed97144f598cb31a2bd36e103d637

https://gist.github.com/4ecb7d12647cdc95aada299f455621af

I used 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.

https://gist.github.com/43baa9ae2cfa1eec41c1f5a0dd1e1869

https://gist.github.com/a501c4f066fa1041ec1edf625a244809

We’re getting closer! Let’s do the simplest thing possible to make one of the tests pass.

https://gist.github.com/58f41e3048be50580ab19bbe4ec61206

https://gist.github.com/659c4120cb7ff499c2fc5d938d77960e

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 CheckAnswer function.

https://gist.github.com/9ddaba39607268e5c17f1204b10ffdad

https://gist.github.com/bec64680b999466b8938c9d327c5286f

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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment