tdd-deciphered.com

created by @parsingphase

Chapter 6: Simulating a rotor

So far we've made one very simple comment about the Rotor class - that it's an encryptor - but we've made no attempt to actually model the real rotor's behaviour. In order to do that, we need to more closely specify the rotor's properties and capabilities:

  • A rotor has 26 inputs on its right hand side, and 26 outputs on its left hand side.
  • Each input is connected to exactly one output. The way in which it connects is specified by the internal wiring of the rotor, which is determined by the rotor’s identity, generally referred to by roman numerals.
  • A rotor consists of an internal disk or "core" carrying the wiring, and an external ring, often referred to as a tyre, which carries the letter indicators.
  • Each rotor has at least one carry notch on the left side of the ring, which can cause the next rotor up to move around one place.
  • The ring can be moved relative to the core. The offset is referred to as the "Ring Setting" or "Ringstellung".

The functionality we've already decided to provide, by agreeing to implement the Phase\Enigma\EncryptorInterface is "Output value for given input value".

So, with the information we now have, can we implement and test our Rotor class?

Well, not quite.

We can see that the behaviour of a single rotor depends on both the identity of the rotor (and thereby its internal wiring), and its Ringstellung. We need to specify both of these to be able to simulate the rotor. We could specify both of these as configuration options to our Rotor class - and, leaving aside issues of design purity for now, let's do so.

For the moment, we don't want to break our existing unit test by changing the rotor's constructor, so let's add setRingOffset and setInputOutputMapping functions to our Enigma_Rotor class. For testing convenience, we'll also add relevant get* functions as well.

However, since we're going for pure TDD at the moment, we add these to the tests first.

Add a second method to tests/Phase/Enigma/RotorTest.php:

public function testSetAndRememberRingOffset ()
{
    $rotor = new Rotor();
    $ringOffset=15;
    $rotor->setRingOffset($ringOffset);
    $this->assertSame($ringOffset,$rotor->getRingOffset());
}

Run the test, and of course it'll crash, because the functions aren't there. Adding them's easy enough:

Update Rotor.php to read:

<?php
class Rotor implements EncryptorInterface
{
    /**
     * @var string Single letter; Position of the ring relative to the core
     */
    protected $ringOffset;

    /**
     * @return string
     */
    public function getRingOffset()
    {
        return $this->ringOffset;
    }

    /**
     * @param string $ringOffset
     */
    public function setRingOffset($ringOffset)
    {
        $this->ringOffset = $ringOffset;
    }
}

Run the tests, and they pass, so we can move on to add the tests and functionality for the core mapping:

tests/Phase/Enigma/RotorTest.php gains:

public function testSetAndRememberCoreMapping ()
{
    $rotor = new Rotor();
    $coreMapping=array();
    $rotor->setCoreMapping($coreMapping);
    $this->assertSame($coreMapping,$rotor->getCoreMapping());
}

and we test, crash, and fix by adding to the Rotor class:

/**
 * @var array 26-element character-indexed array of input to output
 */
protected $coreMapping;

/**
 * @return array
 */
public function getCoreMapping()
{
    return $this->coreMapping;
}

/**
 * @param array $coreMapping
 */
public function setCoreMapping($coreMapping)
{
    $this->coreMapping = $coreMapping;
}

So, we pass again (or “go green” if you’re running phpunit with the --color parameter). But our empty mapping is of little functional use, which underscores the fact that the mere presence of tests won’t guarantee functional code. We have to write tests that assert the actual functionality we need.

So we want to reject invalid data, but how do we define valid data? The simplest solution would be an associative array, of input position => output position. For readability, we'll work with an array of upper case letters, and for the sake of our "valid mapping" test, we'll just insist (for now) that it has 26 elements.

We now want to do at least two tests: - Valid data is accepted. - Invalid data is rejected.

We'll defined some really simple valid data for now - a "pass-through" rotor that maps any input character to itself. This may seem simple, but there is a historical relevance to it that we'll cover later.

Comment out the testSetAndRememberCoreMapping function for the moment, as we no longer consider setting an empty array to be a valid test.

Replace it with a test for the valid 1-1 mapping:

public function testSetAndRememberValidCoreIdentityMapping ()
{
    $rotor = new Rotor();
    $coreMapping=array(
        'A'=>'A','B'=>'B','C'=>'C','D'=>'D','E'=>'E','F'=>'F','G'=>'G',
        'H'=>'H','I'=>'I','J'=>'J','K'=>'K','L'=>'L','M'=>'M','N'=>'N',
        'O'=>'O','P'=>'P','Q'=>'Q','R'=>'R','S'=>'S','T'=>'T','U'=>'U',
        'V'=>'V','W'=>'W','X'=>'X','Y'=>'Y','Z'=>'Z'
    );
    $rotor->setCoreMapping($coreMapping);
    $this->assertSame($coreMapping,$rotor->getCoreMapping());
}

Testing the code now will still pass - the main purpose of this method is to ensure that, once we start rejecting bad inputs, we keep accepting good ones. So let’s write a test to ensure that invalid inputs are rejected:

/**
 * Rotors should refuse to accept invalid arrays
 *
 * @expectedException \InvalidArgumentException
 */
public function testSetInvalidCoreMappingThrowsException()
{
    $this->setExpectedException('Exception');
    $rotor = new Rotor();
    $coreMapping=array(
        'A'=>'A','B'=>'B','C'=>'C','D'=>'D','E'=>'E','F'=>'F','G'=>'G',
        'H'=>'H','I'=>'I','J'=>'J','K'=>'K','L'=>'L','M'=>'M','N'=>'N',
        'O'=>'O','P'=>'P','Q'=>'Q','R'=>'R','S'=>'S','T'=>'T','U'=>'U',
        'V'=>'V','W'=>'W','X'=>'X','Y'=>'Y'
    );
    $rotor->setCoreMapping($coreMapping);
}

We’re doing something new here - we’re using a PHPDoc notation that PHPUnit will see and recognise as telling it that “this test function MUST see an InvalidArgumentException thrown”. We’re using a standard SPL exception here; see http://php.net/manual/en/class.invalidargumentexception.php

But if we run the test, it will fail:

There was 1 failure:

1) Phase\Enigma\RotorTest::testSetInvalidCoreMappingThrowsException
Failed asserting that exception of type "Exception" is thrown.

So we now add checks in our Rotor class’s setCoreMapping function for bad inputs. Firstly, check for arrays of the wrong size:

/**
 * @param array $coreMapping
 */
public function setCoreMapping($coreMapping)
{
    if (count($coreMapping) != 26) {
        throw new \InvalidArgumentException("Mapping must have 26 elements");
    }

    $this->coreMapping = $coreMapping;
}

Run tests… they pass.

So, we’ve put some basic testing on our rotor mapping - a bit too basic really, as we should really check that our mapping is exactly 26 upper-case characters, each to an upper-case character, and that each character appear on the input side and output side exactly once. (For the sake of brevity, I’m not going to implement that right now, although you can do it as an exercise). Do we really need to test quite this pedantically?

Well, testing a setter to this extent may verge on overkill, but by doing so (rather than just "writing what works") we’ve already considered one error condition we probably wouldn’t have spotted otherwise (the empty array). It’s also worth remembering that we’re not writing these tests just to confirm we can write the setter, but to test at any point in the future, when we’ve got thousands of lines of code in our application, that every single part of the app still works perfectly. If something breaks, and we’ve tested this thoroughly, we’ll know exactly what went wrong.

For the moment, though, we’ll take a breather, then return and look at more exhaustive testing of a slightly simpler value, the “ring offset”. Again, commit the code before we move on. I’ll stop telling you to do this now, I’ll just include the tags in my repo. We’re now at Chapter6-1-MappedRotors