Chapter 23: running the tests, and the Journal

Chapter 23 ends two ways: fifteen tests that cover the record, the class and the functions, and a game you can actually play. This lesson plays the first ending, and then looks inside the class those tests already exercised, through the Journal. To play it in the IDE: Help > Lessons > Writing code…, or ek9 -ide -lesson book-game-journal.

The EK9 IDE as the lesson opens

Start from

The code the lesson starts from: copy it into a file of your own and follow the steps.

Guess.ek9

#!ek9
defines module book.guess

  defines record

    Score
      who as String: String()
      attempts as Integer: Integer()

      default Score() as pure

      Score() as pure
        ->
          name as String
          tries as Integer

        who :=? String(name)
        attempts :=? Integer(tries)

      operator $ as pure
        <- rtn as String: `${who} (${attempts})`

      operator <=> as pure
        -> other as Score
        <- rtn as Integer: attempts <=> other.attempts

      operator #? as pure
        <- rtn as Integer: #?attempts

      default operator ?

  defines class

    //Holds one game: the secret, how many tries are allowed, and how it is going.
    Game
      secret as Integer: Integer()
      maxTries as Integer: Integer()
      tries as Integer: 0
      solved as Boolean: false

      default private Game() as pure

      Game() as pure
        ->
          chosen as Integer
          allowed as Integer

        secret :=? Integer(chosen)
        maxTries :=? Integer(allowed)

      //Take one guess. Count it, say which way to go, and note a win.
      guess()
        -> number as Integer
        <- rtn as String: String()
        tries += 1
        rtn: hint(secret, number)
        if number == secret
          solved := true

      over() as pure
        <- rtn as Boolean: solved or tries >= maxTries

      triesUsed() as pure
        <- rtn <- Integer(tries)

      report() as pure
        -> player as String
        <- rtn as String: String()
        rtn: solved <- `Well done ${player} - ${tries} tries.` : `Out of tries - the number was ${secret}.`

      default operator ?

  defines function

    //Compare a guess against the secret and say which way to go.
    hint() as pure
      ->
        secret as Integer
        guess as Integer
      <- rtn as String: String()

      order <- guess <=> secret
      rtn: order < 0 <- "higher" : order > 0 <- "lower" : "correct"

    //Put a score board in order: fewest tries first.
    rank() as pure
      -> board as List of Score
      <- rtn as List of Score: List() of Score
      rtn: cat board | sort | collect as List of Score

    //The scores already on the board when a new game starts.
    startingBoard()
      <- rtn as List of Score: List() of Score
      rtn += Score("Ada", 3)
      rtn += Score("Alan", 5)

    //Add this game's score to the opening board.
    finalBoard()
      ->
        player as String
        tries as Integer
      <- rtn as List of Score: List() of Score

      rtn: startingBoard()
      rtn += Score(player, tries)

    //Render the ranked board as one block of text, best first.
    boardLines()
      -> board as List of Score
      <- rtn as String: ""
      for entry in rank(board)
        rtn += `  ${entry}\n`

    //Work out the player's name from the command-line options.
    playerName()
      -> argv as List of String
      <- rtn as String: String()
      usage <- "Usage: Guess [-p name]"
      supported <- {
        "-p": ":"
      }
      rtn: GetOpt(":", supported, usage).options(argv).getOrDefault("-p", "Player")

  defines program

    Guess()
      -> argv as List of String

      stdin <- Stdin()
      stdout <- Stdout()

      player <- playerName(argv)
      high <- 100
      maxTries <- 7
      game <- Game(OS().random(1, high), maxTries)

      stdout.println(`I am thinking of a number from 1 to ${high}. You have ${maxTries} tries.`)

      while not game.over()
        number <- Integer(stdin.next())
        stdout.println(`  ${number}: ${game.guess(number)}`)

      stdout.println(game.report(player))
      stdout.println("Fewest tries wins:")
      stdout.print(boardLines(finalBoard(player, game.triesUsed())))

dev/tests.ek9

#!ek9
defines module book.guess.tests

  references
    book.guess::boardLines
    book.guess::finalBoard
    book.guess::Game
    book.guess::hint
    book.guess::playerName
    book.guess::rank
    book.guess::Score
    book.guess::startingBoard

  defines program

    @Test
    HigherTest()
      assert hint(50, 20) == "higher"

    @Test
    LowerTest()
      assert hint(30, 80) == "lower"

    @Test
    CorrectTest()
      assert hint(70, 70) == "correct"

    @Test
    ScoreShowsNameAndTries()
      assert $Score("Ada", 3) == "Ada (3)"

    @Test
    FewerTriesComeFirst()
      strong <- Score("Grace", 1)
      weak <- Score("Alan", 6)
      assert strong <=> weak < 0
      assert strong?

    @Test
    EqualScoresHashAlike()
      assert (#?Score("Grace", 1)) == (#?Score("Grace", 1))

    @Test
    GameIsSet()
      assert Game(8, 9)?

    @Test
    WinningGame()
      game <- Game(42, 7)
      assert game.guess(50) == "lower"
      assert not game.over()
      assert game.guess(42) == "correct"
      assert game.over()
      assert game.triesUsed() == 2
      assert game.report("Sam") == "Well done Sam - 2 tries."

    @Test
    LosingGame()
      game <- Game(30, 3)
      assert game.guess(10)?
      assert game.guess(90)?
      assert game.guess(25)?
      assert game.over()
      assert game.report("Sam") == "Out of tries - the number was 30."

    @Test
    RankPutsFewestFirst()
      board <- [
      Score("Cleo", 5),
      Score("Bob", 4)
      ]
      best <- rank(board).getOrDefault(0, Score())
      assert best.who == "Bob"

    @Test
    BoardOpensWithAda()
      opening <- rank(startingBoard()).getOrDefault(0, Score())
      assert opening.who == "Ada"

    @Test
    TopScoreJoinsBoard()
      best <- rank(finalBoard("Max", 1)).getOrDefault(0, Score())
      assert best.who == "Max"

    @Test
    BoardLinesListsEveryone()
      lines <- boardLines([Score("Ida", 7), Score("Uma", 2)])
      assert lines contains "Ida (7)"
      assert lines contains "Uma (2)"

    @Test
    PlayerNameFromFlag()
      assert playerName(["-p", "Zoe"]) == "Zoe"

    @Test
    PlayerNameDefaults()
      assert playerName(List() of String) == "Player"

Step 1: Running the tests

Run ▸ Run Tests (⌘T) runs every @Test in the project - all fifteen of them - and reports pass or fail, the same as ek9 -t at the command line. Choose Run Tests.

The EK9 IDE once this is done: Run ▸ Run Tests (⌘T) runs every @Test in the project - all fifteen of them - and reports pass or fail, the same as ek9 -t at the command line

The Tests tab lists all fifteen, each with a green tick. They cover Score, Game and the six functions - everything the chapter's own prose calls out as tested. The one thing no test here touches is Guess itself: the interactive loop, which needs a keyboard, not an assertion.

The EK9 IDE once this is done: The Tests tab lists all fifteen, each with a green tick

Step 2: The Journal

Choose Run > New Journal Investigation…

The EK9 IDE once this is done: Choose Run > New Journal Investigation…

The project has several programs - Guess, and every @Test. Choose WinningGame, watch Game.tries, and leave the input sets blank: WinningGame takes no arguments, so one line is one run with none. Choose Start.

The EK9 IDE just before choosing Start: The project has several programs - Guess, and every @Test

The test runs while the Journal records every change to Game.tries. Every assert in WinningGame holds, and the run shows in the Journal tab as pass.

The EK9 IDE once this is done: The test runs while the Journal records every change to Game.tries

A run with no symptom opens at its WHOLE story, oldest first: Game.tries starts at 0, then becomes 1 - the first guess, 50, which guess() counted before it compared - then 2, the winning guess, 42. Two ticks, one for each call to guess() the test makes: the class was already proved correct, long before any program used it.

The result

All fifteen tests pass, covering everything but the interactive loop - and the Journal showed exactly what WinningGame's own asserts already proved: tries counted up, one for each guess, inside a class that was tested long before the program that uses it ever ran.

Where this lesson comes from