Test-first practice with PHP 8.1

Photo by Tara Winstead: https://www.pexels.com/photo/white-and-black-letter-t-7666402/

What you’ll learn in this article

This is the first part of an ongoing series about programming with PHP and different topics. This article is an introduction to the problem and a very basic implementation of the first requirement.

Mars Rover Kata

Overview

You’re part of the team that explores Mars by sending remotely controlled vehicles (Rover) to the surface of the planet.

   ┌────────────────────────────────────────────┐
│ │ │ │ │ │ │ │ │ │
8 │ ───┼────┼────┼────┼────┼────┼────┼────┼─── │
│ │ │ │ │ │ │ │ │ │
7 │ ───┼────┼────┼────┼────┼────┼────┼────┼─── │
│ │ │ │ │ │ │ ┌─┴─┐ │ │
6 │ ───┼────┼────┼────┼────┼────┼──┤7,6├──┼─── │
│ │ │ │ │ │ │ └─┬─┘ │ │
5 │ ───┼────┼────┼────┼────┼────┼────┼────┼─── │
│ │ │ │ │ │ │ │ │ │
4 │ ───┼────┼────┼────┼────┼────┼────┼────┼─── │
│ │ │ ┌─┴─┐ │ │ │ │ │ │
3 │ ───┼────┼──┤3,3├──┼────┼────┼────┼────┼─── │
│ │ │ └─┬─┘ │ │ │ │ │ │
2 │ ───┼────┼────┼────┼────┼────┼────┼────┼─── │
│ │ │ │ │ │ │ │ │ │
1 │ ───┼────┼────┼────┼────┼────┼────┼────┼─── │
│ │ │ │ │ │ │ │ │ │
y └────────────────────────────────────────────┘
x 1 2 3 4 5 6 7 8

Rules

  • A rover can’t escape the world, if it reaches the edges,
    it appears on the other side (like in a game like Snake or Pac-Man)
  • There are other obstacles on Mars the rover can’t move over
  • The rover is dropped from a spaceship at an initial starting point (x, y) and a cardinal direction (N, S, W, E) the rover is facing
  • The Rover receives a collection of commands (character string):
    f for forward movement, b for backward movement
    l for turn left, r for turn right (does not change x or y)

Tasks

  • Drop the rover on the planet at an initial position (x, y, cardinal direction)
  • Add position reporting (e.g., “1,1 N” for bottom left corner facing north)
  • Implement commands that move the rover forward/backward
  • Implement commands that turn the rover left/right
  • 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

Considerations

Test driven development does not mean you don’t need a plan. Of course, some design evolves from writing tests first, but you should have (at least) a big-picture architecture and design in mind before starting to write code.

Layered architecture with Commands and Queries

Setup

I set up a simple Docker container and a standard Symfony project. I won’t go into details about this.

The first test

I add a file in my projects tests folder tests/RoverDropTest.php (in anticipation of the first use case) and just call the TestCase::fail method. When I run this test, the test obviously fails ❌ and thereby confirms that I set up everything correctly. Perfect!

The first use case

The first use case is to drop the Rover on Mars and then check that the Rover reports its initial position.

The TDD circle

Red ❌ — Green ✅ — Refactor

Outside-in methodology

I don’t want to couple my test code to my production code, so I’m trying to keep a bird’s eye view and just check the behaviour of my components at their borders (test only the public API). This is of course an opinionated view, and I’ll change my tactic in other parts (as you’ll see in later parts of the series), where coupling is not that much of a problem.

The failing test

I run the test, and it fails (expectedly). ❌

Error : Class “App\Tests\Mars” not found
/app/tests/RoverDropTest.php:17
Error : Class “App\Tests\DropRover” not found
Error : Call to undefined method App\Rover\Application\DropRover::with()
/app/tests/RoverDropTest.php:25
Error : Class “App\Tests\DropRoverHandler” not found
/app/tests/RoverDropTest.php:31
Error : Object of type App\Rover\Application\Handler\DropRoverHandler is not callable
/app/tests/RoverDropTest.php:32
Error : Class “App\Tests\GetRoverPosition” not found
Error : Class “App\Tests\GetRoverPositionHandler” not found
/app/tests/RoverDropTest.php:37
Error : Object of type App\Rover\Application\Handler\GetRoverPositionHandler is not callable
/app/tests/RoverDropTest.php:38
TypeError : App\Rover\Application\Handler\GetRoverPositionHandler::__invoke(): Return value must be of type string, none returned
/app/src/Rover/Application/Handler/GetRoverPositionHandler.php:18
/app/tests/RoverDropTest.php:38

Implement the first use case

Now that we have a green test and no compile errors, I’ll add the actual functionality.

Error : Class “App\Tests\Spaceship” not found
/app/tests/RoverDropTest.php:23
Error : Call to undefined method App\Rover\Domain\Entity\Spaceship::with()
/app/tests/RoverDropTest.php:24
Error : Class “App\Tests\Rover” not found
/app/tests/RoverDropTest.php:24
TypeError : App\Rover\Domain\Entity\Spaceship::with(): Argument #1 ($param) must be of type App\Tests\Rover, App\Rover\Domain\Entity\Rover given, called in /app/tests/RoverDropTest.php on line 25
/app/src/Rover/Domain/Entity/Spaceship.php:9
/app/tests/RoverDropTest.php:25
TypeError : App\Rover\Domain\Entity\Spaceship::with(): Return value must be of type App\Rover\Domain\Entity\Spaceship, none returned
/app/src/Rover/Domain/Entity/Spaceship.php:11
/app/tests/RoverDropTest.php:25
Error : Call to undefined method App\Rover\Domain\Entity\Rover::id()
/app/src/Rover/Domain/Entity/Spaceship.php:13
/app/src/Rover/Domain/Entity/Spaceship.php:18
/app/tests/RoverDropTest.php:25
TypeError : App\Rover\Application\Handler\DropRoverHandler::__construct(): Argument #1 ($mars) must be of type App\Rover\Domain\Entity\Mars, App\Rover\Domain\Entity\Spaceship given, called in /app/tests/RoverDropTest.php on line 37
/app/src/Rover/Application/Handler/DropRoverHandler.php:12
/app/tests/RoverDropTest.php:37

A passing test

I adapt the handler arguments and run the test again.

Removing the hardcoded values

Now I turn my attention to the Query handler GetRoverPositionHandler. The Query handler gets the Planet via dependency injection and we can query the Rover there.

Refactoring the test

I turn my attention to the test itself and make it more easily readable.

Preview

In the next article we’ll take a look at value objects and primitive obsession and how to secure our domain against invalid values.

Credits

Mars Rover Kata on kata-log.rocks

--

--

Programmer, Photographer, Gardener

Love podcasts or audiobooks? Learn on the go with our new app.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store