tdd-deciphered.com

created by @parsingphase

Chapter 7: A closer look at the ring setting

So, we’ve decided we need to test for a valid ring offset; we're going to be rolling these rotors around later on, and if they can get stuck in illogical positions we will get very confused.

So what defines a valid offset, and what does that offset mean? We could take a look at our system and say that we're looking at an offset in an array of mappings, and so that'll logically go from 0-25. But does that make sense with the hardware? Would it be meaningful to an original Enigma user or codebreaker?

Well, yes and no. It's incredibly tricky trying to nail down precise details for an 80-year old hardware device that was classified as Secret until 30 years ago, and most of the books on the topic weren't written for software engineers trying to write simulators. But you're lucky, because this site is.

The "yes and no" answer lies in the fact that there were at least two dozen different types of Enigma device (see the Enigma Family Tree for details), and that different users (eg the German Army, or the Japanese Diplomatic Service) used them differently. We need to find a way to specify the offset that would make sense to all of them.

As it turns out, while most Enigma users appear to have used letters to indicate positions and offsets, the German Army (the 'Wehrmacht') tended to use devices with numbered (rather than lettered) rotors and may (so far as I can tell) also have specified ring positions numerically. So we'll start off with that convention.

(For the avoidance of confusion, the typical Wehrmacht machine, the Type I, still had 26-position rotors. A later 10-position purely numeric system, the Z-series, was barely used).

So, we've said numeric's OK, but what numbers? Zero-indexed is natural to us, but these systems were all built before the computer age, and conventions and nomenclature were often different from what we're used to. Would 1-indexed make more sense?

Well, rather helpfully, the book Codebreakers: The Inside Story of Bletchley Park happens to have a picture of a daily settings sheet for a whole month, and in the numbers under "Ring Offsets" it contains one 26, and no zeros.

So, 1-26. We can add a test for this for now, and worry about what we mean by it later. We can be rather more confident about an alphabetic offset; only A-Z can make any sense.

Writing the numeric test first gives us a chance to decide which conditions we should test. We know that: - A rotor offset that's not numeric should fail (we'll ignore the character case for now and add that in the next iteration). - Any rotor offset less than one must fail. - Any rotor offset greater than 26 must fail.

Now, we could meet all these requirements just by rejecting all input (this is known as "smartarse TDD"), so let's also state what must pass: - Any integer from 1 to 26 inclusive

Now, there's an infinite number of possible inputs, so which should we test against? One principle that will serve use well here is to test against "boundary conditions"; locations where the data is either just right, or just wrong.

For example: 0: just wrong; integer, but slightly too low 1: just right; integer, the lowest that passes 26: just right; integer, the highest that passes 27: just wrong; integer, just too high 0.99; just wrong; numeric, too low and not integer 25.99; just wrong; numeric, in range but not integer

The last two probably aren't pure boundary conditions, but they're still useful tests. We'll also add: 'A'; wrong datatype for our current spec - remember that we can update our tests as we update specs and system capabilities.

We can use these rules to build a couple of tests, each with their own sets of test data, which we’ll provide with a PHPUnit feature called “Data Providers”.

Firstly, modify testSetAndRememberRingOffset to accept a data provider:

/**
 * Ensure we accept a range of valid ring offset values
 *
 * @dataProvider validRingOffsetProvider
 * @param int $ringOffset
 */
public function testSetAndRememberRingOffset($ringOffset)
{
    $rotor = new Rotor();
    $rotor->setRingOffset($ringOffset);
    $this->assertSame($ringOffset, $rotor->getRingOffset());
}

public function validRingOffsetProvider()
{
    return [
        [1], // minimum good value
        [15], // our previous mid-range good value
        [26] // our maximum good value
    ];
}

Test… green… good.

Now let’s set up the bad data and make sure it fails:

/**
 * Ensure we accept a range of valid ring offset values
 *
 * @dataProvider badRingOffsetProvider
 * @expectedException \InvalidArgumentException
 * @param int $ringOffset
 */
public function testRejectBadRingOffset($ringOffset)
{
    $rotor = new Rotor();
    $rotor->setRingOffset($ringOffset);
}

public function badRingOffsetProvider()
{
    return [
        [0], // just too low
        [27], // just too high
        [0.99], // Not integer, and too low
        [1.99], // Not integer, but in range
        ['A QA engineer walks into a bar'] // just wrong
    ];
}

Test.. red. Because we’ve not added the checks to the setter yet. Add those:

/**
 * @param string $ringOffset
 */
public function setRingOffset($ringOffset)
{
    if(!is_integer($ringOffset) || ($ringOffset<1) || ($ringOffset>26)) {
        throw new \InvalidArgumentException("Offset must be integer in range 1..26");
    }
    $this->ringOffset = $ringOffset;
}

Test… green… all good. Time for a cuppa. Git tag: Chapter7-1-RingOffsets