JWJames Wallis
Cover for What I learnt using TDD for Advent of Code (2020)

What I learnt using TDD for Advent of Code (2020)

23rd December 2020

2020 hasn't been great for anyone, but it has gifted me the ability to work from home and reduced my morning commute to about 30 seconds. As it's December now, I figured I could use the time I've gained from missing the commute and try my hand at this year's Advent of Code.

I used Go and Test Driven Development to complete Advent of Code. I opted for Go over JavaScript, my usual language of choice for projects, as one of my goals for the year was to learn it and use it in a personal project. Although I had to pause this due to other comittments. AoC provided a great opportunity to use a different language and pick up some skills!

Real quick - What is Advent of Code?

Advent of Code is an Advent calendar of small programming puzzles for a variety of skill sets and skill levels that can be solved in any programming language you like. People use them as a speed contest, interview prep, company training, university coursework, practice problems, or to challenge each other.

Eric Wastl - creator of Advent of Code.

Why did I use TDD?

Originally I had no intention of using Test Driven Development to complete Advent of Code. I figured that I could log on each day, write a program and submit the answer - because of course my code would always be 100% flawless.

I found out pretty early on that this approach was wrong for myself as my code, like most, was not always going to work correctly on its first iteration. After attempting the first day and having an answer that wasn't quite right, I decided that blindly creating a program was not the correct approach, I needed to have more faith that I was on the right track before submitting my answer.

Moreover, if you look at the way that each Advent of Code task is structured, you'll see that it favours a TDD approach.

General AoC task structure

  1. Introduction
  2. Explanation of task
  3. Example input and output
  4. Your task and an input file (which you can copy and paste into a file locally on your machine)
  5. Answer submission box

The important section for TDD is the example input and output. While their primary function is to further explain the task, we can use them to base our TDD around - instead of using the input we're given for the actual solution, we can use the example input and ensure that when it is used by our program, it always returns the example output.

What I gained by using TDD

Through using Test Driven Development to complete tasks in Advent of Code I was able to gain:

  • Confidence that my solution was correct
    • This was a huge benefit, AoC doesn't give you any hints if you supply an incorrect answer so it is easy to get lost trying to find tiny bugs in the program. Using TDD meant that I could have full faith that each function was working as expected.
  • Less time spent debugging the program.
    • You'll have to debug when a test fails but splitting it up makes bugs so much easier to find.
  • Code quality is improved.
    • Each function is better defined as time is spent focusing purely on it rather than the whole program.
  • Skills in Go
    • Learning the language itself.
    • Reinforcing how I could both write tests and develop using TDD.
  • Skills and experience using TDD to design and write a program.
    • Obviously.

Example - How I used TDD for AoC day 4

As usual the code can be found on my GitHub: Advent of Code, Day 4 solution - GitHub.

Task overview

I won't go into huge detail to describe the task for day 4 and I'll only focus on part 1. I recommend you read the brief for AoC day 4 (Passport Processing) on the AoC website.

Essentially the task was to take an input consisting of multiple "passports" (if you are already lost - read the brief) and determine how many are valid when compared to the given set of rules.

Each passport was represented by these items:

byr (Birth Year)
iyr (Issue Year)
eyr (Expiration Year)
hgt (Height)
hcl (Hair Color)
ecl (Eye Color)
pid (Passport ID)
cid (Country ID) - not required to be a valid passport

The given rules for part one state that each field is required in a passport apart from cid (Country ID) which is optional.

Some example passports (separated by a blank line):

# Valid passport
ecl:gry pid:860033327 eyr:2020 hcl:#fffffd
byr:1937 iyr:2017 cid:147 hgt:183cm

# Invalid passport - missing hgt (height)
iyr:2013 ecl:amb cid:350 eyr:2023 pid:028048884
hcl:#cfa07d byr:1929

# Valid passport - missing cid (Country ID) but the rules state this isn't required
hcl:#ae17e1 iyr:2013
eyr:2024
ecl:brn pid:760753108 byr:1931
hgt:179cm

# Invalid passport - missing cid (fine) and byr (Birth Year)
hcl:#cfa07d eyr:2025 pid:166559648
iyr:2011 ecl:brn hgt:59in

Using TDD to solve day 4

I split up the task into separate parts like this:

  1. Read input and convert into a passport
  2. Determine which passports are valid according to the given rules
  3. Count valid passports
  4. Print answer (not tested)

For part 1 I created a function called ConvertLineToPassport and three test cases which would convert a given string into a passport. Once the test were created, I implemented the ConvertLineToPassport function to read all known parameters (ecl, hgt, etc) and assign them either their provided value or an empty string if they were not present.

One of the tests:

t.Run("converts a valid string into a Passport", func(t *testing.T) {
  line := "ecl:gry pid:860033327 eyr:2020 hcl:#fffffd byr:1937 iyr:2017 cid:147 hgt:183cm"

  want := Passport{
    eyeColor:       "gry",
    passportID:     "860033327",
    expirationYear: "2020",
    hairColor:      "#fffffd",
    birthYear:      "1937",
    issueYear:      "2017",
    countryID:      "147",
    height:         "183cm",
  }
  got := ConvertLineToPassport(line)

  if got != want {
    t.Errorf("got %+v want %+v given, %s and", got, want, line)
  }
})

Once I had confidence that my code was 100% working, I moved onto part 2.

For part 2 I needed to determine whether a given passport was valid. I created the IsPassportValidPart1 function and three test cases.

An example test:

t.Run("returns false as passportID is blank (required)", func(t *testing.T) {
  want := false
  got := IsPassportValidPart1(Passport{
    eyeColor:       "gry",
    passportID:     "",
    expirationYear: "2020",
    hairColor:      "#fffffd",
    birthYear:      "1937",
    issueYear:      "2017",
    countryID:      "147",
    height:         "183cm",
  })

  if got != want {
    t.Errorf("got %t want %t", got, want)
  }
})

Using TDD for these parts gave me complete confidence that my code was working as expected meaning that once I'd counted the valid passports, I was sure that my answer would be correct - and it was!

This task may have been a little simple to highlight the benefits of TDD, but day 4 part 2 required stricter rules and TDD came into its own by ensuring that each rule was implemented correctly. Check out the tests on GitHub if you don't believe me!

Summary

If you check my GitHub you'll see that I've fallen behind on this years Advent of Code, I aim to complete it during the Christmas holidays! Nevertheless I've already gained more than enough skills and experience with Go, TDD and general programming to see that Advent of Code is definitely worth doing each year.

If you do take on Advent of Code I recommend:

  1. Using TDD and the examples given as input/output - it gives you so much confidence in your solution.
  2. Pick a language that you're not 100% confident in/are learning, you can always change later if the problems become too difficult - you'll gain a lot of skills at a relatively fast pace.

Hopefully this was an interesting read!

Let me know in the comments why you took on the 2020 Advent of Code challenge, how far you've got and what approach you took!

React, comment and follow on