David Conmy

I am a Software Engineer based in Galway, Ireland.
Currently I am focused on full-stack web application development in the cloud.

Breakout running on my Chip-8 Emulator

TDD Example - I Built an Emulator

25 Feb 2020 - David Conmy

I built an emulator as an exercise in developing a project using Test-Driven Development (TDD).

At the beginning of starting this project I had the following goals:

  • Develop solely using TDD
  • See how I can approach a problem with TDD principles
  • Investigate how testing first influences how I develop a codebase
  • Develop something cool
Screenshot of Pong on my completed emulator
Screenshot of Pong on my completed emulator

Non-Trivial

I wanted a project that would be non-trivial.

Whenever I’m trying a new framework or tool, my go-to project is a TODO list. This is the kind of project example you see all the time on different blogs or How-To articles. After several iterations, I would call this a trivial implementation. This is what I wanted to avoid. I thought that if I had a new problem to solve it would give me a better understanding of whether TDD was a workflow that I could feasibly use to solve difficult problems or problems that I haven’t faced before.

I wanted to create a project outside of my “comfort zone”. I also wanted to make something cool.

Inception

One of the benefits of having a background in Electronic Engineering is that I have quite a good fundamental understanding of Embedded Systems and Digital Systems Design. My Google suggestions are littered with articles about electronic devices, gaming devices and the latest articles from the world of application development. An emulator is a nice mix of all of these things. I was delighted when this resulted in Google suggesting an article written by someone else who created an NES emulator.

Emulators were always something that interested me from the first time I saw an emulator magazine. It was advertising that I could play 100s of retro games from my PC at home by installing a small program. It even came with a disk with several emulators on it and even a few ROMS to run on them. (I’m sure these were all openly available and non-copyrighted ROMs - all above board and legal)

I also knew an NES emulator would be a timely project. It was maybe a project that would be too big for a test-run of a new programming approach but it was a project that pulled my attention.

Enter the Chip-8.

Chip 8

Chip 8 is an interpreted programming language and not a physical chip so emulating it is a little different from emulating actual hardware but the principles are the same (or at least have some cross-over). It’s a simple machine definition that originally ran on a 1970’s COSMAC VIP among other machines of that era. What’s cool is that eventually, interpreters of chip 8 started to appear on graphing calculators in the 1980’s. I can only imagine the feeling of finding a way to play bootleg pong on a graphing calculator in the 80’s.

I will admit that I knew very little about this at the time of wanting to start this project but then I read that the Chip8 is a great starting point for learning about writing an emulator. Its instruction set is relatively small, the hardware definition isn’t very complex, and it will get across the major design points of writing an emulator that I could write in a relatively short time.

I found this article about how to write a chip 8 emulator, pinned the wiki page of the Op-Codes in my browser, re-read the article, and then set about writing my first emulator using a TDD approach.

Breakout on my Chip8 emulator
Breakout on my Chip8 emulator

TDD

The practice of TDD at its core is about iterating over the following steps:

  • Write a Test
  • Watch it Fail
  • Make it Pass
  • Refactor

The skill is in the approach to each of the steps.

There’s a skill to writing tests. There’s a skill in making tests pass. There’s a skill to Refactoring. There’s also a skill in watching the tests fail.

A benefit that I find with this approach is that because I iterate over the same steps in a processed way, it becomes a habit to continue working on harder problems because I always have the next step to fall back on. I find myself less burdened with the overall picture and can focus on the task in hand. Sometimes when the problem is non-trivial I find that I could stop to think for a very long time. I can get distracted with branching complexity or parallel concerns and I can lose myself in thinking about the problem. This can lead to me not addressing the original problem or spending a long time in the thinking side to the developer’s job rather than the more measurably productive aspect of writing the actual code.

It is still important to think critically (always) but with the workflow of TDD and an iterative approach to development, I can set aside my overall worries and compartmentalize the issues knowing that I will not forget about them. The most advantageous outcome of this is that I feel like I am always making progress - even if I keep finding parts of my code to improve.

My Process

I won’t cover the process of writing the entire code here but I will give an example of a particular iteration or two that helped me solve a bigger problem.

When I first started, I considered the overall shape of what the emulator would have to do and broke it down in a chart.

Chip-8 proposed process
Chip-8 proposed process

The app would have to do the following things:

  1. Load the application binary
  2. Read the next op-code into memory
  3. Perform the op-code operation
  4. Loop back to step 2 until done

The questions I had at this stage were two-fold:

  • How should I parse the op-code into a command to perform?
  • How would I test:
    • that the op-code gets translated to the correct operation to perform
    • that the code for performing the operation actually performs the correct action?

At first glance, this is looking like a large “switch” statement based on what op-code is read in at the time. In the diagram above, you can see I saw the “Op” growing based on the number of operations I had coded.

I picture something like this:

switch (opCode) {
    case 0x0001:
        // perform operation for 0x0001
        break;
    case 0x0002:
        // perform operation for 0x0002
        break;
    case 0x0003:
        ...
}

Considering that each operation could actually contain a few steps, this looked hard to test. (It’s also not very object-oriented and I was picking up on the potenital “code-smell” of a large conditional block influencing the flow of the code)

Instead, I decided to break it down further. What if I decoded the operation into some form of object that would then perform the required action?

Chip-8 proposed process 2
Chip-8 proposed process 2

I still forsee a switch statement for decoding the op-code but I feel better that I was able to separate the actual work of performing the operation and the work of doing the decoding of the op-code. I also feel better that I will be able to test these two parts separately now.

Everything above isn’t really about testing yet, is it? That changes now. Let’s finally do some coding and create our first test.

Iteration 1

Write a Test

We will need some form of factory like we see in the diagram above. This factory will decode the op-code into the correct type of class we expect that will be able to perform the operation.

package me.conmy.emu.chip8.operations;

import org.junit.Test;
import org.junit.Assert;

import static org.hamcrest.CoreMatchers.instanceOf;

public class OperationFactoryTest {

    @Test
    void createReturnFromSubroutineOperationWithCode0x00EE() {
        char opCode = 0x00EE;
        Class expectedClass = ReturnFromSubroutineOperation.class;

        Operation op = OperationFactory.decodeOpCodeToOperation(opCode);

        Assert.assertThat(op, instanceOf(expectedClass));
    }
}
Notes on the above:
    I used a char to store the opCode because char in Java is 2 bytes long. I could have used a `short` also - which is also 2 byts long. In retrospect, I think I should have stuck with `short` and `integer` data types for all codes like this (and I may refactor this in the future to do so). 
    At the time, I was worried about assigning an unsigned value to a signed data type. I expected problems if I did this especially around comparisons in later operations.
    Instead, I went with an approach that involved character manipulation and parsing to and from number data-types. Maybe this is a lesson for me in premature optimization?

This test passes an opCode to an OperationFactory that will attempt to decode the opCode. Our Test expects that the result of the “decode” method will be an object of type Operation and that this object is an instance of our expected class ReturnFromSubroutineOperation.

Since this is the only code I have written so far, it won’t compile. I consider this a failed test.

Watch it Fail

This is my first test. And it fails. GREAT.

Now I need to make this test pass.

Make it Pass

I create classes for all the complaints the compiler is giving me.


package me.conmy.emu.chip8.operations;

public class ReturnFromSubroutineOperation {

}

package me.conmy.emu.chip8.operations;

public interface Operation {

}

package me.conmy.emu.chip8.operations;

public class OperationFactory {
    public static Operation decodeOpCodeToOperation(char opCode) {
        return null;
    }
}

Now it compiles and I can run my test for real this time.

If I do this I get the following test failure:

java.lang.AssertionError:
Expected: an instance of me.conmy.emu.chip8.operations.ReturnFromSubroutineOperation
    but: null

This is good because we are closer to making the test work. I think it is worth mentioning that there are a number of approaches that I can take to the next step:

  1. Implement the agreed/decided implementation (from design phase?)​
  2. Implement the obvious solution​
  3. Fake it until you make it​

I’m going to continue to “Fake-it” by implementing just enough to get the test to pass.

My assertion is expecting a class of type ReturnFromSubroutineOperation but my OperationFactory is returning null. I’ll fix this first.

package me.conmy.emu.chip8.operations;

public class OperationFactory {
    public static Operation decodeOpCodeToOperation(char opCode) {
        return new ReturnFromSubroutineOperation();
    }
}

When I check my test now it still doesn’t pass. This is because the OperationFactory is set up to return an object of class Operation but our ReturnFromSubroutineOperation isn’t an Operation - yet. It should be. Let’s implement that now too.

package me.conmy.emu.chip8.operations;

public class ReturnFromSubroutineOperation implements Operation {

}

After I do all of this, my test passes! I can move on to the last step of an iteration - Refactor.

Refactor

It is at this step that I would review the code that I just wrote and see if there is anything that could be refactored. I look for the usual suspects:

  • Repeated code snippets
  • Ugly “work-arounds” that could be achieved in a more elegant way
  • Naming convention
  • Known anti-patterns that I can spot

Since this is the first iteration, I don’t recognise anything major so I skip any refactoring.

This will be the only time I’ll feel comfortable skipping this step. The codebase is too new to have any repeated code or repeated logic and I deem any improvements I could see to be achieved by adding more tests and tackling them in that iteration. I should make a note of them now as part of this step.

  1. The Operation interface has no defined methods that an implementation would need to implement
    • Add a doOperation method to this interface that will be called to actually “do” the operation
  2. ReturnFromSubroutineOperation is a stub implementation class and doesn’t actually do anything
    • Implement the Return-From-Subroutine action itself (in the doOperation method)
  3. decodeOpCodeToOperation method doesn’t actually do anything with the opCode.
    • Decode and return different operations based on the opCode provided

I like to add TODO comments in the code (especially if it’s a personal project) because I can look these up in the IDE next time I load it and it gives me potential places to start working on the code again straight away. Some team scenarios might not like merging or committing code with TODOs in it so a separate TODO list or marking the TODOs as tasks in the team backlog (maybe even as sub-tasks of the current backlog item you’re working on if they are needed before completion of your current task) could also suffice.

I mention this because I see TDD as part of a workflow and managing tasks and TODOs is part of this workflow. They come hand-in-hand.

Now it would be time to start Iteration 2.

Judging from the list I created of next steps in the refactor step, I have a number of things I could do next. Which one to do next is up to choice but in a TDD approach I would implement them using the same method:

  • Write a Test
  • Watch it Fail
  • Make it pass
  • Refactor

Conclusion

Full code for this project can be found on my github page https://github.com/Conmy/chip8-emulator

TDD is a powerful tool and I hope from reading this piece you can get a feel for how useful it is even from the very beginning of a project.

One of the biggest advantages I find is in the case where I get interrupted from my work. When I come back there would usually be a cognitive burden to try get back into the difficult problem I was trying to solve. With TDD this burden is softened a little because I can assess which step of the “workflow” I was last on and continue from there.

Bonus points if I got interrupted or left my work just after writing a test - when I come back, I have a failing test to get working and I can dive straight in with the “Fake it ‘till you make it” strategy and start writing code almost immediately.

I will admit that adopting TDD for someone who is new to testing may slow down initial returns. As all good workflows are, it is a discipline and considering the well documented benefits of tests in code (in general) and the idea that adopting a TDD approach puts writing tests to the forefront for a developers workflow (and they are no longer an afterthought). I think learning the discipline is worthwhile.

I don’t always code with TDD to the extent my example gives above but when a problem is becoming difficult or if I’m stuck in a rut, TDD and the individual steps are always there to give me something to do next and allow me to increase my output during these times.

Other Blog Posts on this Site

jekyll logo

How it's Made

How I made this website
breakout on a chip8 emulator

I Made an Emulator

An Example of TDD in action