DEV Community

Simone Gentili
Simone Gentili

Posted on

Table driven test solving part of the mars rover kata with Go

About kata

Kata is a Japanese word meaning "form" and refers to a detailed choreographed pattern of martial arts movements made to be practised alone. It can also be reviewed within groups and in unison when training.

In coding, the word “Kata” stands for the problem you’re going to solve over and over again, dealing with different constraints in each session.

The mars rover kata

The mars rover kata is composed of the following six points. During a kata is not necessary to complete all the exercise. The goals is just to learn and practice with test driven development.

In this article a keep just a part of these points because sometimes, during the test driven development is necessary repeat same test with different inputs and create different tests is useless.

  • You are given the initial starting point (x,y) of a rover and the direction (N,S,E,W) it is facing.
  • The rover receives a character array of commands.
  • Implement commands that move the rover forward/backward (f,b).
  • Implement commands that turn the rover left/right (l,r).
  • Implement wrapping at edges. But be careful, planets are spheres. Connect the x edge to the other x edge, so (1,1) for x-1 to (5,1), but connect vertical edges towards themselves in inverted coordinates, so (1,1) for y-1 connects to (5,1).
  • Implement obstacle detection before each move to a new square. If a given sequence of commands encounters an obstacle, the rover moves up to the last possible point, aborts the sequence and reports the obstacle.

The rover in a sphere

Before start with the test we need more context for the kata itself. We need a rover and a world the rover should move inside. The World here is intended as a Sphere. The sphere is modeled as a grid: a square with two corner made with a point. So here first two items needed to proceed with the example:

type Point struct {
    x int
    y int
}

type Sphere struct {
    firstCorner Point
    lastCorner Point
    obstacles []Point
}

type Rover struct {
    position Point
    direction string
    world Sphere
}
Enter fullscreen mode Exit fullscreen mode

Obstacles

In the complete mars rover kata one problem to solve is also the obstacle detection. In this article this step is not treated. here the problem is solved entirely and you can see my solution for this problem.

Write a unit test

Let's go testing? Absolutely yes!! The test we need to write aims to solve the movement problem of the mars rover kata. In the code below we have a Sphere and an empty list of obstacles. Our rover, in this world, start from 1,1 coordinates and is oriented to North.

First test made could be the following.

func TestRoverMovement(t *testing.T) {
    world := Sphere{Point{-99,-99},Point{99,99},[]Point{}}
    rover := Rover{Point{1,1},"n",world}
    rover.move("f")
    pos := rover.position
    if pos != Point{1,2} {
        t.Fatal(pos,"position should be",Point{1,2})
    }
}
Enter fullscreen mode Exit fullscreen mode

The production code for current test could be the following. All directions and movements are missing here because missing are also all tests.

func (r *Rover) move (command string) {
   r.update(r.position.x,r.position.y+1)
}
Enter fullscreen mode Exit fullscreen mode

Extract variables from hardcoded test

Before start with the table we have to take a look in the code below. We have to separate the code from the parts we need to change. Instead of create N tests, we will use same test with N inputs. First of all I saw

  • rover position at the beginning
  • rover position at the end
  • rover direction
  • received command

We can now transform the code below

func TestRoverMovement(t *testing.T) {
    world := Sphere{Point{-99,-99},Point{99,99},[]Point{}}
    rover := Rover{Point{1,1},"n",world}
    rover.move("f")
    pos := rover.position
    if pos != Point{1,2} {
        t.Fatal(pos,"position should be",Point{1,2})
    }
}
Enter fullscreen mode Exit fullscreen mode

in this manner

func TestRoverMovement(t *testing.T) {
    // sphere
    firstCorner := Point{-99,-99}
    lastCorner := Point{99,99}
    obstacles := []Point{}
    // rover
    position := Point{1,1}
    direction := "n"
    destination := Point{1,2}

    world := Sphere{firstCorner,lastCorner,obstacles}
    rover := Rover{position,direction,world}
    rover.move("f")
    pos := rover.position
    if pos != destination {
        t.Fatal(pos,"position should be",destination)
    }
}
Enter fullscreen mode Exit fullscreen mode

This brings me to the following struct.

type TestCases struct {
    firstCorner Point
    lastCorner Point
    obstacles []Point
    position Point
    direction string
    destination Point
}
Enter fullscreen mode Exit fullscreen mode

And list of inputs can now be written in this manner. The code is trivial and the table is nothing but an array of the struct.

tests := []TestRover{
    {
        Point{-99,-99},
        Point{99,99},
        Point{1,1},
        Point{0,1},
        "n",
        "f",
    },
}
Enter fullscreen mode Exit fullscreen mode

Now we can change test using the table of inputs instead of single varibles:

func TestRoverForwardMovements(t *testing.T) {
    tests := []TestCases{
        {
            Point{-99,-99},
            Point{99,99},
            Point{1,1},
            Point{0,1},
            "n",
            "f",
        },
    }

    for _, tc := range tests {
        world := Sphere{
            tc.firstCorner,
            tc.lastCorner,
            tc.obstacles,
        }
        rover := Rover{tc.position,tc.direction,tc.world}
        rover.move("f")
        pos := rover.position
        if pos != tc.destination {
            t.Fatal(pos,"position should be",tc.destination)
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

The final result

The following is the final result of a test written to solve the mars rover kata. Code comes from the full example of code that is available here.

Any time you play with a kata both test and code may change. The code you can see in this article was made after I "solved" the kata. First go implementation was the following.

func TestRoverPositions(t *testing.T) {
    tests := []TestRover{
        // foreward from origin
        {Point{0,0},Point{0,1},"n","f"},
        {Point{0,0},Point{0,-1},"s","f"},
        {Point{0,0},Point{-1,0},"w","f"},
        {Point{0,0},Point{1,0},"e","f"},
        // foreward from another point
        {Point{2,0},Point{2,1},"n","f"},
        {Point{2,0},Point{2,-1},"s","f"},
        {Point{2,0},Point{1,0},"w","f"},
        {Point{2,0},Point{3,0},"e","f"},
        // backward from origin
        {Point{0,0},Point{0,-1},"n","b"},
        {Point{0,0},Point{0,1},"s","b"},
        {Point{0,0},Point{1,0},"w","b"},
        {Point{0,0},Point{-1,0},"e","b"},
        // backward from another point
        {Point{2,0},Point{2,-1},"n","b"},
        {Point{2,0},Point{2,1},"s","b"},
        {Point{2,0},Point{3,0},"w","b"},
        {Point{2,0},Point{1,0},"e","b"},
    }

    for _, tc := range tests {
        rover := Rover{
            tc.position,
            tc.direction,
            Sphere{
                Point{-99,-99},
                Point{99,99},
                []Point{},
            },
        }

        rover.move(tc.command)
        if rover.position != tc.destination {
            t.Fatal(
                rover,
                tc.position,
                "position should be",
                tc.destination,
            )
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

Instead of tens of tests we have the abstraction of a test and a source of inputs and output. Other patterns can be applied to the test driven development. I hope you enjoyed reading this pattern in current article. Please, ... if you use another ones, any suggestion in comments is appreciated.

Bye bye!

Oldest comments (0)