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 forcing 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
package main
import (
"bufio"
"encoding/csv"
"fmt"
"log"
"os"
"strings"
)
type quizItem struct {
question string
answer string
}
func main() {
csvFile, err := os.Open("problems.csv")
if err != nil {
log.Fatal(err)
}
defer csvFile.Close()
reader := csv.NewReader(csvFile)
records, err := reader.ReadAll()
if err != nil {
log.Fatal(err)
}
for i := 0; i < len(records); i++ {
// Create quizItem object
quizItem := quizItem{records[i][0], records[i][1]}
// Print out the question
fmt.Println("Question:", quizItem.question)
// Create reader and allow user to input their answer
reader := bufio.NewReader(os.Stdin)
fmt.Print("Enter your answer now: ")
// Expect answer to be given once they hit return
text, _ := reader.ReadString('\n')
fmt.Println("Your answer is:", text)
// Trim the newline suffix from the input
text = strings.TrimSuffix(text, "\n")
if text == quizItem.answer {
fmt.Println("Correct! Well done")
} else {
fmt.Println("WRONG! Answer is:", quizItem.answer)
}
}
}
Let’s recap on what the main
function is doing.
- 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) - 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. - 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
quizItem
struct from the information contained in the inner slice. - We print the question from the
quizItem
struct, and print an instruction to the user. - We instantiate a
bufio
reader to take the input fromstdin
until it receives a/n
keypress. - We print the user’s answer, and then use
TrimSuffix
to remove the invisible/n
character 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
Chrono Trigger: Main Theme, a song by London Philharmonic Orchestra on Spotify
Let’s write a test in Go! There are three steps here
- Creating a function which tests the unit of code in question. The test function should have the format
function TestXxx(*testing.T)
whereXxx
does 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. - 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 build
— 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 main_test.go
package main
import “testing”
func TestCompareAnswer(t *testing.T) {
got := CompareAnswer(“3”, “3”)
if got != true {
t.Errorf(“CompareAnswer = %v; want true”, got)
}
}
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 means 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
.
func TestCheckAnswer(t *testing.T) {
got := CheckAnswer(“3”, “3”)
if got != true {
t.Errorf(“CheckAnswer = %v; want true”, got)
}
}
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.
func TestCheckAnswer(t *testing.T) {
gotCorrect := CheckAnswer(“3”, “3”)
if gotCorrect != “Correct! Well done” {
t.Errorf(“CheckAnswer = %v; want ‘Correct! Well Done’”, gotCorrect)
}
gotIncorrect := CheckAnswer(“3”, “2”)
if gotIncorrect != “WRONG! Answer is: 2” {
t.Errorf(“CheckAnswer = %v; want ‘WRONG! Answer is: 2’”, gotIncorrect)
}
}
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 to 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’mTestCheckAnswer
, 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 testingCheckAnswer
! In fact, I don’t even wanna be calledTestCheckAnswer
, I wanna be calledTestCheckCorrectAnswer
so I don’t have to be here doing the work of two people! Do me a solid and change that ASAP, causeCheckAnswer
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 likemain
, 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;
package main
import “testing”
func TestCheckCorrectAnswer(t *testing.T) {
got := CheckAnswer(“3”, “3”)
if got != “Correct! Well done” {
t.Errorf(“CheckCorrectAnswer = %v; want ‘Correct! Well Done’”, got)
}
}
func TestCheckIncorrectAnswer(t *testing.T) {
got := CheckAnswer(“3”, “2”)
if got != “WRONG! Answer is: 2” {
t.Errorf(“CheckIncorrectAnswer = %v; want ‘WRONG! Answer is: 2’”, got)
}
}
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!
06:24 $ go test
# github.com/adilw3nomad/GopherQuiz [github.com/adilw3nomad/GopherQuiz.test]
./main_test.go:6:9: undefined: CheckAnswer
./main_test.go:13:9: undefined: CheckAnswer
FAIL github.com/adilw3nomad/GopherQuiz [build failed]
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!
func CheckAnswer() {
}
That’s it! We make the smallest change possible to satisfy the error we received. Now we run the test again.
06:29 $ go test
# github.com/adilw3nomad/GopherQuiz [github.com/adilw3nomad/GopherQuiz.test]
./main_test.go:6:20: too many arguments in call to CheckAnswer
have (string, string)
want ()
./main_test.go:6:20: CheckAnswer("3", "3") used as value
./main_test.go:13:20: too many arguments in call to CheckAnswer
have (string, string)
want ()
./main_test.go:13:20: CheckAnswer("3", "2") used as value
FAIL github.com/adilw3nomad/GopherQuiz [build failed]
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.
func CheckAnswer(string, string) {
return
}
Running the test again gives us a more peculiar output;
06:35 $ go test
# github.com/adilw3nomad/GopherQuiz [github.com/adilw3nomad/GopherQuiz.test]
./main_test.go:6:20: CheckAnswer("3", "3") used as value
./main_test.go:13:20: CheckAnswer("3", "2") used as value
FAIL github.com/adilw3nomad/GopherQuiz [build failed]
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.
func CheckAnswer(string, string) int {
return 1
}
06:36 $ go test
# github.com/adilw3nomad/GopherQuiz [github.com/adilw3nomad/GopherQuiz.test]
./main_test.go:7:9: cannot convert "Correct! Well done" (type untyped string) to type int
./main_test.go:7:9: invalid operation: got != "Correct! Well done" (mismatched types int and string)
./main_test.go:14:9: cannot convert "WRONG! Answer is: 2" (type untyped string) to type int
./main_test.go:14:9: invalid operation: got != "WRONG! Answer is: 2" (mismatched types int and string)
FAIL github.com/adilw3nomad/GopherQuiz [build failed]
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.
func CheckAnswer(string, string) string {
return ""
}
06:47 $ go test
--- FAIL: TestCheckCorrectAnswer (0.00s)
main_test.go:8: CheckCorrectAnswer = ; want 'Correct! Well Done'
--- FAIL: TestCheckIncorrectAnswer (0.00s)
main_test.go:15: CheckIncorrectAnswer = ; want 'WRONG! Answer is: 2'
FAIL
exit status 1
FAIL github.com/adilw3nomad/GopherQuiz 0.005s
We’re getting closer! Let’s do the simplest thing possible to make one of the tests pass.
func CheckAnswer(string, string) string {
return "Correct! Well done"
}
06:51 $ go test
--- FAIL: TestCheckIncorrectAnswer (0.00s)
main_test.go:15: CheckIncorrectAnswer = Correct! Well done; want 'WRONG! Answer is: 2'
FAIL
exit status 1
FAIL github.com/adilw3nomad/GopherQuiz 0.005s
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.
func CheckAnswer(answer string, correctAnswer string) string {
if answer == correctAnswer {
return “Correct! Well done”
} else {
return (“WRONG! Answer is: “ + correctAnswer)
}
}
06:56 $ go test
PASS
ok github.com/adilw3nomad/GopherQuiz 0.004s
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
Top comments (0)